From d139cf6a28f37ad407652d3295e68c977dc0a5c2 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Thu, 17 Sep 2020 17:59:05 +0200 Subject: [PATCH] Created a plugin for cloud build as CI/CD --- .github/CODEOWNERS | 17 +- packages/app/package.json | 3 +- .../app/src/components/catalog/EntityPage.tsx | 6 + packages/app/src/plugins.ts | 1 + plugins/cloudbuild/.eslintrc.js | 3 + plugins/cloudbuild/README.md | 13 + plugins/cloudbuild/dev/index.tsx | 19 ++ plugins/cloudbuild/package.json | 55 ++++ plugins/cloudbuild/src/api/CloudbuildApi.ts | 74 ++++++ .../cloudbuild/src/api/CloudbuildClient.ts | 134 ++++++++++ plugins/cloudbuild/src/api/index.ts | 19 ++ plugins/cloudbuild/src/api/types.ts | 223 +++++++++++++++++ .../cloudbuild/src/components/Cards/Cards.tsx | 120 +++++++++ .../cloudbuild/src/components/Cards/index.ts | 16 ++ plugins/cloudbuild/src/components/Router.tsx | 47 ++++ .../WorkflowRunDetails/WorkflowRunDetails.tsx | 234 ++++++++++++++++++ .../components/WorkflowRunDetails/index.ts | 16 ++ .../WorkflowRunDetails/useWorkflowRunJobs.ts | 32 +++ .../useWorkflowRunsDetails.ts | 38 +++ .../WorkflowRunLogs/WorkflowRunLogs.tsx | 169 +++++++++++++ .../src/components/WorkflowRunLogs/index.ts | 16 ++ .../useDownloadWorkflowRunLogs.ts | 38 +++ .../WorkflowRunStatus/WorkflowRunStatus.tsx | 70 ++++++ .../src/components/WorkflowRunStatus/index.ts | 17 ++ .../WorkflowRunsTable/WorkflowRunsTable.tsx | 190 ++++++++++++++ .../src/components/WorkflowRunsTable/index.ts | 17 ++ .../src/components/useProjectName.ts | 27 ++ .../src/components/useWorkflowRuns.ts | 98 ++++++++ plugins/cloudbuild/src/index.ts | 20 ++ plugins/cloudbuild/src/plugin.test.ts | 22 ++ plugins/cloudbuild/src/plugin.ts | 36 +++ plugins/cloudbuild/src/setupTests.ts | 18 ++ 32 files changed, 1799 insertions(+), 9 deletions(-) create mode 100644 plugins/cloudbuild/.eslintrc.js create mode 100644 plugins/cloudbuild/README.md create mode 100644 plugins/cloudbuild/dev/index.tsx create mode 100644 plugins/cloudbuild/package.json create mode 100644 plugins/cloudbuild/src/api/CloudbuildApi.ts create mode 100644 plugins/cloudbuild/src/api/CloudbuildClient.ts create mode 100644 plugins/cloudbuild/src/api/index.ts create mode 100644 plugins/cloudbuild/src/api/types.ts create mode 100644 plugins/cloudbuild/src/components/Cards/Cards.tsx create mode 100644 plugins/cloudbuild/src/components/Cards/index.ts create mode 100644 plugins/cloudbuild/src/components/Router.tsx create mode 100644 plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx create mode 100644 plugins/cloudbuild/src/components/WorkflowRunDetails/index.ts create mode 100644 plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunJobs.ts create mode 100644 plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunsDetails.ts create mode 100644 plugins/cloudbuild/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx create mode 100644 plugins/cloudbuild/src/components/WorkflowRunLogs/index.ts create mode 100644 plugins/cloudbuild/src/components/WorkflowRunLogs/useDownloadWorkflowRunLogs.ts create mode 100644 plugins/cloudbuild/src/components/WorkflowRunStatus/WorkflowRunStatus.tsx create mode 100644 plugins/cloudbuild/src/components/WorkflowRunStatus/index.ts create mode 100644 plugins/cloudbuild/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx create mode 100644 plugins/cloudbuild/src/components/WorkflowRunsTable/index.ts create mode 100644 plugins/cloudbuild/src/components/useProjectName.ts create mode 100644 plugins/cloudbuild/src/components/useWorkflowRuns.ts create mode 100644 plugins/cloudbuild/src/index.ts create mode 100644 plugins/cloudbuild/src/plugin.test.ts create mode 100644 plugins/cloudbuild/src/plugin.ts create mode 100644 plugins/cloudbuild/src/setupTests.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index de0655cdbc..960c1c823b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,11 +4,12 @@ # The last matching pattern takes precedence. # https://help.github.com/articles/about-codeowners/ -* @spotify/backstage-core -/docs/features/techdocs @spotify/techdocs-core -/plugins/techdocs @spotify/techdocs-core -/plugins/techdocs-backend @spotify/techdocs-core -/packages/techdocs-cli @spotify/techdocs-core -/packages/techdocs-container @spotify/techdocs-core -/.github/workflows/techdocs.yml @spotify/techdocs-core -/.github/workflows/techdocs-pypi.yml @spotify/techdocs-core +* @spotify/backstage-core +/.github/workflows/techdocs-pypi.yml @spotify/techdocs-core +/.github/workflows/techdocs.yml @spotify/techdocs-core +/docs/features/techdocs @spotify/techdocs-core +/packages/techdocs-cli @spotify/techdocs-core +/packages/techdocs-container @spotify/techdocs-core +/plugins/cloudbuild @trivago/ebarrios +/plugins/techdocs @spotify/techdocs-core +/plugins/techdocs-backend @spotify/techdocs-core diff --git a/packages/app/package.json b/packages/app/package.json index bf0f340a40..12d22074ec 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -4,12 +4,13 @@ "private": true, "bundled": true, "dependencies": { - "@backstage/cli": "^0.1.1-alpha.21", "@backstage/catalog-model": "^0.1.1-alpha.21", + "@backstage/cli": "^0.1.1-alpha.21", "@backstage/core": "^0.1.1-alpha.21", "@backstage/plugin-api-docs": "^0.1.1-alpha.21", "@backstage/plugin-catalog": "^0.1.1-alpha.21", "@backstage/plugin-circleci": "^0.1.1-alpha.21", + "@backstage/plugin-cloudbuild": "^0.1.1-alpha.21", "@backstage/plugin-explore": "^0.1.1-alpha.21", "@backstage/plugin-gcp-projects": "^0.1.1-alpha.21", "@backstage/plugin-github-actions": "^0.1.1-alpha.21", diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 257fde3c24..994f3b9263 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -17,6 +17,10 @@ import { Router as GitHubActionsRouter, isPluginApplicableToEntity as isGitHubActionsAvailable, } from '@backstage/plugin-github-actions'; +import { + Router as CloudbuildRouter, + isPluginApplicableToEntity as isCloudbuildAvailable, +} from '@backstage/plugin-cloudbuild'; import { Router as CircleCIRouter, isPluginApplicableToEntity as isCircleCIAvailable, @@ -42,6 +46,8 @@ const CICDSwitcher = ({ entity }: { entity: Entity }) => { return ; case isCircleCIAvailable(entity): return ; + case isCloudbuildAvailable(entity): + return ; default: return ( diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index b36d0323e0..d8b60eee1a 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -33,3 +33,4 @@ export { plugin as Jenkins } from '@backstage/plugin-jenkins'; export { plugin as ApiDocs } from '@backstage/plugin-api-docs'; export { plugin as GithubPullRequests } from '@roadiehq/backstage-plugin-github-pull-requests'; export { plugin as GcpProjects } from '@backstage/plugin-gcp-projects'; +export { plugin as Cloudbuild } from '@backstage/plugin-cloudbuild'; diff --git a/plugins/cloudbuild/.eslintrc.js b/plugins/cloudbuild/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/cloudbuild/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/cloudbuild/README.md b/plugins/cloudbuild/README.md new file mode 100644 index 0000000000..c3192e72f8 --- /dev/null +++ b/plugins/cloudbuild/README.md @@ -0,0 +1,13 @@ +# cloudbuild + +Welcome to the cloudbuild plugin! + +_This plugin was created through the Backstage CLI_ + +## Getting started + +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 [/cloudbuild](http://localhost:3000/cloudbuild). + +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. diff --git a/plugins/cloudbuild/dev/index.tsx b/plugins/cloudbuild/dev/index.tsx new file mode 100644 index 0000000000..264d6f801f --- /dev/null +++ b/plugins/cloudbuild/dev/index.tsx @@ -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. + */ +import { createDevApp } from '@backstage/dev-utils'; +import { plugin } from '../src/plugin'; + +createDevApp().registerPlugin(plugin).render(); diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json new file mode 100644 index 0000000000..a26a9eea31 --- /dev/null +++ b/plugins/cloudbuild/package.json @@ -0,0 +1,55 @@ +{ + "name": "@backstage/plugin-cloudbuild", + "version": "0.1.1-alpha.21", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": true, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/catalog-model": "^0.1.1-alpha.21", + "@backstage/core": "^0.1.1-alpha.21", + "@backstage/core-api": "^0.1.1-alpha.21", + "@backstage/plugin-catalog": "^0.1.1-alpha.21", + "@backstage/theme": "^0.1.1-alpha.21", + "@material-ui/core": "^4.11.0", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "@octokit/rest": "^18.0.0", + "@octokit/types": "^5.4.1", + "moment": "^2.27.0", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-lazylog": "^4.5.3", + "react-router": "6.0.0-beta.0", + "react-router-dom": "6.0.0-beta.0", + "react-use": "^15.3.3" + }, + "devDependencies": { + "@backstage/cli": "^0.1.1-alpha.21", + "@backstage/dev-utils": "^0.1.1-alpha.21", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", + "@types/jest": "^26.0.7", + "@types/node": "^12.0.0", + "jest-fetch-mock": "^3.0.3" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/cloudbuild/src/api/CloudbuildApi.ts b/plugins/cloudbuild/src/api/CloudbuildApi.ts new file mode 100644 index 0000000000..d5f5448b59 --- /dev/null +++ b/plugins/cloudbuild/src/api/CloudbuildApi.ts @@ -0,0 +1,74 @@ +/* + * 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 { + ActionsListWorkflowRunsForRepoResponseData, + ActionsGetWorkflowResponseData, + ActionsGetWorkflowRunResponseData, + EndpointInterface, +} from '../api/types'; + +export const cloudbuildApiRef = createApiRef({ + id: 'plugin.cloudbuild.service', + description: 'Used by the Cloudbuild plugin to make requests', +}); + +export type CloudbuildApi = { + listWorkflowRuns: ({ + token, + projectId, + }: { + token: string; + projectId: string; + }) => Promise; + getWorkflow: ({ + token, + projectId, + id, + }: { + token: string; + projectId: string; + id: string; + }) => Promise; + getWorkflowRun: ({ + token, + projectId, + id, + }: { + token: string; + projectId: string; + id: string; + }) => Promise; + reRunWorkflow: ({ + token, + projectId, + runId, + }: { + token: string; + projectId: string; + runId: string; + }) => Promise; + downloadJobLogsForWorkflowRun: ({ + token, + projectId, + runId, + }: { + token: string; + projectId: string; + runId: string; + }) => Promise; +}; diff --git a/plugins/cloudbuild/src/api/CloudbuildClient.ts b/plugins/cloudbuild/src/api/CloudbuildClient.ts new file mode 100644 index 0000000000..4e27eddd76 --- /dev/null +++ b/plugins/cloudbuild/src/api/CloudbuildClient.ts @@ -0,0 +1,134 @@ +/* + * 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 { CloudbuildApi } from './CloudbuildApi'; +import { + ActionsListWorkflowRunsForRepoResponseData, + ActionsGetWorkflowResponseData, + ActionsGetWorkflowRunResponseData, + EndpointInterface, + Build, +} from '../api/types'; + +export class CloudbuildClient implements CloudbuildApi { + async reRunWorkflow({ + token, + projectId, + runId, + }: { + token: string; + projectId: string; + runId: string; + }): Promise { + return await fetch( + `https://cloudbuild.googleapis.com/v1/projects/${projectId}/builds/${runId}:retry`, + { + headers: new Headers({ + Accept: '*/*', + Authorization: `Bearer ${token}`, + }), + }, + ); + } + async listWorkflowRuns({ + token, + projectId, + }: { + token: string; + projectId: string; + }): Promise { + const workflowRuns = await fetch( + `https://cloudbuild.googleapis.com/v1/projects/${projectId}/builds`, + { + headers: new Headers({ + Accept: '*/*', + Authorization: `Bearer ${token}`, + }), + }, + ); + const builds: Build[] = await workflowRuns.json(); + + const response: ActionsListWorkflowRunsForRepoResponseData = { + total_count: builds.length, + workflow_runs: builds, + }; + + return response; + } + async getWorkflow({ + token, + projectId, + id, + }: { + token: string; + projectId: string; + id: string; + }): Promise { + const workflow = await fetch( + `https://cloudbuild.googleapis.com/v1/projects/${projectId}/builds/${id}`, + { + headers: new Headers({ + Accept: '*/*', + Authorization: `Bearer ${token}`, + }), + }, + ); + + const build: ActionsGetWorkflowResponseData = await workflow.json(); + + return build; + } + async getWorkflowRun({ + token, + projectId, + id, + }: { + token: string; + projectId: string; + id: string; + }): Promise { + const workflow = await fetch( + `https://cloudbuild.googleapis.com/v1/projects/${projectId}/builds/${id}`, + { + headers: new Headers({ + Accept: '*/*', + Authorization: `Bearer ${token}`, + }), + }, + ); + const build: ActionsGetWorkflowResponseData = await workflow.json(); + + return build; + } + async downloadJobLogsForWorkflowRun({}: // token, + // projectId, + // runId, + { + // token: string; + // projectId: string; + // runId: string; + }): Promise { + // console.log("Token: ",token," projectId: ", projectId," runId: ",runId) + // const workflow = await new Octokit({ + // auth: token, + // }).actions.downloadJobLogsForWorkflowRun({ + // owner, + // repo, + // job_id: runId, + // }); + return []; + } +} diff --git a/plugins/cloudbuild/src/api/index.ts b/plugins/cloudbuild/src/api/index.ts new file mode 100644 index 0000000000..643eecb818 --- /dev/null +++ b/plugins/cloudbuild/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 './CloudbuildApi'; +export * from './CloudbuildClient'; +export * from './types'; diff --git a/plugins/cloudbuild/src/api/types.ts b/plugins/cloudbuild/src/api/types.ts new file mode 100644 index 0000000000..74ac89ee11 --- /dev/null +++ b/plugins/cloudbuild/src/api/types.ts @@ -0,0 +1,223 @@ +/* + * 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 type Step = { +// name: string; +// status: string; +// conclusion: string; +// number: number; // starts from 1 +// started_at: string; +// completed_at: string; +// }; + +// export type Job = { +// html_url: string; +// status: string; +// conclusion: string; +// started_at: string; +// completed_at: string; +// id: string; +// name: string; +// steps: Step[]; +// }; +export interface Build { + id: string; + status: string; + source: Source; + createTime: string; + startTime: string; + steps: Step[]; + timeout: string; + projectId: string; + logsBucket: string; + sourceProvenance: SourceProvenance; + buildTriggerId: string; + options: Options; + logUrl: string; + substitutions: Substitutions; + tags: string[]; + queueTtl: string; + name: string; + finishTime: any; + results: Results; + timing: Timing2; +} + +export type Jobs = { + total_count: number; + jobs: Build[]; +}; + +// export enum BuildStatus { +// 'success', +// 'failure', +// 'pending', +// 'running', +// } + +export interface ActionsListWorkflowRunsForRepoResponseData { + total_count: number; + workflow_runs: { + id: string; + status: string; + source: Source; + createTime: string; + startTime: string; + steps: Step[]; + timeout: string; + projectId: string; + logsBucket: string; + sourceProvenance: SourceProvenance; + buildTriggerId: string; + options: Options; + logUrl: string; + substitutions: Substitutions; + tags: string[]; + queueTtl: string; + name: string; + finishTime: any; + results: Results; + timing: Timing2; + }[]; +} +export type ActionsGetWorkflowResponseData = { + id: string; + status: string; + source: Source; + createTime: string; + startTime: string; + steps: Step[]; + timeout: string; + projectId: string; + logsBucket: string; + sourceProvenance: SourceProvenance; + buildTriggerId: string; + options: Options; + logUrl: string; + substitutions: Substitutions; + tags: string[]; + queueTtl: string; + name: string; + finishTime: any; + results: Results; + timing: Timing2; +}; +export type ActionsGetWorkflowRunResponseData = { + id: string; + status: string; + source: Source; + createTime: string; + startTime: string; + steps: Step[]; + timeout: string; + projectId: string; + logsBucket: string; + sourceProvenance: SourceProvenance; + buildTriggerId: string; + options: Options; + logUrl: string; + substitutions: Substitutions; + tags: string[]; + queueTtl: string; + name: string; + finishTime: any; + results: Results; + timing: Timing2; +}; +export type EndpointInterface = {}; + +export interface Step { + name: string; + args: string[]; + id: string; + waitFor: string[]; + entrypoint: string; + volumes: Volume[]; + dir: string; + timing: Timing; + status: string; + pullTiming: PullTiming; +} + +export interface Timing2 { + BUILD: BUILD; + FETCHSOURCE: FETCHSOURCE; +} + +export interface SourceProvenance { + resolvedStorageSource: {}; + fileHashes: {}; +} + +export interface Options { + machineType: string; + substitutionOption: string; + logging: string; + dynamicSubstitutions: boolean; +} + +export interface Substitutions { + COMMIT_SHA: string; + SHORT_SHA: string; + BRANCH_NAME: string; + REPO_NAME: string; + REVISION_ID: string; +} + +export interface Results { + buildStepImages: string[]; + buildStepOutputs: string[]; +} + +export interface BUILD { + startTime: string; + endTime: string; +} + +export interface FETCHSOURCE { + startTime: string; + endTime: string; +} + +export interface StorageSource { + bucket: string; + object: string; +} + +export interface Source { + storageSource: StorageSource; +} + +export interface Volume { + name: string; + path: string; +} + +export interface Timing { + startTime: string; + endTime: string; +} + +export interface PullTiming { + startTime: string; + endTime: string; +} + +export interface ResolvedStorageSource { + bucket: string; + object: string; + generation: string; +} diff --git a/plugins/cloudbuild/src/components/Cards/Cards.tsx b/plugins/cloudbuild/src/components/Cards/Cards.tsx new file mode 100644 index 0000000000..84d1bf9109 --- /dev/null +++ b/plugins/cloudbuild/src/components/Cards/Cards.tsx @@ -0,0 +1,120 @@ +/* + * 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 React, { useEffect } from 'react'; +import { useWorkflowRuns } from '../useWorkflowRuns'; +import { WorkflowRun, WorkflowRunsTable } from '../WorkflowRunsTable'; +import { Entity } from '@backstage/catalog-model'; +import { WorkflowRunStatus } from '../WorkflowRunStatus'; +import { + Link, + Theme, + makeStyles, + LinearProgress, + Typography, +} from '@material-ui/core'; +import { + InfoCard, + StructuredMetadataTable, + errorApiRef, + useApi, +} from '@backstage/core'; +import ExternalLinkIcon from '@material-ui/icons/Launch'; +import { CLOUDBUILD_ANNOTATION } from '../useProjectName'; + +const useStyles = makeStyles({ + externalLinkIcon: { + fontSize: 'inherit', + verticalAlign: 'bottom', + }, +}); + +const WidgetContent = ({ + error, + loading, + lastRun, + branch, +}: { + error?: Error; + loading?: boolean; + lastRun: WorkflowRun; + branch: string; +}) => { + const classes = useStyles(); + if (error) return Couldn't fetch latest {branch} run; + if (loading) return ; + return ( + + + + ), + message: lastRun.message, + url: ( + + See more on Google{' '} + + + ), + }} + /> + ); +}; + +export const LatestWorkflowRunCard = ({ + entity, + branch = 'master', +}: { + entity: Entity; + branch: string; +}) => { + const errorApi = useApi(errorApiRef); + const projectId = entity?.metadata.annotations?.[CLOUDBUILD_ANNOTATION] || ''; + + const [{ runs, loading, error }] = useWorkflowRuns({ + projectId, + }); + const lastRun = runs?.[0] ?? ({} as WorkflowRun); + useEffect(() => { + if (error) { + errorApi.post(error); + } + }, [error, errorApi]); + + return ( + + + + ); +}; + +export const LatestWorkflowsForBranchCard = ({ + entity, + branch = 'master', +}: { + entity: Entity; + branch: string; +}) => ( + + + +); diff --git a/plugins/cloudbuild/src/components/Cards/index.ts b/plugins/cloudbuild/src/components/Cards/index.ts new file mode 100644 index 0000000000..8c987ea1d5 --- /dev/null +++ b/plugins/cloudbuild/src/components/Cards/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { LatestWorkflowRunCard, LatestWorkflowsForBranchCard } from './Cards'; diff --git a/plugins/cloudbuild/src/components/Router.tsx b/plugins/cloudbuild/src/components/Router.tsx new file mode 100644 index 0000000000..65eec7cde9 --- /dev/null +++ b/plugins/cloudbuild/src/components/Router.tsx @@ -0,0 +1,47 @@ +/* + * 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 React from 'react'; +import { Entity } from '@backstage/catalog-model'; +import { Routes, Route } from 'react-router'; +import { rootRouteRef, buildRouteRef } from '../plugin'; +import { WorkflowRunDetails } from './WorkflowRunDetails'; +import { WorkflowRunsTable } from './WorkflowRunsTable'; +import { CLOUDBUILD_ANNOTATION } from './useProjectName'; +import { WarningPanel } from '@backstage/core'; + +export const isPluginApplicableToEntity = (entity: Entity) => + Boolean(entity.metadata.annotations?.[CLOUDBUILD_ANNOTATION]) && + entity.metadata.annotations?.[CLOUDBUILD_ANNOTATION] !== ''; + +export const Router = ({ entity }: { entity: Entity }) => + // TODO(shmidt-i): move warning to a separate standardized component + !isPluginApplicableToEntity(entity) ? ( + +
{CLOUDBUILD_ANNOTATION}
annotation is missing on the entity. +
+ ) : ( + + } + /> + } + /> + ) + + ); diff --git a/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx b/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx new file mode 100644 index 0000000000..55623162e4 --- /dev/null +++ b/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx @@ -0,0 +1,234 @@ +/* + * 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 { Entity } from '@backstage/catalog-model'; +import { Link } from '@backstage/core'; +import { + Breadcrumbs, + LinearProgress, + Link as MaterialLink, + makeStyles, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableRow, + Theme, + Typography, +} from '@material-ui/core'; +import ExternalLinkIcon from '@material-ui/icons/Launch'; +import React from 'react'; +import { useProjectName } from '../useProjectName'; +import { WorkflowRunStatus } from '../WorkflowRunStatus'; +import { useWorkflowRunsDetails } from './useWorkflowRunsDetails'; + +const useStyles = makeStyles(theme => ({ + root: { + maxWidth: 720, + margin: theme.spacing(2), + }, + title: { + padding: theme.spacing(1, 0, 2, 0), + }, + table: { + padding: theme.spacing(1), + }, + accordionDetails: { + padding: 0, + }, + button: { + order: -1, + marginRight: 0, + marginLeft: '-20px', + }, + externalLinkIcon: { + fontSize: 'inherit', + verticalAlign: 'bottom', + }, +})); + +// const JobsList = ({ jobs, entity }: { jobs?: Jobs; entity: Entity }) => { +// const classes = useStyles(); +// return ( +// +// {jobs && +// jobs.total_count > 0 && +// jobs.jobs.map((job: Build) => ( +// +// ))} +// +// ); +// }; + +// 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 ( +// +// +// +// +// +// +// +// +// ); +// }; + +// const JobListItem = ({ +// job, +// className, +// entity, +// }: { +// job: Build; +// className: string; +// entity: Entity; +// }) => { +// const classes = useStyles(); +// return ( +// +// } +// aria-controls={`panel-${name}-content`} +// id={`panel-${name}-header`} +// IconButtonProps={{ +// className: classes.button, +// }} +// > +// +// {job.name} ({getElapsedTime(job.startTime, job.finishTime)}) +// +// +// +// +// +// {job.steps.map((step: Step) => ( +// +// ))} +//
+//
+//
+// {job.status === 'QUEUED' || job.status === 'WORKING' ? ( +// +// ) : ( +// +// )} +//
+// ); +// }; + +export const WorkflowRunDetails = ({ entity }: { entity: Entity }) => { + // const projectName = useProjectName(entity); + const { value: projectName, loading, error } = useProjectName(entity); + const [projectId] = (projectName ?? '/').split('/'); + + // const [projectId] = projectName.value ? projectName.value : []; + const details = useWorkflowRunsDetails(projectId); + // const steps = useWorkflowRunJobs(projectId) + + const classes = useStyles(); + if (error) { + return ( + + Failed to load build, {error.message} + + ); + } else if (loading) { + return ; + } + return ( +
+ + Workflow runs + Workflow run details + + + + + + + Branch + + {details.value?.substitutions.BRANCH_NAME} + + + + Message + + {details.value?.substitutions.REPO_NAME} + + + + Commit ID + + {details.value?.substitutions.COMMIT_SHA} + + + + Status + + + + + + + + Author + + {`${details.value?.name}`} + + + + Links + + + {details.value?.logUrl && ( + + Workflow runs on Google{' '} + + + )} + + + {/* + + Jobs + {loading ? ( + + ) : ( + + )} + + */} + +
+
+
+ ); +}; diff --git a/plugins/cloudbuild/src/components/WorkflowRunDetails/index.ts b/plugins/cloudbuild/src/components/WorkflowRunDetails/index.ts new file mode 100644 index 0000000000..2886a26740 --- /dev/null +++ b/plugins/cloudbuild/src/components/WorkflowRunDetails/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { WorkflowRunDetails } from './WorkflowRunDetails'; diff --git a/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunJobs.ts b/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunJobs.ts new file mode 100644 index 0000000000..f471ce1875 --- /dev/null +++ b/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunJobs.ts @@ -0,0 +1,32 @@ +/* + * 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 { Jobs } from '../../api/types'; + +export const useWorkflowRunJobs = (jobsUrl?: string) => { + const jobs = useAsync(async (): Promise => { + if (jobsUrl === undefined) { + return { + total_count: 0, + jobs: [], + }; + } + + const data = await fetch(jobsUrl).then(d => d.json()); + return data; + }, [jobsUrl]); + return jobs; +}; diff --git a/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunsDetails.ts b/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunsDetails.ts new file mode 100644 index 0000000000..ca0f843f11 --- /dev/null +++ b/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunsDetails.ts @@ -0,0 +1,38 @@ +/* + * 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, googleAuthApiRef } from '@backstage/core'; +import { useParams } from 'react-router-dom'; +import { useAsync } from 'react-use'; +import { cloudbuildApiRef } from '../../api'; + +export const useWorkflowRunsDetails = (projectId: string) => { + const api = useApi(cloudbuildApiRef); + const auth = useApi(googleAuthApiRef); + const { id } = useParams(); + const details = useAsync(async () => { + const token = await auth.getAccessToken([ + 'https://www.googleapis.com/auth/cloud-platform', + ]); + return projectId + ? api.getWorkflowRun({ + token, + projectId, + id: id, + }) + : Promise.reject('No projectId provided'); + }, [projectId, id]); + return details; +}; diff --git a/plugins/cloudbuild/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx b/plugins/cloudbuild/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx new file mode 100644 index 0000000000..36abf8180e --- /dev/null +++ b/plugins/cloudbuild/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx @@ -0,0 +1,169 @@ +/* + * 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 { + Accordion, + AccordionSummary, + CircularProgress, + Fade, + LinearProgress, + makeStyles, + Modal, + Theme, + Tooltip, + Typography, + Zoom, +} from '@material-ui/core'; + +import React, { Suspense } from 'react'; +import { useDownloadWorkflowRunLogs } from './useDownloadWorkflowRunLogs'; +import LinePart from 'react-lazylog/build/LinePart'; +import { useProjectName } from '../useProjectName'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import DescriptionIcon from '@material-ui/icons/Description'; +import { Entity } from '@backstage/catalog-model'; + +const LazyLog = React.lazy(() => import('react-lazylog/build/LazyLog')); + +const useStyles = makeStyles(() => ({ + button: { + order: -1, + marginRight: 0, + marginLeft: '-20px', + }, + modal: { + display: 'flex', + alignItems: 'center', + width: '85%', + height: '85%', + justifyContent: 'center', + margin: 'auto', + }, + normalLog: { + height: '75vh', + width: '100%', + }, + modalLog: { + height: '100%', + width: '100%', + }, +})); + +const DisplayLog = ({ + jobLogs, + className, +}: { + jobLogs: any; + className: string; +}) => { + return ( + }> +
+ { + if ( + line.toLocaleLowerCase().includes('error') || + line.toLocaleLowerCase().includes('failed') || + line.toLocaleLowerCase().includes('failure') + ) { + return ( + + ); + } + return line; + }} + /> +
+
+ ); +}; + +/** + * A component for Run Logs visualization. + */ +export const WorkflowRunLogs = ({ + entity, + runId, + inProgress, +}: { + entity: Entity; + runId: string; + inProgress: boolean; +}) => { + const classes = useStyles(); + const projectName = useProjectName(entity); + + const repo = projectName.value || ''; + const jobLogs = useDownloadWorkflowRunLogs(repo, runId); + const [open, setOpen] = React.useState(false); + + const handleOpen = () => { + setOpen(true); + }; + + const handleClose = () => { + setOpen(false); + }; + + return ( + + } + aria-controls={`panel-${name}-content`} + id={`panel-${name}-header`} + IconButtonProps={{ + className: classes.button, + }} + > + + {jobLogs.loading ? : 'Job Log'} + + + { + event.stopPropagation(); + handleOpen(); + }} + style={{ marginLeft: 'auto' }} + /> + + event.stopPropagation()} + open={open} + onClose={handleClose} + > + + + + + + {jobLogs.value && ( + + )} + + ); +}; diff --git a/plugins/cloudbuild/src/components/WorkflowRunLogs/index.ts b/plugins/cloudbuild/src/components/WorkflowRunLogs/index.ts new file mode 100644 index 0000000000..0fcffd4dec --- /dev/null +++ b/plugins/cloudbuild/src/components/WorkflowRunLogs/index.ts @@ -0,0 +1,16 @@ +/* + * 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 { WorkflowRunLogs } from './WorkflowRunLogs'; diff --git a/plugins/cloudbuild/src/components/WorkflowRunLogs/useDownloadWorkflowRunLogs.ts b/plugins/cloudbuild/src/components/WorkflowRunLogs/useDownloadWorkflowRunLogs.ts new file mode 100644 index 0000000000..98ff3a974e --- /dev/null +++ b/plugins/cloudbuild/src/components/WorkflowRunLogs/useDownloadWorkflowRunLogs.ts @@ -0,0 +1,38 @@ +/* + * 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, googleAuthApiRef } from '@backstage/core'; +import { useAsync } from 'react-use'; +import { cloudbuildApiRef } from '../../api'; + +export const useDownloadWorkflowRunLogs = (projectId: string, id: string) => { + const api = useApi(cloudbuildApiRef); + const auth = useApi(googleAuthApiRef); + const details = useAsync(async () => { + const token = await auth.getAccessToken([ + 'https://www.googleapis.com/auth/cloud-platform', + ]); + const runId = id; + return projectId + ? api.downloadJobLogsForWorkflowRun({ + token, + projectId, + runId, + }) + : Promise.reject('No repo/owner provided'); + }, [projectId, id]); + return details; +}; diff --git a/plugins/cloudbuild/src/components/WorkflowRunStatus/WorkflowRunStatus.tsx b/plugins/cloudbuild/src/components/WorkflowRunStatus/WorkflowRunStatus.tsx new file mode 100644 index 0000000000..45a8fc662c --- /dev/null +++ b/plugins/cloudbuild/src/components/WorkflowRunStatus/WorkflowRunStatus.tsx @@ -0,0 +1,70 @@ +/* + * 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, + StatusAborted, + StatusError, +} from '@backstage/core'; +import React from 'react'; + +export const WorkflowRunStatus = ({ + status, +}: { + status: string | undefined; +}) => { + if (status === undefined) return null; + switch (status.toLowerCase()) { + case 'queued': + return ( + <> + Queued + + ); + case 'working': + return ( + <> + In progress + + ); + case 'success': + return ( + <> + Completed + + ); + case 'cancelled': + return ( + <> + Cancelled + + ); + case 'failure': + return ( + <> + Failed + + ); + default: + return ( + <> + Pending + + ); + } +}; diff --git a/plugins/cloudbuild/src/components/WorkflowRunStatus/index.ts b/plugins/cloudbuild/src/components/WorkflowRunStatus/index.ts new file mode 100644 index 0000000000..8ebca32cbd --- /dev/null +++ b/plugins/cloudbuild/src/components/WorkflowRunStatus/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { WorkflowRunStatus } from './WorkflowRunStatus'; diff --git a/plugins/cloudbuild/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx b/plugins/cloudbuild/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx new file mode 100644 index 0000000000..a97bec3650 --- /dev/null +++ b/plugins/cloudbuild/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx @@ -0,0 +1,190 @@ +/* + * 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 React, { FC } from 'react'; +import { Link, Typography, Box, IconButton, Tooltip } from '@material-ui/core'; +import RetryIcon from '@material-ui/icons/Replay'; +import GoogleIcon from '@material-ui/icons/CloudCircle'; +import { Link as RouterLink, generatePath } from 'react-router-dom'; +import { Table, TableColumn } from '@backstage/core'; +import { useWorkflowRuns } from '../useWorkflowRuns'; +import { WorkflowRunStatus } from '../WorkflowRunStatus'; +import SyncIcon from '@material-ui/icons/Sync'; +import { useProjectName } from '../useProjectName'; +import { Entity } from '@backstage/catalog-model'; +import { Substitutions } from '../../api/types'; +import { buildRouteRef } from '../../plugin'; + +export type WorkflowRun = { + id: string; + message: string; + url?: string; + googleUrl?: string; + status: string; + substitutions: Substitutions; + createTime: string; + org: string; + onReRunClick: () => void; +}; + +const generatedColumns: TableColumn[] = [ + { + title: 'Status', + width: '150px', + + render: (row: Partial) => ( + + + + ), + }, + { + title: 'Build', + field: 'id', + type: 'numeric', + width: '150px', + render: (row: Partial) => ( + +

{row.id?.substring(0, 8)}

+ {/*

{row.substitutions?.COMMIT_SHA}

*/} +
+ ), + }, + { + title: 'Source', + field: 'source', + highlight: true, + width: '200px', + render: (row: Partial) => ( + + {row.org}/{row.message} + + ), + }, + { + title: 'Ref', + render: (row: Partial) => ( + +

{row.substitutions?.BRANCH_NAME}

+
+ ), + }, + { + title: 'Commit', + render: (row: Partial) => ( + +

{row.substitutions?.SHORT_SHA}

+
+ ), + }, + { + title: 'Created', + render: (row: Partial) => ( + +

+ {row.createTime?.substring(6, 7)}/{row.createTime?.substring(8, 10)}/ + {row.createTime?.substring(0, 4)}, {row.createTime?.substring(11, 16)} +

+
+ ), + }, + { + title: 'Actions', + render: (row: Partial) => ( + + + + + + ), + width: '10%', + }, +]; + +type Props = { + loading: boolean; + retry: () => void; + runs?: WorkflowRun[]; + projectName: string; + page: number; + onChangePage: (page: number) => void; + total: number; + pageSize: number; + onChangePageSize: (pageSize: number) => void; +}; + +export const WorkflowRunsTableView: FC = ({ + projectName, + loading, + pageSize, + page, + retry, + runs, + onChangePage, + onChangePageSize, + total, +}) => { + return ( + , + tooltip: 'Reload workflow runs', + isFreeAction: true, + onClick: () => retry(), + }, + ]} + data={runs ?? []} + onChangePage={onChangePage} + onChangeRowsPerPage={onChangePageSize} + style={{ width: '100%' }} + title={ + + + + {projectName} + + } + columns={generatedColumns} + /> + ); +}; + +export const WorkflowRunsTable = ({ entity }: { entity: Entity }) => { + const { value: projectName, loading } = useProjectName(entity); + const [projectId] = (projectName ?? '/').split('/'); + + const [tableProps, { retry, setPage, setPageSize }] = useWorkflowRuns({ + projectId, + }); + + return ( + + ); +}; diff --git a/plugins/cloudbuild/src/components/WorkflowRunsTable/index.ts b/plugins/cloudbuild/src/components/WorkflowRunsTable/index.ts new file mode 100644 index 0000000000..4e21cc67c2 --- /dev/null +++ b/plugins/cloudbuild/src/components/WorkflowRunsTable/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { WorkflowRunsTable, WorkflowRunsTableView } from './WorkflowRunsTable'; +export type { WorkflowRun } from './WorkflowRunsTable'; diff --git a/plugins/cloudbuild/src/components/useProjectName.ts b/plugins/cloudbuild/src/components/useProjectName.ts new file mode 100644 index 0000000000..5f6752dce5 --- /dev/null +++ b/plugins/cloudbuild/src/components/useProjectName.ts @@ -0,0 +1,27 @@ +/* + * 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 { Entity } from '@backstage/catalog-model'; + +export const CLOUDBUILD_ANNOTATION = 'google.com/cloudbuild'; + +export const useProjectName = (entity: Entity) => { + const { value, loading, error } = useAsync(async () => { + return entity?.metadata.annotations?.[CLOUDBUILD_ANNOTATION] ?? ''; + }); + return { value, loading, error }; +}; diff --git a/plugins/cloudbuild/src/components/useWorkflowRuns.ts b/plugins/cloudbuild/src/components/useWorkflowRuns.ts new file mode 100644 index 0000000000..7e6affb137 --- /dev/null +++ b/plugins/cloudbuild/src/components/useWorkflowRuns.ts @@ -0,0 +1,98 @@ +/* + * 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 { useState } from 'react'; +import { useAsyncRetry } from 'react-use'; +import { WorkflowRun } from './WorkflowRunsTable/WorkflowRunsTable'; +import { cloudbuildApiRef } from '../api/CloudbuildApi'; +import { useApi, googleAuthApiRef, errorApiRef } from '@backstage/core'; +import { ActionsListWorkflowRunsForRepoResponseData } from '../api/types'; + +export function useWorkflowRuns({ projectId }: { projectId: string }) { + const api = useApi(cloudbuildApiRef); + const auth = useApi(googleAuthApiRef); + const errorApi = useApi(errorApiRef); + + const [total, setTotal] = useState(0); + const [page, setPage] = useState(0); + const [pageSize, setPageSize] = useState(5); + + const { loading, value: runs, retry, error } = useAsyncRetry< + WorkflowRun[] + >(async () => { + const token = await auth.getAccessToken([ + 'https://www.googleapis.com/auth/cloud-platform', + ]); + return api + .listWorkflowRuns({ + token, + projectId, + }) + .then( + ( + workflowRunsData: ActionsListWorkflowRunsForRepoResponseData, + ): WorkflowRun[] => { + setTotal(workflowRunsData.total_count); + // Transformation here + return workflowRunsData.workflow_runs.builds.map(run => ({ + message: run.substitutions.REPO_NAME, + id: `${run.id}`, + onReRunClick: async () => { + try { + await api.reRunWorkflow({ + token, + projectId, + runId: run.id, + }); + } catch (e) { + errorApi.post(e); + } + }, + substitutions: run.substitutions, + source: { + branchName: run.substitutions.REPO_NAME, + commit: { + hash: run.substitutions.COMMIT_SHA, + url: run.substitutions.REPO_NAME, + }, + }, + status: run.status, + url: run.logUrl, + googleUrl: run.logUrl, + org: 'trivago', + createTime: run.createTime, + })); + }, + ); + }, [page, pageSize, projectId]); + + return [ + { + page, + pageSize, + loading, + runs, + projectName: `${projectId}`, + total, + error, + }, + { + runs, + setPage, + setPageSize, + retry, + }, + ] as const; +} diff --git a/plugins/cloudbuild/src/index.ts b/plugins/cloudbuild/src/index.ts new file mode 100644 index 0000000000..616bb6b4e7 --- /dev/null +++ b/plugins/cloudbuild/src/index.ts @@ -0,0 +1,20 @@ +/* + * 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 { plugin } from './plugin'; +export * from './api'; +export { Router, isPluginApplicableToEntity } from './components/Router'; +export * from './components/Cards'; +export { CLOUDBUILD_ANNOTATION } from './components/useProjectName'; diff --git a/plugins/cloudbuild/src/plugin.test.ts b/plugins/cloudbuild/src/plugin.test.ts new file mode 100644 index 0000000000..dae16f1d1b --- /dev/null +++ b/plugins/cloudbuild/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * 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 { plugin } from './plugin'; + +describe('cloudbuild', () => { + it('should export plugin', () => { + expect(plugin).toBeDefined(); + }); +}); diff --git a/plugins/cloudbuild/src/plugin.ts b/plugins/cloudbuild/src/plugin.ts new file mode 100644 index 0000000000..61ff78198f --- /dev/null +++ b/plugins/cloudbuild/src/plugin.ts @@ -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 { + createPlugin, + createRouteRef, + createApiFactory, +} from '@backstage/core'; +import { cloudbuildApiRef, CloudbuildClient } from './api'; + +export const rootRouteRef = createRouteRef({ + path: '', + title: 'Google Cloudbuild', +}); + +export const buildRouteRef = createRouteRef({ + path: ':id', + title: 'Cloudbuild Run', +}); + +export const plugin = createPlugin({ + id: 'cloudbuild', + apis: [createApiFactory(cloudbuildApiRef, new CloudbuildClient())], +}); diff --git a/plugins/cloudbuild/src/setupTests.ts b/plugins/cloudbuild/src/setupTests.ts new file mode 100644 index 0000000000..4b4cdbdaaf --- /dev/null +++ b/plugins/cloudbuild/src/setupTests.ts @@ -0,0 +1,18 @@ +/* + * 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 '@testing-library/jest-dom'; + +require('jest-fetch-mock').enableMocks();