From 5e93d376b515f2260656daeac3cefbacf7efa79e Mon Sep 17 00:00:00 2001 From: ebarrios Date: Fri, 10 Jul 2020 15:48:03 +0200 Subject: [PATCH] Removed Mock implementation and tried to add the real one --- plugins/github-actions/package.json | 2 + .../src/api/GithubActionsApi.ts | 29 +++ .../src/api/GithubActionsClient.ts | 154 ++++++++++++ plugins/github-actions/src/api/index.test.ts | 44 ++++ plugins/github-actions/src/api/index.ts | 19 ++ plugins/github-actions/src/api/types.ts | 224 ++++++++++++++++++ .../BuildDetailsPage/BuildDetailsPage.tsx | 11 +- .../BuildInfoCard/BuildInfoCard.tsx | 8 +- .../BuildListPage/BuildListPage.tsx | 20 +- .../BuildStatusIndicator.tsx | 2 +- plugins/github-actions/src/index.ts | 1 + 11 files changed, 497 insertions(+), 17 deletions(-) create mode 100644 plugins/github-actions/src/api/GithubActionsApi.ts create mode 100644 plugins/github-actions/src/api/GithubActionsClient.ts create mode 100644 plugins/github-actions/src/api/index.test.ts create mode 100644 plugins/github-actions/src/api/index.ts create mode 100644 plugins/github-actions/src/api/types.ts diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 456bd826c0..8ac474169f 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -21,7 +21,9 @@ "clean": "backstage-cli clean" }, "dependencies": { + "@backstage/catalog-model": "^0.1.1-alpha.13", "@backstage/core": "^0.1.1-alpha.13", + "@backstage/core-api": "^0.1.1-alpha.13", "@backstage/theme": "^0.1.1-alpha.13", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", diff --git a/plugins/github-actions/src/api/GithubActionsApi.ts b/plugins/github-actions/src/api/GithubActionsApi.ts new file mode 100644 index 0000000000..58892173ed --- /dev/null +++ b/plugins/github-actions/src/api/GithubActionsApi.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createApiRef } from '@backstage/core'; +import { Build, BuildDetails } from './types'; +import { Entity } from '@backstage/catalog-model'; + +export const githubActionsApiRef = createApiRef({ + id: 'plugin.githubactions.service', + description: 'Used by the Github Actions plugin to make requests', +}); + +export type GithubActionsApi = { + listBuilds: (entity: Entity, token: Promise) => Promise; + getBuild: (buildUri: string, token: Promise) => Promise; +}; diff --git a/plugins/github-actions/src/api/GithubActionsClient.ts b/plugins/github-actions/src/api/GithubActionsClient.ts new file mode 100644 index 0000000000..0c9a98e57c --- /dev/null +++ b/plugins/github-actions/src/api/GithubActionsClient.ts @@ -0,0 +1,154 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { GithubActionsApi } from './GithubActionsApi'; +import { Build, BuildDetails, BuildStatus, WorkflowRun } from './types'; +import { Entity } from '@backstage/catalog-model'; + +export class GithubActionsClient implements GithubActionsApi { + async listBuilds(entity: Entity, token: Promise): Promise { + // ### Feedback request ### + // I asumed the following: (maybe not the best. Ideally this should come from the link to the component.yaml file) + // entity.metadata.namespace => org name + // entity.metadata.name => repo name + // entityUri -> entity:spotify:backstage + + let url: string; + if (entity.metadata.name !== '') { + url = `https://api.github.com/repos/${entity.metadata.namespace}/${entity.metadata.name}/runs`; + } else { + url = 'https://api.github.com/repos/spotify/backstage/actions/runs'; + } + + const response = await fetch(url, { + headers: new Headers({ + Authorization: `Bearer ${await token}`, + }), + }); + + if (response.status > 200) { + return [ + { + commitId: 'Error', + message: 'ResponseCode > 200', + branch: 'Error', + status: BuildStatus.Failure, + uri: 'Error', + }, + ]; + } + + const data = await response.json(); + + const newData: WorkflowRun[] = data.workflow_runs; + + const endData: Build[] = []; + + newData.forEach((element, index) => { + const transData: Build = { + commitId: '', + message: '', + branch: '', + status: BuildStatus.Null, + uri: '', + }; + transData.commitId = String(element.head_commit.id); + transData.branch = element.head_branch; + + // ### Feedback request ### + // TODO: I am not sure about this part. Looks ugly. Maybe there is a better way of doing this. + if (element.conclusion === 'success') { + transData.status = BuildStatus.Success; + } else if (element.conclusion === 'failure') { + transData.status = BuildStatus.Failure; + } else if (element.conclusion === 'pending') { + transData.status = BuildStatus.Pending; + } else if (element.conclusion === 'running') { + transData.status = BuildStatus.Running; + } else { + if (element.status === 'in_progress') { + transData.status = BuildStatus.Running; + } else { + transData.status = BuildStatus.Null; + } + } + transData.message = element.head_commit.message; + transData.uri = element.url; + endData[index] = transData; + }); + + return endData; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async getBuild( + buildUri: string, + token: Promise, + ): Promise { + const response = await fetch(buildUri, { + headers: new Headers({ + Authorization: `Bearer ${await token}`, + }), + }); + const buildBlank: Build = { + commitId: '', + message: '', + branch: '', + status: BuildStatus.Null, + uri: '', + }; + + const dataBlank: BuildDetails = { + build: buildBlank, + author: '', + logUrl: '', + overviewUrl: '', + }; + + if (response.status > 200) { + return dataBlank; + } + + const data = await response.json(); + + const newData: WorkflowRun = data; + + dataBlank.author = newData.head_commit.author.name; + dataBlank.build.branch = newData.head_branch; + dataBlank.build.commitId = newData.head_commit.id; + dataBlank.build.message = newData.head_commit.message; + + // ### Feedback request ### + // TODO: I am not sure about this part. Look ugly. Maybe there is a better way of doing this. + if (newData.status === 'completed') { + dataBlank.build.status = BuildStatus.Success; + } else if (newData.status === 'in_progress') { + dataBlank.build.status = BuildStatus.Running; + } else if (newData.status === 'pending') { + dataBlank.build.status = BuildStatus.Pending; + } else if (newData.status === 'failure') { + dataBlank.build.status = BuildStatus.Failure; + } else { + dataBlank.build.status = BuildStatus.Null; + } + + dataBlank.build.uri = newData.url; + dataBlank.logUrl = newData.logs_url; + dataBlank.overviewUrl = newData.html_url; + + return dataBlank; + } +} diff --git a/plugins/github-actions/src/api/index.test.ts b/plugins/github-actions/src/api/index.test.ts new file mode 100644 index 0000000000..feff353115 --- /dev/null +++ b/plugins/github-actions/src/api/index.test.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { GithubActionsClient } from './GithubActionsClient'; +import { BuildStatus } from './types'; + +describe('Github Actions API', () => { + let client: GithubActionsClient; + beforeEach(() => { + client = new GithubActionsClient(); + }); + describe('Mock client', () => { + it('gets a list of builds by a project id', async () => { + await expect(client.listBuilds()).resolves.toEqual([]); + }); + it('gets a build info by its id', async () => { + await expect(client.getBuild()).resolves.toEqual({ + build: { + commitId: 'TODO', + branch: 'TODO', + uri: 'TODO', + status: BuildStatus.Running, + message: 'TODO', + }, + author: 'TODO', + logUrl: 'TODO', + overviewUrl: 'TODO', + }); + }); + }); +}); diff --git a/plugins/github-actions/src/api/index.ts b/plugins/github-actions/src/api/index.ts new file mode 100644 index 0000000000..9383250bfb --- /dev/null +++ b/plugins/github-actions/src/api/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './GithubActionsApi'; +export * from './GithubActionsClient'; +export * from './types'; diff --git a/plugins/github-actions/src/api/types.ts b/plugins/github-actions/src/api/types.ts new file mode 100644 index 0000000000..8a7b2ca548 --- /dev/null +++ b/plugins/github-actions/src/api/types.ts @@ -0,0 +1,224 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export enum BuildStatus { + Null, + Success, + Failure, + Pending, + Running, +} + +export type Build = { + commitId: string; + message: string; + branch: string; + status: BuildStatus; + uri: string; +}; + +export type BuildDetails = { + build: Build; + author: string; + logUrl: string; + overviewUrl: string; +}; + +export interface Author { + name: string; + email: string; +} + +export interface Committer { + name: string; + email: string; +} + +export interface HeadCommit { + id: string; + tree_id: string; + message: string; + timestamp: Date; + author: Author; + committer: Committer; +} + +export interface Owner { + login: string; + id: number; + node_id: string; + avatar_url: string; + gravatar_id: string; + url: string; + html_url: string; + followers_url: string; + following_url: string; + gists_url: string; + starred_url: string; + subscriptions_url: string; + organizations_url: string; + repos_url: string; + events_url: string; + received_events_url: string; + type: string; + site_admin: boolean; +} + +export interface Repository { + id: number; + node_id: string; + name: string; + full_name: string; + private: boolean; + owner: Owner; + html_url: string; + description?: any; + fork: boolean; + url: string; + forks_url: string; + keys_url: string; + collaborators_url: string; + teams_url: string; + hooks_url: string; + issue_events_url: string; + events_url: string; + assignees_url: string; + branches_url: string; + tags_url: string; + blobs_url: string; + git_tags_url: string; + git_refs_url: string; + trees_url: string; + statuses_url: string; + languages_url: string; + stargazers_url: string; + contributors_url: string; + subscribers_url: string; + subscription_url: string; + commits_url: string; + git_commits_url: string; + comments_url: string; + issue_comment_url: string; + contents_url: string; + compare_url: string; + merges_url: string; + archive_url: string; + downloads_url: string; + issues_url: string; + pulls_url: string; + milestones_url: string; + notifications_url: string; + labels_url: string; + releases_url: string; + deployments_url: string; +} + +export interface Owner2 { + login: string; + id: number; + node_id: string; + avatar_url: string; + gravatar_id: string; + url: string; + html_url: string; + followers_url: string; + following_url: string; + gists_url: string; + starred_url: string; + subscriptions_url: string; + organizations_url: string; + repos_url: string; + events_url: string; + received_events_url: string; + type: string; + site_admin: boolean; +} + +export interface HeadRepository { + id: number; + node_id: string; + name: string; + full_name: string; + private: boolean; + owner: Owner2; + html_url: string; + description?: any; + fork: boolean; + url: string; + forks_url: string; + keys_url: string; + collaborators_url: string; + teams_url: string; + hooks_url: string; + issue_events_url: string; + events_url: string; + assignees_url: string; + branches_url: string; + tags_url: string; + blobs_url: string; + git_tags_url: string; + git_refs_url: string; + trees_url: string; + statuses_url: string; + languages_url: string; + stargazers_url: string; + contributors_url: string; + subscribers_url: string; + subscription_url: string; + commits_url: string; + git_commits_url: string; + comments_url: string; + issue_comment_url: string; + contents_url: string; + compare_url: string; + merges_url: string; + archive_url: string; + downloads_url: string; + issues_url: string; + pulls_url: string; + milestones_url: string; + notifications_url: string; + labels_url: string; + releases_url: string; + deployments_url: string; +} + +export interface WorkflowRun { + id: number; + node_id: string; + head_branch: string; + head_sha: string; + run_number: number; + event: string; + status: string; + conclusion: string; + workflow_id: number; + url: string; + html_url: string; + pull_requests: any[]; + created_at: Date; + updated_at: Date; + jobs_url: string; + logs_url: string; + check_suite_url: string; + artifacts_url: string; + cancel_url: string; + rerun_url: string; + workflow_url: string; + head_commit: HeadCommit; + repository: Repository; + head_repository: HeadRepository; +} diff --git a/plugins/github-actions/src/components/BuildDetailsPage/BuildDetailsPage.tsx b/plugins/github-actions/src/components/BuildDetailsPage/BuildDetailsPage.tsx index bb07e0d6aa..84e9332837 100644 --- a/plugins/github-actions/src/components/BuildDetailsPage/BuildDetailsPage.tsx +++ b/plugins/github-actions/src/components/BuildDetailsPage/BuildDetailsPage.tsx @@ -32,8 +32,9 @@ import { import React from 'react'; import { useParams } from 'react-router-dom'; import { useAsync } from 'react-use'; -import { BuildsClient } from '../../apis/builds'; import { BuildStatusIndicator } from '../BuildStatusIndicator'; +import { useApi, githubAuthApiRef } from '@backstage/core-api'; +import { githubActionsApiRef } from '../../api'; const useStyles = makeStyles(theme => ({ root: { @@ -48,12 +49,14 @@ const useStyles = makeStyles(theme => ({ }, })); -const client = BuildsClient.create(); - export const BuildDetailsPage = () => { + const api = useApi(githubActionsApiRef); + const githubApi = useApi(githubAuthApiRef); + const token = githubApi.getAccessToken('repo'); + const classes = useStyles(); const { buildUri } = useParams(); - const status = useAsync(() => client.getBuild(buildUri), [buildUri]); + const status = useAsync(() => api.getBuild(buildUri, token), [buildUri]); if (status.loading) { return ; diff --git a/plugins/github-actions/src/components/BuildInfoCard/BuildInfoCard.tsx b/plugins/github-actions/src/components/BuildInfoCard/BuildInfoCard.tsx index a04189debc..2e4cbb03bd 100644 --- a/plugins/github-actions/src/components/BuildInfoCard/BuildInfoCard.tsx +++ b/plugins/github-actions/src/components/BuildInfoCard/BuildInfoCard.tsx @@ -27,10 +27,9 @@ import { } from '@material-ui/core'; import React from 'react'; import { useAsync } from 'react-use'; -import { BuildsClient } from '../../apis/builds'; import { BuildStatusIndicator } from '../BuildStatusIndicator'; - -const client = BuildsClient.create(); +import { githubActionsApiRef } from '../../api'; +import { useApi } from '@backstage/core-api'; const useStyles = makeStyles(theme => ({ root: { @@ -43,7 +42,8 @@ const useStyles = makeStyles(theme => ({ export const BuildInfoCard = () => { const classes = useStyles(); - const status = useAsync(() => client.listBuilds('entity:spotify:backstage')); + const api = useApi(githubActionsApiRef); + const status = useAsync(() => api.listBuilds('entity:spotify:backstage')); let content: JSX.Element; diff --git a/plugins/github-actions/src/components/BuildListPage/BuildListPage.tsx b/plugins/github-actions/src/components/BuildListPage/BuildListPage.tsx index fc784108d4..ae979d0cc0 100644 --- a/plugins/github-actions/src/components/BuildListPage/BuildListPage.tsx +++ b/plugins/github-actions/src/components/BuildListPage/BuildListPage.tsx @@ -29,12 +29,12 @@ import { Tooltip, Typography, } from '@material-ui/core'; -import React from 'react'; +import React, { FC } from 'react'; import { useAsync } from 'react-use'; -import { BuildsClient } from '../../apis/builds'; import { BuildStatusIndicator } from '../BuildStatusIndicator'; - -const client = BuildsClient.create(); +import { githubActionsApiRef } from '../../api'; +import { useApi, githubAuthApiRef } from '@backstage/core-api'; +import { Entity } from '@backstage/catalog-model'; const LongText = ({ text, max }: { text: string; max: number }) => { if (text.length < max) { @@ -56,9 +56,13 @@ const useStyles = makeStyles(theme => ({ }, })); -const PageContents = () => { +const PageContents: FC<{ entity: Entity }> = ({ entity }) => { + const api = useApi(githubActionsApiRef); + const githubApi = useApi(githubAuthApiRef); + const token = githubApi.getAccessToken('repo'); + const { loading, error, value } = useAsync(() => - client.listBuilds('entity:spotify:backstage'), + api.listBuilds(entity, token), ); if (loading) { @@ -115,14 +119,14 @@ const PageContents = () => { ); }; -export const BuildListPage = () => { +export const BuildListPage: FC<{ entity: Entity }> = ({ entity }) => { const classes = useStyles(); return (
CI/CD Builds - +
); }; diff --git a/plugins/github-actions/src/components/BuildStatusIndicator/BuildStatusIndicator.tsx b/plugins/github-actions/src/components/BuildStatusIndicator/BuildStatusIndicator.tsx index 332a03d67a..1a198f4df2 100644 --- a/plugins/github-actions/src/components/BuildStatusIndicator/BuildStatusIndicator.tsx +++ b/plugins/github-actions/src/components/BuildStatusIndicator/BuildStatusIndicator.tsx @@ -21,7 +21,7 @@ import SuccessIcon from '@material-ui/icons/CheckCircle'; import FailureIcon from '@material-ui/icons/Error'; import UnknownIcon from '@material-ui/icons/Help'; import React from 'react'; -import { BuildStatus } from '../../apis/builds'; +import { BuildStatus } from '../../api/types'; type Props = { status?: BuildStatus; diff --git a/plugins/github-actions/src/index.ts b/plugins/github-actions/src/index.ts index 3a0a0fe2d3..d67bc6a864 100644 --- a/plugins/github-actions/src/index.ts +++ b/plugins/github-actions/src/index.ts @@ -15,3 +15,4 @@ */ export { plugin } from './plugin'; +export * from './api';