From d139cf6a28f37ad407652d3295e68c977dc0a5c2 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Thu, 17 Sep 2020 17:59:05 +0200 Subject: [PATCH 01/27] 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(); From 0d1695715e2e71df085bf29317706f5cfd0f9644 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Fri, 18 Sep 2020 10:50:50 +0200 Subject: [PATCH 02/27] Removed unused types --- plugins/cloudbuild/src/api/CloudbuildApi.ts | 10 -- .../cloudbuild/src/api/CloudbuildClient.ts | 22 +-- plugins/cloudbuild/src/api/types.ts | 50 +----- .../WorkflowRunLogs/WorkflowRunLogs.tsx | 169 ------------------ .../src/components/WorkflowRunLogs/index.ts | 16 -- .../useDownloadWorkflowRunLogs.ts | 38 ---- .../WorkflowRunsTable/WorkflowRunsTable.tsx | 3 +- .../src/components/useWorkflowRuns.ts | 3 +- 8 files changed, 5 insertions(+), 306 deletions(-) delete mode 100644 plugins/cloudbuild/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx delete mode 100644 plugins/cloudbuild/src/components/WorkflowRunLogs/index.ts delete mode 100644 plugins/cloudbuild/src/components/WorkflowRunLogs/useDownloadWorkflowRunLogs.ts diff --git a/plugins/cloudbuild/src/api/CloudbuildApi.ts b/plugins/cloudbuild/src/api/CloudbuildApi.ts index d5f5448b59..a340246348 100644 --- a/plugins/cloudbuild/src/api/CloudbuildApi.ts +++ b/plugins/cloudbuild/src/api/CloudbuildApi.ts @@ -19,7 +19,6 @@ import { ActionsListWorkflowRunsForRepoResponseData, ActionsGetWorkflowResponseData, ActionsGetWorkflowRunResponseData, - EndpointInterface, } from '../api/types'; export const cloudbuildApiRef = createApiRef({ @@ -62,13 +61,4 @@ export type CloudbuildApi = { 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 index 4e27eddd76..f2fedecacd 100644 --- a/plugins/cloudbuild/src/api/CloudbuildClient.ts +++ b/plugins/cloudbuild/src/api/CloudbuildClient.ts @@ -19,7 +19,6 @@ import { ActionsListWorkflowRunsForRepoResponseData, ActionsGetWorkflowResponseData, ActionsGetWorkflowRunResponseData, - EndpointInterface, Build, } from '../api/types'; @@ -59,11 +58,12 @@ export class CloudbuildClient implements CloudbuildApi { }), }, ); + const builds: Build[] = await workflowRuns.json(); const response: ActionsListWorkflowRunsForRepoResponseData = { total_count: builds.length, - workflow_runs: builds, + builds: builds, }; return response; @@ -113,22 +113,4 @@ export class CloudbuildClient implements CloudbuildApi { 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/types.ts b/plugins/cloudbuild/src/api/types.ts index 74ac89ee11..f3072bb98d 100644 --- a/plugins/cloudbuild/src/api/types.ts +++ b/plugins/cloudbuild/src/api/types.ts @@ -14,25 +14,6 @@ * 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; @@ -61,37 +42,9 @@ export type Jobs = { 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; - }[]; + builds: Build[]; } export type ActionsGetWorkflowResponseData = { id: string; @@ -137,7 +90,6 @@ export type ActionsGetWorkflowRunResponseData = { results: Results; timing: Timing2; }; -export type EndpointInterface = {}; export interface Step { name: string; diff --git a/plugins/cloudbuild/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx b/plugins/cloudbuild/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx deleted file mode 100644 index 36abf8180e..0000000000 --- a/plugins/cloudbuild/src/components/WorkflowRunLogs/WorkflowRunLogs.tsx +++ /dev/null @@ -1,169 +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 { - 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 deleted file mode 100644 index 0fcffd4dec..0000000000 --- a/plugins/cloudbuild/src/components/WorkflowRunLogs/index.ts +++ /dev/null @@ -1,16 +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. - */ -export { WorkflowRunLogs } from './WorkflowRunLogs'; diff --git a/plugins/cloudbuild/src/components/WorkflowRunLogs/useDownloadWorkflowRunLogs.ts b/plugins/cloudbuild/src/components/WorkflowRunLogs/useDownloadWorkflowRunLogs.ts deleted file mode 100644 index 98ff3a974e..0000000000 --- a/plugins/cloudbuild/src/components/WorkflowRunLogs/useDownloadWorkflowRunLogs.ts +++ /dev/null @@ -1,38 +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 { 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/WorkflowRunsTable/WorkflowRunsTable.tsx b/plugins/cloudbuild/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx index a97bec3650..7c361f7fc4 100644 --- a/plugins/cloudbuild/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx +++ b/plugins/cloudbuild/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx @@ -35,7 +35,6 @@ export type WorkflowRun = { status: string; substitutions: Substitutions; createTime: string; - org: string; onReRunClick: () => void; }; @@ -73,7 +72,7 @@ const generatedColumns: TableColumn[] = [ // to={`https://www.github.com/trivago/${row.message}`} to={generatePath(buildRouteRef.path, { id: row.id! })} > - {row.org}/{row.message} + {row.message} ), }, diff --git a/plugins/cloudbuild/src/components/useWorkflowRuns.ts b/plugins/cloudbuild/src/components/useWorkflowRuns.ts index 7e6affb137..6e20ebbff1 100644 --- a/plugins/cloudbuild/src/components/useWorkflowRuns.ts +++ b/plugins/cloudbuild/src/components/useWorkflowRuns.ts @@ -46,7 +46,7 @@ export function useWorkflowRuns({ projectId }: { projectId: string }) { ): WorkflowRun[] => { setTotal(workflowRunsData.total_count); // Transformation here - return workflowRunsData.workflow_runs.builds.map(run => ({ + return workflowRunsData.builds.builds.map(run => ({ message: run.substitutions.REPO_NAME, id: `${run.id}`, onReRunClick: async () => { @@ -71,7 +71,6 @@ export function useWorkflowRuns({ projectId }: { projectId: string }) { status: run.status, url: run.logUrl, googleUrl: run.logUrl, - org: 'trivago', createTime: run.createTime, })); }, From 8bdf33c0d44a94862b5a5e41f2659618ed64c2ac Mon Sep 17 00:00:00 2001 From: ebarrios Date: Fri, 18 Sep 2020 11:51:11 +0200 Subject: [PATCH 03/27] Cleanup unused types --- plugins/cloudbuild/src/api/CloudbuildApi.ts | 3 +- .../cloudbuild/src/api/CloudbuildClient.ts | 11 +-- plugins/cloudbuild/src/api/types.ts | 57 ++--------- .../WorkflowRunDetails/WorkflowRunDetails.tsx | 96 ------------------- .../WorkflowRunDetails/useWorkflowRunJobs.ts | 8 +- .../src/components/useWorkflowRuns.ts | 2 +- 6 files changed, 18 insertions(+), 159 deletions(-) diff --git a/plugins/cloudbuild/src/api/CloudbuildApi.ts b/plugins/cloudbuild/src/api/CloudbuildApi.ts index a340246348..e9760677fb 100644 --- a/plugins/cloudbuild/src/api/CloudbuildApi.ts +++ b/plugins/cloudbuild/src/api/CloudbuildApi.ts @@ -18,7 +18,6 @@ import { createApiRef } from '@backstage/core'; import { ActionsListWorkflowRunsForRepoResponseData, ActionsGetWorkflowResponseData, - ActionsGetWorkflowRunResponseData, } from '../api/types'; export const cloudbuildApiRef = createApiRef({ @@ -51,7 +50,7 @@ export type CloudbuildApi = { token: string; projectId: string; id: string; - }) => Promise; + }) => Promise; reRunWorkflow: ({ token, projectId, diff --git a/plugins/cloudbuild/src/api/CloudbuildClient.ts b/plugins/cloudbuild/src/api/CloudbuildClient.ts index f2fedecacd..4219ecabc1 100644 --- a/plugins/cloudbuild/src/api/CloudbuildClient.ts +++ b/plugins/cloudbuild/src/api/CloudbuildClient.ts @@ -18,8 +18,7 @@ import { CloudbuildApi } from './CloudbuildApi'; import { ActionsListWorkflowRunsForRepoResponseData, ActionsGetWorkflowResponseData, - ActionsGetWorkflowRunResponseData, - Build, + Builds, } from '../api/types'; export class CloudbuildClient implements CloudbuildApi { @@ -59,11 +58,11 @@ export class CloudbuildClient implements CloudbuildApi { }, ); - const builds: Build[] = await workflowRuns.json(); + const builds: Builds = await workflowRuns.json(); const response: ActionsListWorkflowRunsForRepoResponseData = { - total_count: builds.length, - builds: builds, + total_count: builds.builds.length, + builds: builds.builds, }; return response; @@ -99,7 +98,7 @@ export class CloudbuildClient implements CloudbuildApi { token: string; projectId: string; id: string; - }): Promise { + }): Promise { const workflow = await fetch( `https://cloudbuild.googleapis.com/v1/projects/${projectId}/builds/${id}`, { diff --git a/plugins/cloudbuild/src/api/types.ts b/plugins/cloudbuild/src/api/types.ts index f3072bb98d..bcbde4df66 100644 --- a/plugins/cloudbuild/src/api/types.ts +++ b/plugins/cloudbuild/src/api/types.ts @@ -14,61 +14,16 @@ * limitations under the License. */ -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 interface ActionsListWorkflowRunsForRepoResponseData { total_count: number; - builds: Build[]; + builds: ActionsGetWorkflowResponseData[]; } -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 Builds = { + builds: ActionsGetWorkflowResponseData[]; }; -export type ActionsGetWorkflowRunResponseData = { + +export type ActionsGetWorkflowResponseData = { id: string; status: string; source: Source; diff --git a/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx b/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx index 55623162e4..d3484ba707 100644 --- a/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx +++ b/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx @@ -60,97 +60,11 @@ const useStyles = makeStyles(theme => ({ }, })); -// 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) { @@ -216,16 +130,6 @@ export const WorkflowRunDetails = ({ entity }: { entity: Entity }) => { )} - {/* - - Jobs - {loading ? ( - - ) : ( - - )} - - */} diff --git a/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunJobs.ts b/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunJobs.ts index f471ce1875..2c3df61abe 100644 --- a/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunJobs.ts +++ b/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunJobs.ts @@ -14,14 +14,16 @@ * limitations under the License. */ import { useAsync } from 'react-use'; -import { Jobs } from '../../api/types'; +import { ActionsListWorkflowRunsForRepoResponseData } from '../../api/types'; export const useWorkflowRunJobs = (jobsUrl?: string) => { - const jobs = useAsync(async (): Promise => { + const jobs = useAsync(async (): Promise< + ActionsListWorkflowRunsForRepoResponseData + > => { if (jobsUrl === undefined) { return { total_count: 0, - jobs: [], + builds: [], }; } diff --git a/plugins/cloudbuild/src/components/useWorkflowRuns.ts b/plugins/cloudbuild/src/components/useWorkflowRuns.ts index 6e20ebbff1..bc887f9a1a 100644 --- a/plugins/cloudbuild/src/components/useWorkflowRuns.ts +++ b/plugins/cloudbuild/src/components/useWorkflowRuns.ts @@ -46,7 +46,7 @@ export function useWorkflowRuns({ projectId }: { projectId: string }) { ): WorkflowRun[] => { setTotal(workflowRunsData.total_count); // Transformation here - return workflowRunsData.builds.builds.map(run => ({ + return workflowRunsData.builds.map(run => ({ message: run.substitutions.REPO_NAME, id: `${run.id}`, onReRunClick: async () => { From f12de035d22f3ce2b8753f9c27c55b6178f6f547 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Fri, 18 Sep 2020 12:14:30 +0200 Subject: [PATCH 04/27] Change readme from cloudbuild to Goolge cloudbuild --- plugins/cloudbuild/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/cloudbuild/README.md b/plugins/cloudbuild/README.md index c3192e72f8..38aa7fc52f 100644 --- a/plugins/cloudbuild/README.md +++ b/plugins/cloudbuild/README.md @@ -1,6 +1,6 @@ # cloudbuild -Welcome to the cloudbuild plugin! +Welcome to the Google cloudbuild plugin! _This plugin was created through the Backstage CLI_ From f5b20d5772c1d37b86377de869aae578c0300ac3 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Fri, 18 Sep 2020 12:15:41 +0200 Subject: [PATCH 05/27] Change readme from cloudbuild to Goolge cloudbuild --- plugins/cloudbuild/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/cloudbuild/README.md b/plugins/cloudbuild/README.md index 38aa7fc52f..29a39d69f2 100644 --- a/plugins/cloudbuild/README.md +++ b/plugins/cloudbuild/README.md @@ -1,4 +1,4 @@ -# cloudbuild +# Google cloudbuild Welcome to the Google cloudbuild plugin! From ab4d4d2f03261c42b303c2cb889149c3e63b923a Mon Sep 17 00:00:00 2001 From: ebarrios Date: Fri, 18 Sep 2020 12:45:23 +0200 Subject: [PATCH 06/27] Ran prettier on packages/app/src/components/catalog/EntityPage.tsx --- packages/app/src/components/catalog/EntityPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index d4a45ba60c..5950391a8d 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -21,7 +21,7 @@ import { Router as CloudbuildRouter, isPluginApplicableToEntity as isCloudbuildAvailable, } from '@backstage/plugin-cloudbuild'; - import { +import { Router as JenkinsRouter, isPluginApplicableToEntity as isJenkinsAvailable, LatestRunCard as JenkinsLatestRunCard, From 2eab159423e1253b8d0274c8d813df17230654a6 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Fri, 18 Sep 2020 16:45:02 +0200 Subject: [PATCH 07/27] Modify Author field in details and change it to Service Account --- .../components/WorkflowRunDetails/WorkflowRunDetails.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx b/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx index d3484ba707..0c2635f77b 100644 --- a/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx +++ b/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx @@ -113,9 +113,11 @@ export const WorkflowRunDetails = ({ entity }: { entity: Entity }) => { - Author + Service Account + + + {`${details.value?.projectId}`}@cloudbuild.gserviceaccount.com - {`${details.value?.name}`} From 1f973f4867e442ead044837105166dedf7c6eb4c Mon Sep 17 00:00:00 2001 From: ebarrios Date: Mon, 21 Sep 2020 10:22:41 +0200 Subject: [PATCH 08/27] Modify the value of Service Account in the details to correct the error that was not showing the service account --- .../src/components/WorkflowRunDetails/WorkflowRunDetails.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx b/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx index 0c2635f77b..9f8a6c2229 100644 --- a/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx +++ b/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx @@ -66,6 +66,8 @@ export const WorkflowRunDetails = ({ entity }: { entity: Entity }) => { const details = useWorkflowRunsDetails(projectId); + const serviceAccount = (details.value?.logUrl ?? '=').split('='); + const classes = useStyles(); if (error) { return ( @@ -116,7 +118,7 @@ export const WorkflowRunDetails = ({ entity }: { entity: Entity }) => { Service Account - {`${details.value?.projectId}`}@cloudbuild.gserviceaccount.com + {`${serviceAccount[1]}`}@cloudbuild.gserviceaccount.com From 1b9e7e8e7c694f38562f140a9c5c97bd95704b6e Mon Sep 17 00:00:00 2001 From: ebarrios Date: Wed, 23 Sep 2020 16:33:05 +0200 Subject: [PATCH 09/27] Updated plugin name on the README.md --- plugins/cloudbuild/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/cloudbuild/README.md b/plugins/cloudbuild/README.md index 29a39d69f2..5f1f6519dc 100644 --- a/plugins/cloudbuild/README.md +++ b/plugins/cloudbuild/README.md @@ -1,6 +1,6 @@ -# Google cloudbuild +# Google Cloud Build -Welcome to the Google cloudbuild plugin! +Welcome to the Google Cloud Build plugin! _This plugin was created through the Backstage CLI_ From bd9988d58e0b410897b8222518614ef90015d1ed Mon Sep 17 00:00:00 2001 From: ebarrios Date: Wed, 23 Sep 2020 17:09:23 +0200 Subject: [PATCH 10/27] Using the new way of injecting one API into another and added encodeURIComponent for all urls variables on CloudbuildClient.ts --- plugins/cloudbuild/src/api/CloudbuildApi.ts | 11 +---- .../cloudbuild/src/api/CloudbuildClient.ts | 45 ++++++++++++------- .../useWorkflowRunsDetails.ts | 7 +-- .../src/components/useWorkflowRuns.ts | 8 +--- plugins/cloudbuild/src/plugin.ts | 11 ++++- 5 files changed, 42 insertions(+), 40 deletions(-) diff --git a/plugins/cloudbuild/src/api/CloudbuildApi.ts b/plugins/cloudbuild/src/api/CloudbuildApi.ts index e9760677fb..7091ddf1be 100644 --- a/plugins/cloudbuild/src/api/CloudbuildApi.ts +++ b/plugins/cloudbuild/src/api/CloudbuildApi.ts @@ -26,15 +26,10 @@ export const cloudbuildApiRef = createApiRef({ }); export type CloudbuildApi = { - listWorkflowRuns: ({ - token, - projectId, - }: { - token: string; + listWorkflowRuns: (request: { projectId: string; }) => Promise; getWorkflow: ({ - token, projectId, id, }: { @@ -43,20 +38,16 @@ export type CloudbuildApi = { 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; diff --git a/plugins/cloudbuild/src/api/CloudbuildClient.ts b/plugins/cloudbuild/src/api/CloudbuildClient.ts index 4219ecabc1..572d29b6b8 100644 --- a/plugins/cloudbuild/src/api/CloudbuildClient.ts +++ b/plugins/cloudbuild/src/api/CloudbuildClient.ts @@ -20,40 +20,43 @@ import { ActionsGetWorkflowResponseData, Builds, } from '../api/types'; +import { OAuthApi } from '@backstage/core'; export class CloudbuildClient implements CloudbuildApi { + constructor(private readonly googleAuthApi: OAuthApi) {} + 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`, + `https://cloudbuild.googleapis.com/v1/projects/${encodeURIComponent( + projectId, + )}/builds/${encodeURIComponent(runId)}:retry`, { headers: new Headers({ Accept: '*/*', - Authorization: `Bearer ${token}`, + Authorization: `Bearer ${await this.getToken()}`, }), }, ); } async listWorkflowRuns({ - token, projectId, }: { - token: string; projectId: string; }): Promise { const workflowRuns = await fetch( - `https://cloudbuild.googleapis.com/v1/projects/${projectId}/builds`, + `https://cloudbuild.googleapis.com/v1/projects/${encodeURIComponent( + projectId, + )}/builds`, { headers: new Headers({ Accept: '*/*', - Authorization: `Bearer ${token}`, + Authorization: `Bearer ${await this.getToken()}`, }), }, ); @@ -68,20 +71,20 @@ export class CloudbuildClient implements CloudbuildApi { 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}`, + `https://cloudbuild.googleapis.com/v1/projects/${encodeURIComponent( + projectId, + )}/builds/${encodeURIComponent(id)}`, { headers: new Headers({ Accept: '*/*', - Authorization: `Bearer ${token}`, + Authorization: `Bearer ${await this.getToken()}`, }), }, ); @@ -91,20 +94,20 @@ export class CloudbuildClient implements CloudbuildApi { 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}`, + `https://cloudbuild.googleapis.com/v1/projects/${encodeURIComponent( + projectId, + )}/builds/${encodeURIComponent(id)}`, { headers: new Headers({ Accept: '*/*', - Authorization: `Bearer ${token}`, + Authorization: `Bearer ${await this.getToken()}`, }), }, ); @@ -112,4 +115,14 @@ export class CloudbuildClient implements CloudbuildApi { return build; } + + async getToken(): Promise { + // NOTE(freben - gcp-projects): There's a .read-only variant of this scope that we could + // use for readonly operations, but that means we would ask the user for a + // second auth during creation and I decided to keep the wider scope for + // all ops for now + return this.googleAuthApi.getAccessToken( + 'https://www.googleapis.com/auth/cloud-platform', + ); + } } diff --git a/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunsDetails.ts b/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunsDetails.ts index ca0f843f11..ae5e750d15 100644 --- a/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunsDetails.ts +++ b/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunsDetails.ts @@ -13,22 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { useApi, googleAuthApiRef } from '@backstage/core'; +import { useApi } 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, }) diff --git a/plugins/cloudbuild/src/components/useWorkflowRuns.ts b/plugins/cloudbuild/src/components/useWorkflowRuns.ts index bc887f9a1a..7086ba1c02 100644 --- a/plugins/cloudbuild/src/components/useWorkflowRuns.ts +++ b/plugins/cloudbuild/src/components/useWorkflowRuns.ts @@ -17,12 +17,11 @@ 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 { useApi, 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); @@ -32,12 +31,8 @@ export function useWorkflowRuns({ projectId }: { projectId: string }) { 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( @@ -52,7 +47,6 @@ export function useWorkflowRuns({ projectId }: { projectId: string }) { onReRunClick: async () => { try { await api.reRunWorkflow({ - token, projectId, runId: run.id, }); diff --git a/plugins/cloudbuild/src/plugin.ts b/plugins/cloudbuild/src/plugin.ts index 61ff78198f..ee997cc7ec 100644 --- a/plugins/cloudbuild/src/plugin.ts +++ b/plugins/cloudbuild/src/plugin.ts @@ -17,6 +17,7 @@ import { createPlugin, createRouteRef, createApiFactory, + googleAuthApiRef, } from '@backstage/core'; import { cloudbuildApiRef, CloudbuildClient } from './api'; @@ -32,5 +33,13 @@ export const buildRouteRef = createRouteRef({ export const plugin = createPlugin({ id: 'cloudbuild', - apis: [createApiFactory(cloudbuildApiRef, new CloudbuildClient())], + apis: [ + createApiFactory({ + api: cloudbuildApiRef, + deps: { googleAuthApi: googleAuthApiRef }, + factory({ googleAuthApi }) { + return new CloudbuildClient(googleAuthApi); + }, + }), + ], }); From 6002842bfbb65c8b301c1ab96f6ec6d37339bc4c Mon Sep 17 00:00:00 2001 From: ebarrios Date: Wed, 23 Sep 2020 17:11:30 +0200 Subject: [PATCH 11/27] Change Promise to Promise in reRunWorkflow function --- plugins/cloudbuild/src/api/CloudbuildClient.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/cloudbuild/src/api/CloudbuildClient.ts b/plugins/cloudbuild/src/api/CloudbuildClient.ts index 572d29b6b8..ead6a27b3e 100644 --- a/plugins/cloudbuild/src/api/CloudbuildClient.ts +++ b/plugins/cloudbuild/src/api/CloudbuildClient.ts @@ -31,8 +31,8 @@ export class CloudbuildClient implements CloudbuildApi { }: { projectId: string; runId: string; - }): Promise { - return await fetch( + }): Promise { + await fetch( `https://cloudbuild.googleapis.com/v1/projects/${encodeURIComponent( projectId, )}/builds/${encodeURIComponent(runId)}:retry`, From bd26c9c8da3da3c49d6b796bd5d2b3b34e3f101f Mon Sep 17 00:00:00 2001 From: ebarrios Date: Wed, 23 Sep 2020 17:21:23 +0200 Subject: [PATCH 12/27] Removed total_count from ActionsListWorkflowRunsForRepoResponseData type and calculate it on the fly. Clean up types.ts to remove duplicated entry (builds = ActionsListWorkflowRunsForRepoResponseData). Remove redundant line on router.tsx. Removed extra space in title for WarningPanel in Router.ts. --- plugins/cloudbuild/src/api/CloudbuildClient.ts | 10 ++-------- plugins/cloudbuild/src/api/types.ts | 5 ----- plugins/cloudbuild/src/components/Router.tsx | 5 ++--- plugins/cloudbuild/src/components/useWorkflowRuns.ts | 2 +- 4 files changed, 5 insertions(+), 17 deletions(-) diff --git a/plugins/cloudbuild/src/api/CloudbuildClient.ts b/plugins/cloudbuild/src/api/CloudbuildClient.ts index ead6a27b3e..0c3223aa04 100644 --- a/plugins/cloudbuild/src/api/CloudbuildClient.ts +++ b/plugins/cloudbuild/src/api/CloudbuildClient.ts @@ -18,7 +18,6 @@ import { CloudbuildApi } from './CloudbuildApi'; import { ActionsListWorkflowRunsForRepoResponseData, ActionsGetWorkflowResponseData, - Builds, } from '../api/types'; import { OAuthApi } from '@backstage/core'; @@ -61,14 +60,9 @@ export class CloudbuildClient implements CloudbuildApi { }, ); - const builds: Builds = await workflowRuns.json(); + const builds: ActionsListWorkflowRunsForRepoResponseData = await workflowRuns.json(); - const response: ActionsListWorkflowRunsForRepoResponseData = { - total_count: builds.builds.length, - builds: builds.builds, - }; - - return response; + return builds; } async getWorkflow({ projectId, diff --git a/plugins/cloudbuild/src/api/types.ts b/plugins/cloudbuild/src/api/types.ts index bcbde4df66..7c64fb52b8 100644 --- a/plugins/cloudbuild/src/api/types.ts +++ b/plugins/cloudbuild/src/api/types.ts @@ -15,14 +15,9 @@ */ export interface ActionsListWorkflowRunsForRepoResponseData { - total_count: number; builds: ActionsGetWorkflowResponseData[]; } -export type Builds = { - builds: ActionsGetWorkflowResponseData[]; -}; - export type ActionsGetWorkflowResponseData = { id: string; status: string; diff --git a/plugins/cloudbuild/src/components/Router.tsx b/plugins/cloudbuild/src/components/Router.tsx index 65eec7cde9..b2653a05f2 100644 --- a/plugins/cloudbuild/src/components/Router.tsx +++ b/plugins/cloudbuild/src/components/Router.tsx @@ -23,13 +23,12 @@ 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] !== ''; + Boolean(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/useWorkflowRuns.ts b/plugins/cloudbuild/src/components/useWorkflowRuns.ts index 7086ba1c02..2ebed17e97 100644 --- a/plugins/cloudbuild/src/components/useWorkflowRuns.ts +++ b/plugins/cloudbuild/src/components/useWorkflowRuns.ts @@ -39,7 +39,7 @@ export function useWorkflowRuns({ projectId }: { projectId: string }) { ( workflowRunsData: ActionsListWorkflowRunsForRepoResponseData, ): WorkflowRun[] => { - setTotal(workflowRunsData.total_count); + setTotal(workflowRunsData.builds.length); // Transformation here return workflowRunsData.builds.map(run => ({ message: run.substitutions.REPO_NAME, From 8e2a9082cd028b56bb7f0c6ab94a6e6b8b4356fb Mon Sep 17 00:00:00 2001 From: ebarrios Date: Wed, 23 Sep 2020 17:25:40 +0200 Subject: [PATCH 13/27] Replaced warning with with a in WorkflowRunDetails.tsx --- .../components/WorkflowRunDetails/WorkflowRunDetails.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx b/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx index 9f8a6c2229..ac13ee7cef 100644 --- a/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx +++ b/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ import { Entity } from '@backstage/catalog-model'; -import { Link } from '@backstage/core'; +import { Link, WarningPanel } from '@backstage/core'; import { Breadcrumbs, LinearProgress, @@ -71,9 +71,9 @@ export const WorkflowRunDetails = ({ entity }: { entity: Entity }) => { const classes = useStyles(); if (error) { return ( - - Failed to load build, {error.message} - + + Failed to load build, {error.message}. + ); } else if (loading) { return ; From e3d9a11d45ad5cdc37584c4a7249df634ac93573 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Wed, 23 Sep 2020 17:26:35 +0200 Subject: [PATCH 14/27] Removed commented lines from WorkflowRunsTable.tsx --- .../src/components/WorkflowRunsTable/WorkflowRunsTable.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/cloudbuild/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx b/plugins/cloudbuild/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx index 7c361f7fc4..a0868c806b 100644 --- a/plugins/cloudbuild/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx +++ b/plugins/cloudbuild/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx @@ -57,7 +57,6 @@ const generatedColumns: TableColumn[] = [ render: (row: Partial) => (

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

- {/*

{row.substitutions?.COMMIT_SHA}

*/}
), }, @@ -69,7 +68,6 @@ const generatedColumns: TableColumn[] = [ render: (row: Partial) => ( {row.message} From 631d59e5bdd116ad1de90f775a06753d6f5f4027 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Wed, 23 Sep 2020 17:28:43 +0200 Subject: [PATCH 15/27] Renamed CLOUDBUID_ANNOTATION from google.com/cloudbuild to google.com/cloudbuild-project-slug --- plugins/cloudbuild/src/components/useProjectName.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/cloudbuild/src/components/useProjectName.ts b/plugins/cloudbuild/src/components/useProjectName.ts index 5f6752dce5..0c227f690d 100644 --- a/plugins/cloudbuild/src/components/useProjectName.ts +++ b/plugins/cloudbuild/src/components/useProjectName.ts @@ -17,7 +17,7 @@ import { useAsync } from 'react-use'; import { Entity } from '@backstage/catalog-model'; -export const CLOUDBUILD_ANNOTATION = 'google.com/cloudbuild'; +export const CLOUDBUILD_ANNOTATION = 'google.com/cloudbuild-project-slug'; export const useProjectName = (entity: Entity) => { const { value, loading, error } = useAsync(async () => { From 8a0113b4a7b48011405360c77dee8fce5bfd2610 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Wed, 23 Sep 2020 17:33:19 +0200 Subject: [PATCH 16/27] Replaced id: for id: run.id --- plugins/cloudbuild/src/components/useWorkflowRuns.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/cloudbuild/src/components/useWorkflowRuns.ts b/plugins/cloudbuild/src/components/useWorkflowRuns.ts index 2ebed17e97..8b25890a26 100644 --- a/plugins/cloudbuild/src/components/useWorkflowRuns.ts +++ b/plugins/cloudbuild/src/components/useWorkflowRuns.ts @@ -43,7 +43,7 @@ export function useWorkflowRuns({ projectId }: { projectId: string }) { // Transformation here return workflowRunsData.builds.map(run => ({ message: run.substitutions.REPO_NAME, - id: `${run.id}`, + id: run.id, onReRunClick: async () => { try { await api.reRunWorkflow({ From 295d98d59597c51a9551cce3250f335b4d6aa495 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Wed, 23 Sep 2020 17:39:50 +0200 Subject: [PATCH 17/27] Renamed onReRunClick in useWorkflowRuns.ts to rerun --- .../src/components/WorkflowRunsTable/WorkflowRunsTable.tsx | 4 ++-- plugins/cloudbuild/src/components/useWorkflowRuns.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/cloudbuild/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx b/plugins/cloudbuild/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx index a0868c806b..59c149da32 100644 --- a/plugins/cloudbuild/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx +++ b/plugins/cloudbuild/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx @@ -35,7 +35,7 @@ export type WorkflowRun = { status: string; substitutions: Substitutions; createTime: string; - onReRunClick: () => void; + rerun: () => void; }; const generatedColumns: TableColumn[] = [ @@ -105,7 +105,7 @@ const generatedColumns: TableColumn[] = [ title: 'Actions', render: (row: Partial) => ( - + diff --git a/plugins/cloudbuild/src/components/useWorkflowRuns.ts b/plugins/cloudbuild/src/components/useWorkflowRuns.ts index 8b25890a26..b6d6c2c620 100644 --- a/plugins/cloudbuild/src/components/useWorkflowRuns.ts +++ b/plugins/cloudbuild/src/components/useWorkflowRuns.ts @@ -44,7 +44,7 @@ export function useWorkflowRuns({ projectId }: { projectId: string }) { return workflowRunsData.builds.map(run => ({ message: run.substitutions.REPO_NAME, id: run.id, - onReRunClick: async () => { + rerun: async () => { try { await api.reRunWorkflow({ projectId, From f322ab957fe99969b4adc4d1c8bd1dbe8689c551 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Wed, 23 Sep 2020 17:54:43 +0200 Subject: [PATCH 18/27] Using moment to parse the date --- .../src/components/WorkflowRunsTable/WorkflowRunsTable.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/plugins/cloudbuild/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx b/plugins/cloudbuild/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx index 59c149da32..254e6f89ed 100644 --- a/plugins/cloudbuild/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx +++ b/plugins/cloudbuild/src/components/WorkflowRunsTable/WorkflowRunsTable.tsx @@ -26,6 +26,7 @@ import { useProjectName } from '../useProjectName'; import { Entity } from '@backstage/catalog-model'; import { Substitutions } from '../../api/types'; import { buildRouteRef } from '../../plugin'; +import moment from 'moment'; export type WorkflowRun = { id: string; @@ -94,10 +95,7 @@ const generatedColumns: TableColumn[] = [ 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)} -

+

{moment(row.createTime).format('DD-MM-YYYY hh:mm:ss')}

), }, From f04f36e2db4e09af9bb50aa15599b0400a9a4d05 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Wed, 23 Sep 2020 18:36:26 +0200 Subject: [PATCH 19/27] Change way of getting service account with new URL and qs --- plugins/cloudbuild/package.json | 1 + .../WorkflowRunDetails/WorkflowRunDetails.tsx | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 2bab58690e..02d14b5082 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -32,6 +32,7 @@ "@octokit/rest": "^18.0.0", "@octokit/types": "^5.4.1", "moment": "^2.27.0", + "qs": "^6.9.4", "react": "^16.13.1", "react-dom": "^16.13.1", "react-lazylog": "^4.5.3", diff --git a/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx b/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx index ac13ee7cef..94c6fa4d25 100644 --- a/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx +++ b/plugins/cloudbuild/src/components/WorkflowRunDetails/WorkflowRunDetails.tsx @@ -30,6 +30,7 @@ import { Typography, } from '@material-ui/core'; import ExternalLinkIcon from '@material-ui/icons/Launch'; +import qs from 'qs'; import React from 'react'; import { useProjectName } from '../useProjectName'; import { WorkflowRunStatus } from '../WorkflowRunStatus'; @@ -66,8 +67,6 @@ export const WorkflowRunDetails = ({ entity }: { entity: Entity }) => { const details = useWorkflowRunsDetails(projectId); - const serviceAccount = (details.value?.logUrl ?? '=').split('='); - const classes = useStyles(); if (error) { return ( @@ -77,7 +76,14 @@ export const WorkflowRunDetails = ({ entity }: { entity: Entity }) => { ); } else if (loading) { return ; + } else if (details.value?.logUrl === undefined) { + return ; } + + const serviceAccount = qs.parse(new URL(details.value?.logUrl).search, { + ignoreQueryPrefix: true, + }).project; + return (
@@ -118,7 +124,7 @@ export const WorkflowRunDetails = ({ entity }: { entity: Entity }) => { Service Account - {`${serviceAccount[1]}`}@cloudbuild.gserviceaccount.com + {`${serviceAccount}`}@cloudbuild.gserviceaccount.com From d2dc65637df6d79fcb952461843f94961e573bcc Mon Sep 17 00:00:00 2001 From: ebarrios Date: Wed, 23 Sep 2020 18:41:51 +0200 Subject: [PATCH 20/27] Bumped version to alpha.23 --- plugins/cloudbuild/package.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 02d14b5082..39e0a82ccf 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-cloudbuild", - "version": "0.1.1-alpha.22", + "version": "0.1.1-alpha.23", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,11 +21,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.1.1-alpha.22", - "@backstage/core": "^0.1.1-alpha.22", - "@backstage/core-api": "^0.1.1-alpha.22", - "@backstage/plugin-catalog": "^0.1.1-alpha.22", - "@backstage/theme": "^0.1.1-alpha.22", + "@backstage/catalog-model": "^0.1.1-alpha.23", + "@backstage/core": "^0.1.1-alpha.23", + "@backstage/core-api": "^0.1.1-alpha.23", + "@backstage/plugin-catalog": "^0.1.1-alpha.23", + "@backstage/theme": "^0.1.1-alpha.23", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -41,8 +41,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.1.1-alpha.22", - "@backstage/dev-utils": "^0.1.1-alpha.22", + "@backstage/cli": "^0.1.1-alpha.23", + "@backstage/dev-utils": "^0.1.1-alpha.23", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", From 8eef1d94348fc7d2475f5f8c5cf4d4128bf983f9 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Wed, 23 Sep 2020 18:43:09 +0200 Subject: [PATCH 21/27] Removed unused total_count entry --- .../src/components/WorkflowRunDetails/useWorkflowRunJobs.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunJobs.ts b/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunJobs.ts index 2c3df61abe..2150e01d98 100644 --- a/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunJobs.ts +++ b/plugins/cloudbuild/src/components/WorkflowRunDetails/useWorkflowRunJobs.ts @@ -22,7 +22,6 @@ export const useWorkflowRunJobs = (jobsUrl?: string) => { > => { if (jobsUrl === undefined) { return { - total_count: 0, builds: [], }; } From 167b8a5106975e605c6b7c73df3cf460166899c0 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Wed, 23 Sep 2020 18:55:35 +0200 Subject: [PATCH 22/27] Ran prettier on the two files modified directly on github --- packages/app/package.json | 1 - packages/app/src/plugins.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/app/package.json b/packages/app/package.json index 52570ebc0e..40ab44369b 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -29,7 +29,6 @@ "@backstage/plugin-welcome": "^0.1.1-alpha.23", "@backstage/test-utils": "^0.1.1-alpha.23", "@backstage/theme": "^0.1.1-alpha.23", - "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@octokit/rest": "^18.0.0", diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index 34c055c661..24f8b89e1c 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -34,4 +34,4 @@ 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 Kubernetes } from '@backstage/plugin-kubernetes'; -export { plugin as Cloudbuild } from '@backstage/plugin-cloudbuild'; \ No newline at end of file +export { plugin as Cloudbuild } from '@backstage/plugin-cloudbuild'; From 96a0194d99f895e07d98856292c7cfd57b785907 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Fri, 25 Sep 2020 14:52:41 +0200 Subject: [PATCH 23/27] Remove duplicated line --- packages/app/src/components/catalog/EntityPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 9061ea9516..22cf36e5fb 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -26,7 +26,6 @@ import { Router as JenkinsRouter, isPluginApplicableToEntity as isJenkinsAvailable, LatestRunCard as JenkinsLatestRunCard, - Router as JenkinsRouter, } from '@backstage/plugin-jenkins'; import { isPluginApplicableToEntity as isCircleCIAvailable, From 804e93071a67ab8cd0b6b254548a257907a57cef Mon Sep 17 00:00:00 2001 From: ebarrios Date: Fri, 25 Sep 2020 15:18:14 +0200 Subject: [PATCH 24/27] Updated package.json in plugin to include missing deps --- plugins/cloudbuild/package.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 39e0a82ccf..06699231c7 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -48,7 +48,9 @@ "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", - "jest-fetch-mock": "^3.0.3" + "jest-fetch-mock": "^3.0.3", + "msw": "^0.20.5", + "node-fetch": "^2.6.1" }, "files": [ "dist" From a930b485c3e8592fc8e45a65d27c62ef86318e81 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Mon, 28 Sep 2020 10:16:22 +0200 Subject: [PATCH 25/27] Removed core-api from dependecie list --- plugins/cloudbuild/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 06699231c7..3367bb6cca 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -23,7 +23,6 @@ "dependencies": { "@backstage/catalog-model": "^0.1.1-alpha.23", "@backstage/core": "^0.1.1-alpha.23", - "@backstage/core-api": "^0.1.1-alpha.23", "@backstage/plugin-catalog": "^0.1.1-alpha.23", "@backstage/theme": "^0.1.1-alpha.23", "@material-ui/core": "^4.11.0", From 16bf17903f5172490c2b19351808fb7131238307 Mon Sep 17 00:00:00 2001 From: ebarrios Date: Mon, 28 Sep 2020 10:18:15 +0200 Subject: [PATCH 26/27] Removed leftover token from getWorkflow api call --- plugins/cloudbuild/src/api/CloudbuildApi.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/cloudbuild/src/api/CloudbuildApi.ts b/plugins/cloudbuild/src/api/CloudbuildApi.ts index 7091ddf1be..5aecf970ef 100644 --- a/plugins/cloudbuild/src/api/CloudbuildApi.ts +++ b/plugins/cloudbuild/src/api/CloudbuildApi.ts @@ -33,7 +33,6 @@ export type CloudbuildApi = { projectId, id, }: { - token: string; projectId: string; id: string; }) => Promise; From f192fa5c9693ffbece6512ce7a6bff0bdd645dcb Mon Sep 17 00:00:00 2001 From: ebarrios Date: Mon, 28 Sep 2020 10:20:49 +0200 Subject: [PATCH 27/27] Replaced Typography error with a WarningPanel --- plugins/cloudbuild/src/components/Cards/Cards.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/plugins/cloudbuild/src/components/Cards/Cards.tsx b/plugins/cloudbuild/src/components/Cards/Cards.tsx index 84d1bf9109..bbfb0b59c3 100644 --- a/plugins/cloudbuild/src/components/Cards/Cards.tsx +++ b/plugins/cloudbuild/src/components/Cards/Cards.tsx @@ -18,18 +18,13 @@ 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 { Link, Theme, makeStyles, LinearProgress } from '@material-ui/core'; import { InfoCard, StructuredMetadataTable, errorApiRef, useApi, + WarningPanel, } from '@backstage/core'; import ExternalLinkIcon from '@material-ui/icons/Launch'; import { CLOUDBUILD_ANNOTATION } from '../useProjectName'; @@ -53,7 +48,8 @@ const WidgetContent = ({ branch: string; }) => { const classes = useStyles(); - if (error) return Couldn't fetch latest {branch} run; + if (error) + return Couldn't fetch latest {branch} run; if (loading) return ; return (