Created a plugin for cloud build as CI/CD
This commit is contained in:
+9
-8
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 <GitHubActionsRouter entity={entity} />;
|
||||
case isCircleCIAvailable(entity):
|
||||
return <CircleCIRouter entity={entity} />;
|
||||
case isCloudbuildAvailable(entity):
|
||||
return <CloudbuildRouter entity={entity} />;
|
||||
default:
|
||||
return (
|
||||
<WarningPanel title="CI/CD switcher:">
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
module.exports = {
|
||||
extends: [require.resolve('@backstage/cli/config/eslint')],
|
||||
};
|
||||
@@ -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.
|
||||
@@ -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();
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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<CloudbuildApi>({
|
||||
id: 'plugin.cloudbuild.service',
|
||||
description: 'Used by the Cloudbuild plugin to make requests',
|
||||
});
|
||||
|
||||
export type CloudbuildApi = {
|
||||
listWorkflowRuns: ({
|
||||
token,
|
||||
projectId,
|
||||
}: {
|
||||
token: string;
|
||||
projectId: string;
|
||||
}) => Promise<ActionsListWorkflowRunsForRepoResponseData>;
|
||||
getWorkflow: ({
|
||||
token,
|
||||
projectId,
|
||||
id,
|
||||
}: {
|
||||
token: string;
|
||||
projectId: string;
|
||||
id: string;
|
||||
}) => Promise<ActionsGetWorkflowResponseData>;
|
||||
getWorkflowRun: ({
|
||||
token,
|
||||
projectId,
|
||||
id,
|
||||
}: {
|
||||
token: string;
|
||||
projectId: string;
|
||||
id: string;
|
||||
}) => Promise<ActionsGetWorkflowRunResponseData>;
|
||||
reRunWorkflow: ({
|
||||
token,
|
||||
projectId,
|
||||
runId,
|
||||
}: {
|
||||
token: string;
|
||||
projectId: string;
|
||||
runId: string;
|
||||
}) => Promise<any>;
|
||||
downloadJobLogsForWorkflowRun: ({
|
||||
token,
|
||||
projectId,
|
||||
runId,
|
||||
}: {
|
||||
token: string;
|
||||
projectId: string;
|
||||
runId: string;
|
||||
}) => Promise<EndpointInterface>;
|
||||
};
|
||||
@@ -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<any> {
|
||||
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<ActionsListWorkflowRunsForRepoResponseData> {
|
||||
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<ActionsGetWorkflowResponseData> {
|
||||
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<ActionsGetWorkflowRunResponseData> {
|
||||
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<EndpointInterface> {
|
||||
// console.log("Token: ",token," projectId: ", projectId," runId: ",runId)
|
||||
// const workflow = await new Octokit({
|
||||
// auth: token,
|
||||
// }).actions.downloadJobLogsForWorkflowRun({
|
||||
// owner,
|
||||
// repo,
|
||||
// job_id: runId,
|
||||
// });
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<Theme>({
|
||||
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 <Typography>Couldn't fetch latest {branch} run</Typography>;
|
||||
if (loading) return <LinearProgress />;
|
||||
return (
|
||||
<StructuredMetadataTable
|
||||
metadata={{
|
||||
status: (
|
||||
<>
|
||||
<WorkflowRunStatus status={lastRun.status} />
|
||||
</>
|
||||
),
|
||||
message: lastRun.message,
|
||||
url: (
|
||||
<Link href={lastRun.googleUrl} target="_blank">
|
||||
See more on Google{' '}
|
||||
<ExternalLinkIcon className={classes.externalLinkIcon} />
|
||||
</Link>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<InfoCard title={`Last ${branch} build`}>
|
||||
<WidgetContent
|
||||
error={error}
|
||||
loading={loading}
|
||||
branch={branch}
|
||||
lastRun={lastRun}
|
||||
/>
|
||||
</InfoCard>
|
||||
);
|
||||
};
|
||||
|
||||
export const LatestWorkflowsForBranchCard = ({
|
||||
entity,
|
||||
branch = 'master',
|
||||
}: {
|
||||
entity: Entity;
|
||||
branch: string;
|
||||
}) => (
|
||||
<InfoCard title={`Last ${branch} build`}>
|
||||
<WorkflowRunsTable entity={entity} />
|
||||
</InfoCard>
|
||||
);
|
||||
@@ -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';
|
||||
@@ -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) ? (
|
||||
<WarningPanel title=" Cloudbuild plugin:">
|
||||
<pre>{CLOUDBUILD_ANNOTATION}</pre> annotation is missing on the entity.
|
||||
</WarningPanel>
|
||||
) : (
|
||||
<Routes>
|
||||
<Route
|
||||
path={`/${rootRouteRef.path}`}
|
||||
element={<WorkflowRunsTable entity={entity} />}
|
||||
/>
|
||||
<Route
|
||||
path={`/${buildRouteRef.path}`}
|
||||
element={<WorkflowRunDetails entity={entity} />}
|
||||
/>
|
||||
)
|
||||
</Routes>
|
||||
);
|
||||
@@ -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>(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 (
|
||||
// <Box>
|
||||
// {jobs &&
|
||||
// jobs.total_count > 0 &&
|
||||
// jobs.jobs.map((job: Build) => (
|
||||
// <JobListItem
|
||||
// job={job}
|
||||
// className={
|
||||
// job.status !== 'success' ? classes.failed : classes.success
|
||||
// }
|
||||
// entity={entity}
|
||||
// />
|
||||
// ))}
|
||||
// </Box>
|
||||
// );
|
||||
// };
|
||||
|
||||
// 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 (
|
||||
// <TableRow>
|
||||
// <TableCell>
|
||||
// <ListItemText
|
||||
// primary={step.name}
|
||||
// secondary={getElapsedTime(step.pullTiming.startTime, step.pullTiming.endTime)}
|
||||
// />
|
||||
// </TableCell>
|
||||
// <TableCell>
|
||||
// <WorkflowRunStatus status={step.status.toUpperCase()} />
|
||||
// </TableCell>
|
||||
// </TableRow>
|
||||
// );
|
||||
// };
|
||||
|
||||
// const JobListItem = ({
|
||||
// job,
|
||||
// className,
|
||||
// entity,
|
||||
// }: {
|
||||
// job: Build;
|
||||
// className: string;
|
||||
// entity: Entity;
|
||||
// }) => {
|
||||
// const classes = useStyles();
|
||||
// return (
|
||||
// <Accordion TransitionProps={{ unmountOnExit: true }} className={className}>
|
||||
// <AccordionSummary
|
||||
// expandIcon={<ExpandMoreIcon />}
|
||||
// aria-controls={`panel-${name}-content`}
|
||||
// id={`panel-${name}-header`}
|
||||
// IconButtonProps={{
|
||||
// className: classes.button,
|
||||
// }}
|
||||
// >
|
||||
// <Typography variant="button">
|
||||
// {job.name} ({getElapsedTime(job.startTime, job.finishTime)})
|
||||
// </Typography>
|
||||
// </AccordionSummary>
|
||||
// <AccordionDetails className={classes.accordionDetails}>
|
||||
// <TableContainer>
|
||||
// <Table>
|
||||
// {job.steps.map((step: Step) => (
|
||||
// <StepView step={step} />
|
||||
// ))}
|
||||
// </Table>
|
||||
// </TableContainer>
|
||||
// </AccordionDetails>
|
||||
// {job.status === 'QUEUED' || job.status === 'WORKING' ? (
|
||||
// <WorkflowRunLogs runId={job.id} inProgress entity={entity} />
|
||||
// ) : (
|
||||
// <WorkflowRunLogs runId={job.id} inProgress={false} entity={entity} />
|
||||
// )}
|
||||
// </Accordion>
|
||||
// );
|
||||
// };
|
||||
|
||||
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 (
|
||||
<Typography variant="h6" color="error">
|
||||
Failed to load build, {error.message}
|
||||
</Typography>
|
||||
);
|
||||
} else if (loading) {
|
||||
return <LinearProgress />;
|
||||
}
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Breadcrumbs aria-label="breadcrumb">
|
||||
<Link to="..">Workflow runs</Link>
|
||||
<Typography>Workflow run details</Typography>
|
||||
</Breadcrumbs>
|
||||
<TableContainer component={Paper} className={classes.table}>
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Branch</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{details.value?.substitutions.BRANCH_NAME}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Message</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{details.value?.substitutions.REPO_NAME}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Commit ID</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{details.value?.substitutions.COMMIT_SHA}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Status</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<WorkflowRunStatus status={details.value?.status} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Author</Typography>
|
||||
</TableCell>
|
||||
<TableCell>{`${details.value?.name}`}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Typography noWrap>Links</Typography>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{details.value?.logUrl && (
|
||||
<MaterialLink target="_blank" href={details.value.logUrl}>
|
||||
Workflow runs on Google{' '}
|
||||
<ExternalLinkIcon className={classes.externalLinkIcon} />
|
||||
</MaterialLink>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{/* <TableRow>
|
||||
<TableCell colSpan={2}>
|
||||
<Typography noWrap>Jobs</Typography>
|
||||
{loading ? (
|
||||
<CircularProgress />
|
||||
) : (
|
||||
<JobsList jobs={steps.value} entity={entity} />
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow> */}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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<Jobs> => {
|
||||
if (jobsUrl === undefined) {
|
||||
return {
|
||||
total_count: 0,
|
||||
jobs: [],
|
||||
};
|
||||
}
|
||||
|
||||
const data = await fetch(jobsUrl).then(d => d.json());
|
||||
return data;
|
||||
}, [jobsUrl]);
|
||||
return jobs;
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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<Theme>(() => ({
|
||||
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 (
|
||||
<Suspense fallback={<LinearProgress />}>
|
||||
<div className={className}>
|
||||
<LazyLog
|
||||
text={jobLogs ?? 'No Values Found'}
|
||||
extraLines={1}
|
||||
caseInsensitive
|
||||
enableSearch
|
||||
formatPart={line => {
|
||||
if (
|
||||
line.toLocaleLowerCase().includes('error') ||
|
||||
line.toLocaleLowerCase().includes('failed') ||
|
||||
line.toLocaleLowerCase().includes('failure')
|
||||
) {
|
||||
return (
|
||||
<LinePart style={{ color: 'red' }} part={{ text: line }} />
|
||||
);
|
||||
}
|
||||
return line;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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 (
|
||||
<Accordion TransitionProps={{ unmountOnExit: true }} disabled={inProgress}>
|
||||
<AccordionSummary
|
||||
expandIcon={<ExpandMoreIcon />}
|
||||
aria-controls={`panel-${name}-content`}
|
||||
id={`panel-${name}-header`}
|
||||
IconButtonProps={{
|
||||
className: classes.button,
|
||||
}}
|
||||
>
|
||||
<Typography variant="button">
|
||||
{jobLogs.loading ? <CircularProgress /> : 'Job Log'}
|
||||
</Typography>
|
||||
<Tooltip title="Open Log" TransitionComponent={Zoom} arrow>
|
||||
<DescriptionIcon
|
||||
onClick={event => {
|
||||
event.stopPropagation();
|
||||
handleOpen();
|
||||
}}
|
||||
style={{ marginLeft: 'auto' }}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Modal
|
||||
className={classes.modal}
|
||||
onClick={event => event.stopPropagation()}
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
>
|
||||
<Fade in={open}>
|
||||
<DisplayLog
|
||||
jobLogs={jobLogs.value || undefined}
|
||||
className={classes.modalLog}
|
||||
/>
|
||||
</Fade>
|
||||
</Modal>
|
||||
</AccordionSummary>
|
||||
{jobLogs.value && (
|
||||
<DisplayLog
|
||||
jobLogs={jobLogs.value || undefined}
|
||||
className={classes.normalLog}
|
||||
/>
|
||||
)}
|
||||
</Accordion>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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 (
|
||||
<>
|
||||
<StatusPending /> Queued
|
||||
</>
|
||||
);
|
||||
case 'working':
|
||||
return (
|
||||
<>
|
||||
<StatusRunning /> In progress
|
||||
</>
|
||||
);
|
||||
case 'success':
|
||||
return (
|
||||
<>
|
||||
<StatusOK /> Completed
|
||||
</>
|
||||
);
|
||||
case 'cancelled':
|
||||
return (
|
||||
<>
|
||||
<StatusAborted /> Cancelled
|
||||
</>
|
||||
);
|
||||
case 'failure':
|
||||
return (
|
||||
<>
|
||||
<StatusError /> Failed
|
||||
</>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<>
|
||||
<StatusPending /> Pending
|
||||
</>
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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<WorkflowRun>) => (
|
||||
<Box display="flex" alignItems="center">
|
||||
<WorkflowRunStatus status={row.status} />
|
||||
</Box>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Build',
|
||||
field: 'id',
|
||||
type: 'numeric',
|
||||
width: '150px',
|
||||
render: (row: Partial<WorkflowRun>) => (
|
||||
<Typography variant="body2" noWrap>
|
||||
<p>{row.id?.substring(0, 8)}</p>
|
||||
{/* <p>{row.substitutions?.COMMIT_SHA}</p> */}
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Source',
|
||||
field: 'source',
|
||||
highlight: true,
|
||||
width: '200px',
|
||||
render: (row: Partial<WorkflowRun>) => (
|
||||
<Link
|
||||
component={RouterLink}
|
||||
// to={`https://www.github.com/trivago/${row.message}`}
|
||||
to={generatePath(buildRouteRef.path, { id: row.id! })}
|
||||
>
|
||||
{row.org}/{row.message}
|
||||
</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Ref',
|
||||
render: (row: Partial<WorkflowRun>) => (
|
||||
<Typography variant="body2" noWrap>
|
||||
<p>{row.substitutions?.BRANCH_NAME}</p>
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Commit',
|
||||
render: (row: Partial<WorkflowRun>) => (
|
||||
<Typography variant="body2" noWrap>
|
||||
<p>{row.substitutions?.SHORT_SHA}</p>
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Created',
|
||||
render: (row: Partial<WorkflowRun>) => (
|
||||
<Typography variant="body2" noWrap>
|
||||
<p>
|
||||
{row.createTime?.substring(6, 7)}/{row.createTime?.substring(8, 10)}/
|
||||
{row.createTime?.substring(0, 4)}, {row.createTime?.substring(11, 16)}
|
||||
</p>
|
||||
</Typography>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
render: (row: Partial<WorkflowRun>) => (
|
||||
<Tooltip title="Rerun workflow">
|
||||
<IconButton onClick={row.onReRunClick}>
|
||||
<RetryIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
),
|
||||
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<Props> = ({
|
||||
projectName,
|
||||
loading,
|
||||
pageSize,
|
||||
page,
|
||||
retry,
|
||||
runs,
|
||||
onChangePage,
|
||||
onChangePageSize,
|
||||
total,
|
||||
}) => {
|
||||
return (
|
||||
<Table
|
||||
isLoading={loading}
|
||||
options={{ paging: true, pageSize, padding: 'dense' }}
|
||||
totalCount={total}
|
||||
page={page}
|
||||
actions={[
|
||||
{
|
||||
icon: () => <SyncIcon />,
|
||||
tooltip: 'Reload workflow runs',
|
||||
isFreeAction: true,
|
||||
onClick: () => retry(),
|
||||
},
|
||||
]}
|
||||
data={runs ?? []}
|
||||
onChangePage={onChangePage}
|
||||
onChangeRowsPerPage={onChangePageSize}
|
||||
style={{ width: '100%' }}
|
||||
title={
|
||||
<Box display="flex" alignItems="center">
|
||||
<GoogleIcon />
|
||||
<Box mr={1} />
|
||||
<Typography variant="h6">{projectName}</Typography>
|
||||
</Box>
|
||||
}
|
||||
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 (
|
||||
<WorkflowRunsTableView
|
||||
{...tableProps}
|
||||
loading={loading || tableProps.loading}
|
||||
retry={retry}
|
||||
onChangePageSize={setPageSize}
|
||||
onChangePage={setPage}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
@@ -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 };
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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())],
|
||||
});
|
||||
@@ -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();
|
||||
Reference in New Issue
Block a user