diff --git a/app-config.yaml b/app-config.yaml index 9a4a14b923..35cc7b6e1a 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -11,11 +11,11 @@ backend: credentials: true proxy: - "/circleci/api": - target: "https://circleci.com/api/v1.1" + '/circleci/api': + target: 'https://circleci.com/api/v1.1' changeOrigin: true pathRewrite: - "^/proxy/circleci/api/": "/" + '^/proxy/circleci/api/': '/' organization: name: Spotify diff --git a/plugins/github-actions/README.md b/plugins/github-actions/README.md index b0b339a1b0..72f991e814 100644 --- a/plugins/github-actions/README.md +++ b/plugins/github-actions/README.md @@ -1,13 +1,70 @@ -# github-actions +# GitHub Actions Plugin -Welcome to the github-actions plugin! +Website: [https://github.com/actions](https://github.com/actions) -_This plugin was created through the Backstage CLI_ +## Screenshots -## Getting started +TBD -Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/github-actions](http://localhost:3000/github-actions). +## Setup +### Generic Requirements +1. Provide OAuth credentials: + 1. [Create an OAuth App](https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/) with callback URL set to ```https://localhost:3000/auth/github```. + 2. Take Client ID and Client Secret from the newly created app's settings page and put them into ```AUTH_GITHUB_CLIENT_ID``` and ```AUTH_GITHUB_CLIENT_SECRET``` env variables. +2. Annotate your component with a correct GitHub Actions repository and owner: + + The annotation key is ```backstage.io/github-actions-id```. -You can also serve the plugin in isolation by running `yarn start` in the plugin directory. -This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. -It is only meant for local development, and the setup for it can be found inside the [/dev](/dev) directory. + Example: + ``` + apiVersion: backstage.io/v1alpha1 + kind: Component + metadata: + name: backstage + description: backstage.io + annotations: + backstage.io/github-actions-id: 'spotify/backstage' + spec: + type: website + lifecycle: production + owner: guest + ``` +### Standalone app requirements +If you didn't clone this repo you have to do some extra work. + +1. Add plugin API to your Backstage instance: + +```bash +yarn add @backstage/plugin-github-actions +``` + +```js +// packages/app/src/api.ts +import { ApiRegistry } from '@backstage/core'; +import { GithubActionsClient, githubActionsApiRef } from '@backstage/plugin-github-actions'; + +const builder = ApiRegistry.builder(); +builder.add(githubActionsApiRef, new GithubActionsClient()); + +export default builder.build() as ApiHolder; +``` + +2. Add plugin itself: + +```js +// packages/app/src/plugins.ts +export { plugin as GithubActions } from '@backstage/plugin-github-actions'; +``` + +3. Run the app with `yarn start` and the backend with `yarn --cwd packages/backend start`, navigate to `/github-actions/`. + +## Features + +- List workflow runs for a project +- Dive into one run to see a job steps +- Retry runs +- Pagination for runs + +## Limitations + +- There is a limit of 100 apps for one OAuth client/token pair diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index ce0068c35a..e20b719c31 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -18,18 +18,21 @@ "diff": "backstage-cli plugin:diff", "prepack": "backstage-cli prepack", "postpack": "backstage-cli postpack", - "clean": "backstage-cli clean" + "clean": "backstage-cli clean", + "mock-data": "./scripts/mock-data.sh" }, "dependencies": { "@backstage/catalog-model": "^0.1.1-alpha.16", "@backstage/core": "^0.1.1-alpha.16", "@backstage/core-api": "^0.1.1-alpha.16", "@backstage/theme": "^0.1.1-alpha.16", + "@backstage/plugin-catalog": "^0.1.1-alpha.16", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "@octokit/rest": "^18.0.0", "@octokit/types": "^5.0.1", + "moment": "^2.27.0", "react": "^16.13.1", "react-dom": "^16.13.1", "react-router-dom": "6.0.0-beta.0", diff --git a/plugins/github-actions/scripts/mock-data.sh b/plugins/github-actions/scripts/mock-data.sh new file mode 100755 index 0000000000..2653a415dd --- /dev/null +++ b/plugins/github-actions/scripts/mock-data.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + + curl \ + --location \ + --request POST 'localhost:7000/catalog/locations' \ + --header 'Content-Type: application/json' \ + --data-raw "{\"type\": \"file\", \"target\": \"$(pwd)/scripts/sample.yaml\"}" + echo diff --git a/plugins/github-actions/scripts/sample.yaml b/plugins/github-actions/scripts/sample.yaml new file mode 100644 index 0000000000..3aa75200f5 --- /dev/null +++ b/plugins/github-actions/scripts/sample.yaml @@ -0,0 +1,11 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage + description: backstage.io + annotations: + backstage.io/github-actions-id: 'spotify/backstage' +spec: + type: website + lifecycle: production + owner: guest diff --git a/plugins/github-actions/src/api/GithubActionsApi.ts b/plugins/github-actions/src/api/GithubActionsApi.ts index eedf9686f3..bb54fae703 100644 --- a/plugins/github-actions/src/api/GithubActionsApi.ts +++ b/plugins/github-actions/src/api/GithubActionsApi.ts @@ -72,5 +72,5 @@ export type GithubActionsApi = { owner: string; repo: string; runId: number; - }) => void; + }) => Promise; }; diff --git a/plugins/github-actions/src/api/GithubActionsClient.ts b/plugins/github-actions/src/api/GithubActionsClient.ts index 1606c268f4..a96f5341fd 100644 --- a/plugins/github-actions/src/api/GithubActionsClient.ts +++ b/plugins/github-actions/src/api/GithubActionsClient.ts @@ -23,7 +23,7 @@ import { } from '@octokit/types'; export class GithubActionsClient implements GithubActionsApi { - reRunWorkflow({ + async reRunWorkflow({ token, owner, repo, @@ -33,8 +33,8 @@ export class GithubActionsClient implements GithubActionsApi { owner: string; repo: string; runId: number; - }) { - new Octokit({ auth: token }).actions.reRunWorkflow({ + }): Promise { + return new Octokit({ auth: token }).actions.reRunWorkflow({ owner, repo, run_id: runId, diff --git a/plugins/github-actions/src/api/types.ts b/plugins/github-actions/src/api/types.ts index 8a7b2ca548..cd74a20cda 100644 --- a/plugins/github-actions/src/api/types.ts +++ b/plugins/github-actions/src/api/types.ts @@ -14,211 +14,33 @@ * 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 { +export type Step = { 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; + number: number; // starts from 1 + started_at: string; + completed_at: string; +}; + +export type Job = { 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; + status: string; + conclusion: string; + started_at: string; + completed_at: string; + name: string; + steps: Step[]; +}; + +export type Jobs = { + total_count: number; + jobs: Job[]; +}; + +export enum BuildStatus { + 'success', + 'failure', + 'pending', + 'running', } diff --git a/plugins/github-actions/src/components/BuildDetailsPage/BuildDetailsPage.tsx b/plugins/github-actions/src/components/BuildDetailsPage/BuildDetailsPage.tsx deleted file mode 100644 index 55c568106c..0000000000 --- a/plugins/github-actions/src/components/BuildDetailsPage/BuildDetailsPage.tsx +++ /dev/null @@ -1,143 +0,0 @@ -/* - * 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 { - Button, - LinearProgress, - makeStyles, - Paper, - Table, - TableBody, - TableCell, - TableContainer, - TableRow, - Theme, - Typography, -} from '@material-ui/core'; -import React from 'react'; -import { useParams } from 'react-router-dom'; -import { useAsync } from 'react-use'; -import { Link, useApi, githubAuthApiRef } from '@backstage/core'; -import { githubActionsApiRef } from '../../api'; - -const useStyles = makeStyles(theme => ({ - root: { - maxWidth: 720, - margin: theme.spacing(2), - }, - title: { - padding: theme.spacing(1, 0, 2, 0), - }, - table: { - padding: theme.spacing(1), - }, -})); - -export const BuildDetailsPage = () => { - const repo = 'try-ssr'; - const owner = 'CircleCITest3'; - const api = useApi(githubActionsApiRef); - const auth = useApi(githubAuthApiRef); - - const classes = useStyles(); - const { id } = useParams(); - const status = useAsync(async () => { - const token = await auth.getAccessToken(['repo', 'user']); - return api - .getWorkflowRun({ - token, - owner, - repo, - id: parseInt(id, 10), - }) - .then(data => { - return data; - }); - }, [location.search]); - - if (status.loading) { - return ; - } else if (status.error) { - return ( - - Failed to load build, {status.error.message} - - ); - } - - const details = status.value; - - return ( -
- - - - < - - - Build Details - - - - - - - Branch - - {details?.head_branch} - - - - Message - - {details?.head_commit.message} - - - - Commit ID - - {details?.head_commit.id} - - - - Status - - {details?.status} - - - - Author - - {`${details?.head_commit.author.name} (${details?.head_commit.author.email})`} - - - - Links - - - {details?.html_url && ( - - )} - - - -
-
-
- ); -}; diff --git a/plugins/github-actions/src/components/BuildInfoCard/BuildInfoCard.tsx b/plugins/github-actions/src/components/BuildInfoCard/BuildInfoCard.tsx deleted file mode 100644 index acc1b496ec..0000000000 --- a/plugins/github-actions/src/components/BuildInfoCard/BuildInfoCard.tsx +++ /dev/null @@ -1,107 +0,0 @@ -/* - * 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 { - LinearProgress, - makeStyles, - Table, - TableBody, - TableCell, - TableRow, - Theme, - Typography, -} from '@material-ui/core'; -import React from 'react'; -import { useAsync } from 'react-use'; -import { BuildStatusIndicator } from '../BuildStatusIndicator'; -import { githubActionsApiRef, BuildStatus } from '../../api'; -import { Link, useApi, githubAuthApiRef } from '@backstage/core'; - -const useStyles = makeStyles(theme => ({ - root: { - // height: 400, - }, - title: { - paddingBottom: theme.spacing(1), - }, -})); - -const BuildInfoCardContent = () => { - const api = useApi(githubActionsApiRef); - const auth = useApi(githubAuthApiRef); - - const status = useAsync(async () => { - const token = await auth.getAccessToken(['repo', 'user']); - return api.listWorkflowRuns({ token, owner: 'spotify', repo: 'backstage' }); - }); - - if (status.loading) { - return ; - } else if (status.error) { - return ( - - Failed to load builds, {status.error.message} - - ); - } - - // const [build] = - // status.value?.filter(({ branch }) => branch === 'master') ?? []; - - return ( - - - - - Message - - - - build message - - - - - - Commit ID - - build commit id - - - - Status - - - - - - -
- ); -}; - -export const BuildInfoCard = () => { - const classes = useStyles(); - - return ( -
- - Master Build - - -
- ); -}; diff --git a/plugins/github-actions/src/components/BuildStatusIndicator/BuildStatusIndicator.tsx b/plugins/github-actions/src/components/BuildStatusIndicator/BuildStatusIndicator.tsx deleted file mode 100644 index 1a198f4df2..0000000000 --- a/plugins/github-actions/src/components/BuildStatusIndicator/BuildStatusIndicator.tsx +++ /dev/null @@ -1,74 +0,0 @@ -/* - * 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 { IconComponent } from '@backstage/core'; -import { makeStyles, Theme } from '@material-ui/core'; -import ProgressIcon from '@material-ui/icons/Autorenew'; -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 '../../api/types'; - -type Props = { - status?: BuildStatus; -}; - -type StatusStyle = { - icon: IconComponent; - color: string; -}; - -const styles: { [key in BuildStatus]: StatusStyle } = { - [BuildStatus.Null]: { - icon: UnknownIcon, - color: '#f49b20', - }, - [BuildStatus.Success]: { - icon: SuccessIcon, - color: '#1db855', - }, - [BuildStatus.Failure]: { - icon: FailureIcon, - color: '#CA001B', - }, - [BuildStatus.Pending]: { - icon: UnknownIcon, - color: '#5BC0DE', - }, - [BuildStatus.Running]: { - icon: ProgressIcon, - color: '#BEBEBE', - }, -}; - -const useStyles = makeStyles({ - icon: style => ({ - color: style.color, - }), -}); - -export const BuildStatusIndicator = ({ status }: Props) => { - const style = (status && styles[status]) || styles[BuildStatus.Null]; - const classes = useStyles(style); - const Icon = style.icon; - - return ( -
- -
- ); -}; diff --git a/plugins/github-actions/src/components/WorkflowRunDetailsPage/WorkflowRunDetailsPage.tsx b/plugins/github-actions/src/components/WorkflowRunDetailsPage/WorkflowRunDetailsPage.tsx new file mode 100644 index 0000000000..283fd60ec6 --- /dev/null +++ b/plugins/github-actions/src/components/WorkflowRunDetailsPage/WorkflowRunDetailsPage.tsx @@ -0,0 +1,276 @@ +/* + * 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 { + Button, + LinearProgress, + makeStyles, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableRow, + Theme, + Typography, + Box, + ExpansionPanelDetails, + ExpansionPanel, + ExpansionPanelSummary, + ListItemText, + CircularProgress, + Grid, + Breadcrumbs, +} from '@material-ui/core'; +import moment from 'moment'; + +import React from 'react'; +import { + Link, + Page, + Header, + HeaderLabel, + Content, + ContentHeader, + SupportButton, + pageTheme, +} from '@backstage/core'; +import { Job, Step, Jobs } from '../../api/types'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { useWorkflowRunsDetails } from './useWorkflowRunsDetails'; +import { useWorkflowRunJobs } from './useWorkflowRunJobs'; +import { WorkflowRunStatusIcon } from '../WorkflowRunStatusIcon/WorkflowRunStatusIcon'; +import { useProjectName } from '../useProjectName'; +import GitHubIcon from '@material-ui/icons/GitHub'; + +const useStyles = makeStyles(theme => ({ + root: { + maxWidth: 720, + margin: theme.spacing(2), + }, + title: { + padding: theme.spacing(1, 0, 2, 0), + }, + table: { + padding: theme.spacing(1), + }, + expansionPanelDetails: { + padding: 0, + }, + button: { + order: -1, + marginRight: 0, + marginLeft: '-20px', + }, +})); + +const JobsList = ({ jobs }: { jobs?: Jobs }) => { + const classes = useStyles(); + return ( + + {jobs && + jobs.total_count > 0 && + jobs.jobs.map((job: Job) => ( + + ))} + + ); +}; + +const getElapsedTime = (start: string, end: string) => { + const diff = moment(moment(end || moment()).diff(moment(start))); + const timeElapsed = diff.format('m [minutes] s [seconds]'); + return timeElapsed; +}; + +const StepView = ({ step }: { step: Step }) => { + return ( + + + + + + + {step.status} + + + ); +}; + +const JobListItem = ({ job, className }: { job: Job; className: string }) => { + const classes = useStyles(); + return ( + + } + aria-controls={`panel-${name}-content`} + id={`panel-${name}-header`} + IconButtonProps={{ + className: classes.button, + }} + > + + {job.name} ({getElapsedTime(job.started_at, job.completed_at)}) + + + + + + {job.steps.map((step: Step) => ( + + ))} +
+
+
+
+ ); +}; + +/** + * A component for Jobs visualization. Jobs are a property of a Workflow Run. + */ +export const WorkflowRunDetailsPage = () => { + const [owner, repo] = ( + useProjectName({ + kind: 'Component', + name: 'backstage', + }) ?? '/' + ).split('/'); + const details = useWorkflowRunsDetails(repo, owner); + const jobs = useWorkflowRunJobs(details.value?.jobs_url); + + const classes = useStyles(); + + if (details.loading) { + return ; + } else if (details.error) { + return ( + + Failed to load build, {details.error.message} + + ); + } + + return ( + +
+ + +
+ + + + This plugin allows you to view and interact with your builds within + the GitHub Actions environment. + + + + Workflow runs + Workflow run details + + + +
+ + + + + + Branch + + {details.value?.head_branch} + + + + Message + + + {details.value?.head_commit.message} + + + + + Commit ID + + {details.value?.head_commit.id} + + + + Status + + + {' '} + {details.value?.status.toUpperCase()} + + + + + Author + + {`${details.value?.head_commit.author.name} (${details.value?.head_commit.author.email})`} + + + + Links + + + {details.value?.html_url && ( + + + + )} + + + + + Jobs + {jobs.loading ? ( + + ) : ( + + )} + + + +
+
+
+
+
+
+
+ ); +}; diff --git a/plugins/github-actions/src/components/BuildStatusIndicator/index.ts b/plugins/github-actions/src/components/WorkflowRunDetailsPage/index.ts similarity index 89% rename from plugins/github-actions/src/components/BuildStatusIndicator/index.ts rename to plugins/github-actions/src/components/WorkflowRunDetailsPage/index.ts index 520de525ec..443bcacc88 100644 --- a/plugins/github-actions/src/components/BuildStatusIndicator/index.ts +++ b/plugins/github-actions/src/components/WorkflowRunDetailsPage/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { BuildStatusIndicator } from './BuildStatusIndicator'; +export { WorkflowRunDetailsPage } from './WorkflowRunDetailsPage'; diff --git a/plugins/github-actions/src/components/BuildInfoCard/index.ts b/plugins/github-actions/src/components/WorkflowRunDetailsPage/useWorkflowRunJobs.ts similarity index 64% rename from plugins/github-actions/src/components/BuildInfoCard/index.ts rename to plugins/github-actions/src/components/WorkflowRunDetailsPage/useWorkflowRunJobs.ts index a6409e682a..d4c09abf7d 100644 --- a/plugins/github-actions/src/components/BuildInfoCard/index.ts +++ b/plugins/github-actions/src/components/WorkflowRunDetailsPage/useWorkflowRunJobs.ts @@ -13,5 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { useAsync } from 'react-use'; +import { Jobs } from '../../api/types'; -export { BuildInfoCard } from './BuildInfoCard'; +export const useWorkflowRunJobs = (jobsUrl?: string) => { + const jobs = useAsync(async () => { + if (jobsUrl === undefined) return []; + const data = await fetch(jobsUrl).then(d => d.json()); + return data; + }, [jobsUrl]); + return jobs; +}; diff --git a/plugins/github-actions/src/components/WorkflowRunDetailsPage/useWorkflowRunsDetails.ts b/plugins/github-actions/src/components/WorkflowRunDetailsPage/useWorkflowRunsDetails.ts new file mode 100644 index 0000000000..623f55eea6 --- /dev/null +++ b/plugins/github-actions/src/components/WorkflowRunDetailsPage/useWorkflowRunsDetails.ts @@ -0,0 +1,35 @@ +/* + * 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 { useApi, githubAuthApiRef } from '@backstage/core'; +import { useParams } from 'react-router-dom'; +import { useAsync } from 'react-use'; +import { githubActionsApiRef } from '../../api'; + +export const useWorkflowRunsDetails = (repo: string, owner: string) => { + const api = useApi(githubActionsApiRef); + const auth = useApi(githubAuthApiRef); + const { id } = useParams(); + const details = useAsync(async () => { + const token = await auth.getAccessToken(['repo']); + return api.getWorkflowRun({ + token, + owner, + repo, + id: parseInt(id, 10), + }); + }, [repo, owner, id]); + return details; +}; diff --git a/plugins/github-actions/src/components/WorkflowRunStatusIcon/WorkflowRunStatusIcon.tsx b/plugins/github-actions/src/components/WorkflowRunStatusIcon/WorkflowRunStatusIcon.tsx new file mode 100644 index 0000000000..645224ad24 --- /dev/null +++ b/plugins/github-actions/src/components/WorkflowRunStatusIcon/WorkflowRunStatusIcon.tsx @@ -0,0 +1,36 @@ +/* + * 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 { StatusPending, StatusRunning, StatusOK } from '@backstage/core'; +import React from 'react'; + +export const WorkflowRunStatusIcon = ({ + status, +}: { + status: string | undefined; +}) => { + if (status === undefined) return null; + switch (status.toLowerCase()) { + case 'queued': + return ; + case 'in_progress': + return ; + case 'completed': + return ; + default: + return ; + } +}; diff --git a/plugins/github-actions/src/components/BuildDetailsPage/index.ts b/plugins/github-actions/src/components/WorkflowRunStatusIcon/index.ts similarity index 90% rename from plugins/github-actions/src/components/BuildDetailsPage/index.ts rename to plugins/github-actions/src/components/WorkflowRunStatusIcon/index.ts index d59d0fc396..05fff3f46d 100644 --- a/plugins/github-actions/src/components/BuildDetailsPage/index.ts +++ b/plugins/github-actions/src/components/WorkflowRunStatusIcon/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { BuildDetailsPage } from './BuildDetailsPage'; +export { WorkflowRunStatusIcon } from './WorkflowRunStatusIcon'; diff --git a/plugins/github-actions/src/components/BuildListPage/BuildListPage.tsx b/plugins/github-actions/src/components/WorkflowRunsPage/WorkflowRunsPage.tsx similarity index 84% rename from plugins/github-actions/src/components/BuildListPage/BuildListPage.tsx rename to plugins/github-actions/src/components/WorkflowRunsPage/WorkflowRunsPage.tsx index 172b3611bf..b75a70f326 100644 --- a/plugins/github-actions/src/components/BuildListPage/BuildListPage.tsx +++ b/plugins/github-actions/src/components/WorkflowRunsPage/WorkflowRunsPage.tsx @@ -26,20 +26,20 @@ import { import { Grid } from '@material-ui/core'; import React from 'react'; -import { BuildListTable } from '../BuildListTable'; +import { WorkflowRunsTable } from '../WorkflowRunsTable'; -export const BuildListPage = () => { +export const WorkflowRunsPage = () => { return (
- + This plugin allows you to view and interact with your builds within the GitHub Actions environment. @@ -47,7 +47,7 @@ export const BuildListPage = () => { - + diff --git a/plugins/github-actions/src/components/BuildListPage/index.ts b/plugins/github-actions/src/components/WorkflowRunsPage/index.ts similarity index 91% rename from plugins/github-actions/src/components/BuildListPage/index.ts rename to plugins/github-actions/src/components/WorkflowRunsPage/index.ts index 2134913e51..ef511763f7 100644 --- a/plugins/github-actions/src/components/BuildListPage/index.ts +++ b/plugins/github-actions/src/components/WorkflowRunsPage/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { BuildListPage } from './BuildListPage'; +export { WorkflowRunsPage } from './WorkflowRunsPage'; diff --git a/plugins/github-actions/src/components/BuildListTable/BuildListTable.tsx b/plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx similarity index 58% rename from plugins/github-actions/src/components/BuildListTable/BuildListTable.tsx rename to plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx index b5fd4636f5..313a51e28a 100644 --- a/plugins/github-actions/src/components/BuildListTable/BuildListTable.tsx +++ b/plugins/github-actions/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx @@ -14,25 +14,19 @@ * limitations under the License. */ import React, { FC } from 'react'; -import { Link, Typography, Box, IconButton } from '@material-ui/core'; +import { Link, Typography, Box, IconButton, Tooltip } from '@material-ui/core'; import RetryIcon from '@material-ui/icons/Replay'; import GitHubIcon from '@material-ui/icons/GitHub'; import { Link as RouterLink } from 'react-router-dom'; -import { - StatusError, - StatusWarning, - StatusOK, - StatusPending, - StatusRunning, - Table, - TableColumn, -} from '@backstage/core'; -import { useBuilds } from './useBuilds'; +import { Table, TableColumn } from '@backstage/core'; +import { useWorkflowRuns } from './useWorkflowRuns'; +import { WorkflowRunStatusIcon } from '../WorkflowRunStatusIcon'; +import SyncIcon from '@material-ui/icons/Sync'; -export type Build = { +export type WorkflowRun = { id: string; - buildName: string; - buildUrl?: string; + message: string; + url?: string; source: { branchName: string; commit: { @@ -41,25 +35,7 @@ export type Build = { }; }; status: string; - onRestartClick: () => void; -}; - -// retried, canceled, infrastructure_fail, timedout, not_run, running, failed, queued, scheduled, not_running, no_tests, fixed, success -const getStatusComponent = (status: string | undefined = '') => { - switch (status.toLowerCase()) { - case 'queued': - case 'scheduled': - return ; - case 'running': - return ; - case 'failed': - return ; - case 'success': - return ; - case 'canceled': - default: - return ; - } + onReRunClick: () => void; }; const generatedColumns: TableColumn[] = [ @@ -70,40 +46,47 @@ const generatedColumns: TableColumn[] = [ width: '150px', }, { - title: 'Build', - field: 'buildName', + title: 'Message', + field: 'message', highlight: true, - render: (row: Partial) => ( - - {row.buildName} + render: (row: Partial) => ( + + {row.message} ), }, { title: 'Source', - render: (row: Partial) => ( - <> + render: (row: Partial) => ( +

{row.source?.branchName}

{row.source?.commit.hash}

- +
), }, { title: 'Status', - render: (row: Partial) => ( + render: (row: Partial) => ( - {getStatusComponent(row.status)} + - {row.status} + + {row.status} + ), }, { title: 'Actions', - render: (row: Partial) => ( - - - + render: (row: Partial) => ( + + + + + ), width: '10%', }, @@ -112,7 +95,7 @@ const generatedColumns: TableColumn[] = [ type Props = { loading: boolean; retry: () => void; - builds?: Build[]; + runs?: WorkflowRun[]; projectName: string; page: number; onChangePage: (page: number) => void; @@ -121,13 +104,13 @@ type Props = { onChangePageSize: (pageSize: number) => void; }; -const BuildListTableView: FC = ({ +const WorkflowRunsTableView: FC = ({ projectName, loading, pageSize, page, retry, - builds, + runs, onChangePage, onChangePageSize, total, @@ -140,13 +123,13 @@ const BuildListTableView: FC = ({ page={page} actions={[ { - icon: () => , - tooltip: 'Refresh Data', + icon: () => , + tooltip: 'Reload workflow runs', isFreeAction: true, onClick: () => retry(), }, ]} - data={builds ?? []} + data={runs ?? []} onChangePage={onChangePage} onChangeRowsPerPage={onChangePageSize} title={ @@ -161,19 +144,10 @@ const BuildListTableView: FC = ({ ); }; -export const BuildListTable = ({ - repo, - owner, -}: { - repo: string; - owner: string; -}) => { - const [tableProps, { retry, setPage, setPageSize }] = useBuilds({ - repo, - owner, - }); +export const WorkflowRunsTable = () => { + const [tableProps, { retry, setPage, setPageSize }] = useWorkflowRuns(); return ( - {}; - - const { loading, value: builds, retry } = useAsyncRetry(async () => { - const token = await auth.getAccessToken(['repo', 'user']); - + const projectName = useProjectName({ + kind: 'Component', + name: 'backstage', + }); + const { loading, value: runs, retry } = useAsyncRetry< + WorkflowRun[] + >(async () => { + const token = await auth.getAccessToken(['repo']); + const [owner, repo] = (projectName ?? '/').split('/'); return ( api // GitHub API pagination count starts from 1 .listWorkflowRuns({ token, owner, repo, pageSize, page: page + 1 }) .then( - (allBuilds: ActionsListWorkflowRunsForRepoResponseData): Build[] => { - setTotal(allBuilds.total_count); + ( + workflowRunsData: ActionsListWorkflowRunsForRepoResponseData, + ): WorkflowRun[] => { + setTotal(workflowRunsData.total_count); // Transformation here - return allBuilds.workflow_runs.map(run => ({ - buildName: run.head_commit.message, + return workflowRunsData.workflow_runs.map(run => ({ + message: run.head_commit.message, id: `${run.id}`, - onRestartClick: () => { - api.reRunWorkflow({ - token, - owner, - repo, - runId: run.id, - }); + onReRunClick: async () => { + try { + await api.reRunWorkflow({ + token, + owner, + repo, + runId: run.id, + }); + } catch (e) { + errorApi.post(e); + } }, source: { branchName: run.head_branch, @@ -63,28 +76,26 @@ export function useBuilds({ repo, owner }: { repo: string; owner: string }) { }, }, status: run.status, - buildUrl: run.url, + url: run.url, })); }, ) ); - }, [page, pageSize]); + }, [page, pageSize, projectName]); - const projectName = `${owner}/${repo}`; return [ { page, pageSize, loading, - builds, - projectName, + runs, + projectName: projectName ?? '', total, }, { - builds, + runs, setPage, setPageSize, - restartBuild, retry, }, ] as const; diff --git a/plugins/github-actions/src/components/useProjectName.ts b/plugins/github-actions/src/components/useProjectName.ts new file mode 100644 index 0000000000..d6fe9bd35d --- /dev/null +++ b/plugins/github-actions/src/components/useProjectName.ts @@ -0,0 +1,31 @@ +/* + * 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 { useAsync } from 'react-use'; +import { catalogApiRef, EntityCompoundName } from '@backstage/plugin-catalog'; +import { useApi } from '@backstage/core'; + +export const useProjectName = (name: EntityCompoundName) => { + const catalogApi = useApi(catalogApiRef); + + const { value } = useAsync(async () => { + const entity = await catalogApi.getEntityByName(name); + return ( + entity?.metadata.annotations?.['backstage.io/github-actions-id'] ?? '' + ); + }); + return value; +}; diff --git a/plugins/github-actions/src/plugin.ts b/plugins/github-actions/src/plugin.ts index b728296cbc..7faee4e624 100644 --- a/plugins/github-actions/src/plugin.ts +++ b/plugins/github-actions/src/plugin.ts @@ -15,8 +15,8 @@ */ import { createPlugin, createRouteRef } from '@backstage/core'; -import { BuildDetailsPage } from './components/BuildDetailsPage'; -import { BuildListPage } from './components/BuildListPage'; +import { WorkflowRunDetailsPage } from './components/WorkflowRunDetailsPage'; +import { WorkflowRunsPage } from './components/WorkflowRunsPage'; // TODO(freben): This is just a demo route for now export const rootRouteRef = createRouteRef({ @@ -24,14 +24,14 @@ export const rootRouteRef = createRouteRef({ title: 'GitHub Actions', }); export const buildRouteRef = createRouteRef({ - path: '/github-actions/build/:id', - title: 'GitHub Actions Build', + path: '/github-actions/workflow-run/:id', + title: 'GitHub Actions Workflow Run', }); export const plugin = createPlugin({ id: 'github-actions', register({ router }) { - router.addRoute(rootRouteRef, BuildListPage); - router.addRoute(buildRouteRef, BuildDetailsPage); + router.addRoute(rootRouteRef, WorkflowRunsPage); + router.addRoute(buildRouteRef, WorkflowRunDetailsPage); }, }); diff --git a/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml b/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml index 71f75c7695..ba0e877662 100644 --- a/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/react-ssr-template/template.yaml @@ -13,7 +13,7 @@ spec: type: website path: '.' schema: - required: + required: - component_id - description properties: @@ -22,7 +22,6 @@ spec: type: string description: Unique name of the component description: - title: Description + title: Description type: string description: Description of the component - diff --git a/yarn.lock b/yarn.lock index 1f6c4c56b4..1dd2c9cae4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13765,6 +13765,11 @@ moment@2.26.0, moment@^2.25.3, moment@^2.26.0: resolved "https://registry.npmjs.org/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a" integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw== +moment@^2.27.0: + version "2.27.0" + resolved "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d" + integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ== + monaco-editor@^0.20.0: version "0.20.0" resolved "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.20.0.tgz#5d5009343a550124426cb4d965a4d27a348b4dea"