Merge pull request #8622 from julioz/julioz/gocd-plugin

Add GoCD plugin
This commit is contained in:
Ben Lambert
2022-01-12 14:00:59 +01:00
committed by GitHub
27 changed files with 1162 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-gocd': patch
---
Add GoCD plugin for CI/CD.
+10
View File
@@ -107,6 +107,13 @@ proxy:
headers:
Authorization: ${AIRFLOW_BASIC_AUTH_HEADER}
'/gocd':
target: https://your.gocd.instance.com/go/api
allowedMethods: ['GET']
allowedHeaders: ['Authorization']
headers:
Authorization: Basic ${GOCD_AUTH_CREDENTIALS}
organization:
name: My Company
@@ -450,3 +457,6 @@ azureDevOps:
apacheAirflow:
baseUrl: https://your.airflow.instance.com
gocd:
baseUrl: https://your.gocd.instance.com
@@ -204,6 +204,24 @@ browser when viewing that user.
This annotation can be used on a [User entity](descriptor-format.md#kind-user)
to note that it originated from that user on GitHub.
### gocd.org/pipelines
```yaml
# Example:
metadata:
annotations:
gocd.org/pipelines: backstage,backstage-pr,backstage-builder
```
The value of this annotation is a comma-separated list of the GoCD pipeline
names to fetch CI/CD information for.
The pipeline name is usually defined in the `gocd.yml` file for the pipeline
definition.
Specifying this annotation will enable GoCD related features in Backstage for
that entity.
### sentry.io/project-slug
```yaml
+12
View File
@@ -0,0 +1,12 @@
---
title: GoCD
author: SoundCloud
authorUrl: https://github.com/soundcloud
category: CI/CD
description: GoCD is an open-source tool which is used in software development to help teams and organizations automate the continuous delivery of software.
documentation: https://github.com/backstage/backstage/tree/master/plugins/gocd
iconUrl: https://pics.freeicons.io/uploads/icons/png/13646383971540553613-512.png
npmPackageName: '@backstage/plugin-gocd'
tags:
- ci
- cd
+1
View File
@@ -27,6 +27,7 @@
"@backstage/plugin-explore": "^0.3.24",
"@backstage/plugin-gcp-projects": "^0.3.12",
"@backstage/plugin-github-actions": "^0.4.30",
"@backstage/plugin-gocd": "^0.1.0",
"@backstage/plugin-graphiql": "^0.2.26",
"@backstage/plugin-home": "^0.4.9",
"@backstage/plugin-jenkins": "^0.5.16",
@@ -129,6 +129,7 @@ import {
EntityNewRelicDashboardContent,
EntityNewRelicDashboardCard,
} from '@backstage/plugin-newrelic-dashboard';
import { EntityGoCdContent, isGoCdAvailable } from '@backstage/plugin-gocd';
import React, { ReactNode, useMemo, useState } from 'react';
@@ -185,6 +186,10 @@ export const cicdContent = (
<EntityTravisCIContent />
</EntitySwitch.Case>
<EntitySwitch.Case if={isGoCdAvailable}>
<EntityGoCdContent />
</EntitySwitch.Case>
<EntitySwitch.Case if={isGithubActionsAvailable}>
<EntityGithubActionsContent />
</EntitySwitch.Case>
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+54
View File
@@ -0,0 +1,54 @@
# GoCD
Welcome to the GoCD plugin!
- View recent GoCD Builds
## Installation
GoCD Plugin exposes an entity tab component named `EntityGoCdContent`. You can include it in the
[`EntityPage.tsx`](https://github.com/backstage/backstage/blob/master/packages/app/src/components/catalog/EntityPage.tsx)`:
```tsx
// At the top imports
import { EntityGoCdContent } from '@backstage/plugin-gocd';
// Farther down at the component declaration
const componentEntityPage = (
<EntityLayout>
{/* Place the following section where you want the tab to appear */}
<EntityLayout.Route path="/go-cd" title="GoCD">
<EntityGoCdContent />
</EntityLayout.Route>
```
Now your plugin should be visible as a tab at the top of the entity pages,
specifically for components that are of the type `component`.
However, it warns of a missing `gocd.org/pipelines` annotation.
Add the annotation to your component [catalog-info.yaml](https://github.com/backstage/backstage/blob/master/catalog-info.yaml). You can refer to multiple GoCD pipelines by defining their names separated by commas, as shown in the highlighted example below:
```yaml
metadata:
annotations:
gocd.org/pipelines: '<NAME OF THE PIPELINE 1>[,<NAME OF PIPELINE 2>]'
```
The plugin requires to configure a GoCD API proxy with a `GOCD_AUTH_CREDENTIALS` for authentication in the [app-config.yaml](https://github.com/backstage/backstage/blob/master/app-config.yaml). Its value is an opaque token you can obtain directly from your GoCD instance, in the shape `base64(user + ':' + pass)`. For example, a user "root" and password "root" would become `base64('root:root') = cm9vdDpyb290`:
```yaml
proxy:
'/gocd':
target: '<go cd server host>/go/api'
allowedMethods: ['GET']
allowedHeaders: ['Authorization']
headers:
Authorization: Basic ${GOCD_AUTH_CREDENTIALS}
```
You should also include the `gocd` section to allow for the plugin to redirect back to GoCD pipelines in your deployed instance:
```yaml
gocd:
baseUrl: <go cd server host>
```
+24
View File
@@ -0,0 +1,24 @@
## API Report File for "@backstage/plugin-gocd"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
/// <reference types="react" />
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { Entity } from '@backstage/catalog-model';
// @public
export const EntityGoCdContent: () => JSX.Element;
// @public
export const GOCD_PIPELINES_ANNOTATION = 'gocd.org/pipelines';
// @public
export const gocdPlugin: BackstagePlugin<{}, {}>;
// @public
export const isGoCdAvailable: (entity: Entity) => boolean;
// (No @packageDocumentation comment for this package)
```
+25
View File
@@ -0,0 +1,25 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 interface Config {
/** Configurations for the GoCD plugin */
gocd: {
/**
* The base url of the GoCD installation.
* @visibility frontend
*/
baseUrl: string;
};
}
+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { gocdPlugin } from '../src/plugin';
createDevApp().registerPlugin(gocdPlugin).render();
+68
View File
@@ -0,0 +1,68 @@
{
"name": "@backstage/plugin-gocd",
"description": "A Backstage plugin that integrates towards GoCD",
"version": "0.1.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"private": false,
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "plugins/gocd"
},
"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.9.1",
"@backstage/core-components": "^0.8.3",
"@backstage/core-plugin-api": "^0.4.1",
"@backstage/errors": "^0.1.4",
"@backstage/plugin-catalog-react": "^0.6.0",
"@backstage/theme": "^0.2.14",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
"lodash": "^4.17.21",
"luxon": "^2.0.2",
"qs": "^6.10.1",
"react-use": "^17.2.4"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0"
},
"devDependencies": {
"@backstage/cli": "^0.10.5",
"@backstage/core-app-api": "^0.3.1",
"@backstage/dev-utils": "^0.2.16",
"@backstage/test-utils": "^0.2.1",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^13.1.8",
"@types/jest": "^26.0.7",
"@types/lodash": "^4.14.173",
"@types/luxon": "^2.0.4",
"@types/node": "^14.14.32",
"cross-fetch": "^3.0.6",
"msw": "^0.35.0"
},
"files": [
"dist",
"config.d.ts"
],
"configSchema": "config.d.ts"
}
+43
View File
@@ -0,0 +1,43 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { GoCdApi } from './gocdApi';
import { GoCdApiError, PipelineHistory } from './gocdApi.model';
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { ResponseError } from '@backstage/errors';
export class GoCdClientApi implements GoCdApi {
constructor(private readonly discoveryApi: DiscoveryApi) {}
async getPipelineHistory(
pipelineName: string,
): Promise<PipelineHistory | GoCdApiError> {
const baseUrl = await this.discoveryApi.getBaseUrl('proxy');
const pipelineHistoryResponse = await fetch(
`${baseUrl}/gocd/pipelines/${pipelineName}/history`,
{
headers: {
Accept: 'application/vnd.go.cd+json',
},
},
);
if (!pipelineHistoryResponse.ok) {
throw await ResponseError.fromResponse(pipelineHistoryResponse);
}
return await pipelineHistoryResponse.json();
}
}
+119
View File
@@ -0,0 +1,119 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 interface GoCdApiError {
message: string;
}
export interface PipelineHistory {
_links: Links;
pipelines: Pipeline[];
}
export interface Links {
next: Next;
}
export interface Next {
href: string;
}
export interface Pipeline {
name: string;
counter: number;
label: string;
natural_order?: number;
can_run?: boolean;
preparing_to_schedule?: boolean;
comment: string | null;
scheduled_date?: number;
build_cause?: BuildCause;
stages: Stage[];
}
export interface BuildCause {
trigger_message: string;
trigger_forced: boolean;
approver: string;
material_revisions: MaterialRevision[];
}
export interface MaterialRevision {
changed: boolean;
material: Material;
modifications: Modification[];
}
export interface Material {
name: string;
fingerprint: string;
type: string;
description: string;
}
export interface Modification {
revision: string;
modified_time: number;
user_name: string;
comment: string | null;
email_address: string | null;
}
export interface Stage {
result?: string;
status: string;
rerun_of_counter?: number | null;
name: string;
counter: string;
scheduled: boolean;
approval_type?: string | null;
approved_by?: string | null;
operate_permission?: boolean;
can_run?: boolean;
jobs: Job[];
}
export interface Job {
name: string;
scheduled_date?: number;
state: string;
result: string;
}
export enum GoCdBuildResultStatus {
running,
successful,
warning,
aborted,
error,
pending,
}
export interface GoCdBuildStageResult {
status: GoCdBuildResultStatus;
text: string;
}
export interface GoCdBuildResult {
id: number;
source: string;
stages: GoCdBuildStageResult[];
buildSlug: string;
message: string;
pipeline: string;
author: string | undefined;
commitHash: string;
triggerTime: number | undefined;
}
+22
View File
@@ -0,0 +1,22 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { GoCdApiError, PipelineHistory } from './gocdApi.model';
export interface GoCdApi {
getPipelineHistory(
pipelineName: string,
): Promise<PipelineHistory | GoCdApiError>;
}
@@ -0,0 +1,60 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { ConfigReader } from '@backstage/core-app-api';
import { ConfigApi, configApiRef } from '@backstage/core-plugin-api';
import { EntityProvider } from '@backstage/plugin-catalog-react';
import { renderWithEffects, TestApiProvider } from '@backstage/test-utils';
import { GoCdBuildsComponent } from './GoCdBuildsComponent';
import { gocdApiRef } from '../../plugin';
import { GoCdApi } from '../../api/gocdApi';
const mockApiClient: GoCdApi = {
getPipelineHistory: jest.fn(async () => ({
_links: { next: { href: 'some-href' } },
pipelines: [],
})),
};
describe('GoCdArtifactsComponent', () => {
const configApi: ConfigApi = new ConfigReader({
gocd: {
baseUrl: 'gocd.baseurl.com',
},
});
const entityValue = { entity: { metadata: {} } as Entity };
const renderComponent = () =>
renderWithEffects(
<TestApiProvider
apis={[
[gocdApiRef, mockApiClient],
[configApiRef, configApi],
]}
>
<EntityProvider entity={entityValue.entity}>
<GoCdBuildsComponent />
</EntityProvider>
</TestApiProvider>,
);
it('should display an empty state if an app annotation is missing', async () => {
const rendered = await renderComponent();
expect(await rendered.findByText('Missing Annotation')).toBeInTheDocument();
});
});
@@ -0,0 +1,132 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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, { useState } from 'react';
import { Entity } from '@backstage/catalog-model';
import { useEntity } from '@backstage/plugin-catalog-react';
import {
Content,
ContentHeader,
MissingAnnotationEmptyState,
EmptyState,
Page,
} from '@backstage/core-components';
import { useApi, configApiRef } from '@backstage/core-plugin-api';
import { useAsync } from 'react-use';
import { gocdApiRef } from '../../plugin';
import { GoCdBuildsTable } from '../GoCdBuildsTable/GoCdBuildsTable';
import { GoCdApiError, PipelineHistory } from '../../api/gocdApi.model';
import { Item, Select } from '../Select';
/**
* Constant storing GoCD pipelines annotation.
*
* @public
*/
export const GOCD_PIPELINES_ANNOTATION = 'gocd.org/pipelines';
/**
* Returns true if GoCD annotation is present in the given entity.
*
* @public
*/
export const isGoCdAvailable = (entity: Entity): boolean =>
Boolean(entity.metadata.annotations?.[GOCD_PIPELINES_ANNOTATION]);
export const GoCdBuildsComponent = (): JSX.Element => {
const { entity } = useEntity();
const config = useApi(configApiRef);
const rawPipelines: string[] | undefined = (
entity.metadata.annotations?.[GOCD_PIPELINES_ANNOTATION] as
| string
| undefined
)
?.split(',')
.map(p => p.trim());
const gocdApi = useApi(gocdApiRef);
const [selectedPipeline, setSelectedPipeline] = useState<string>(
rawPipelines ? rawPipelines[0] : '',
);
const {
value: pipelineHistory,
loading,
error,
} = useAsync(async (): Promise<PipelineHistory | GoCdApiError> => {
return await gocdApi.getPipelineHistory(selectedPipeline);
}, [selectedPipeline]);
const onSelectedPipelineChanged = (pipeline: string) => {
setSelectedPipeline(pipeline);
};
const getSelectionItems = (pipelines: string[]): Item[] => {
return pipelines.map(p => ({ label: p, value: p }));
};
function isError(
apiResult: PipelineHistory | GoCdApiError | undefined,
): apiResult is GoCdApiError {
return (apiResult as GoCdApiError)?.message !== undefined;
}
if (!rawPipelines) {
return (
<MissingAnnotationEmptyState annotation={GOCD_PIPELINES_ANNOTATION} />
);
}
if (isError(pipelineHistory)) {
return (
<EmptyState
title="GoCD pipelines"
description={`Could not fetch pipelines defined for entity ${entity.metadata.name}. Error: ${pipelineHistory.message}`}
missing="content"
/>
);
}
if (!loading && !pipelineHistory) {
return (
<EmptyState
title="GoCD pipelines"
description={`We could not find pipelines defined for entity ${entity.metadata.name}.`}
missing="data"
/>
);
}
return (
<Page themeId="tool">
<Content noPadding>
<ContentHeader title={entity.metadata.name}>
<Select
value={selectedPipeline}
onChange={pipeline => onSelectedPipelineChanged(pipeline)}
label="Pipeline"
items={getSelectionItems(rawPipelines)}
/>
</ContentHeader>
<GoCdBuildsTable
goCdBaseUrl={config.getString('gocd.baseUrl')}
pipelineHistory={pipelineHistory}
loading={loading}
error={error}
/>
</Content>
</Page>
);
};
@@ -0,0 +1,20 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 {
GoCdBuildsComponent,
isGoCdAvailable,
GOCD_PIPELINES_ANNOTATION,
} from './GoCdBuildsComponent';
@@ -0,0 +1,264 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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, { useState } from 'react';
import { Entity, getEntitySourceLocation } from '@backstage/catalog-model';
import { useEntity } from '@backstage/plugin-catalog-react';
import { Alert } from '@material-ui/lab';
import { Button } from '@material-ui/core';
import GitHubIcon from '@material-ui/icons/GitHub';
import {
GoCdBuildResult,
GoCdBuildResultStatus,
PipelineHistory,
Pipeline,
} from '../../api/gocdApi.model';
import { DateTime } from 'luxon';
import {
Table,
TableColumn,
Link,
SubvalueCell,
StatusOK,
StatusWarning,
StatusAborted,
StatusError,
StatusRunning,
StatusPending,
} from '@backstage/core-components';
type GoCdBuildsProps = {
goCdBaseUrl: string;
pipelineHistory: PipelineHistory | undefined;
loading: boolean;
error: Error | undefined;
};
const renderTrigger = (build: GoCdBuildResult): React.ReactNode => {
const subvalue = (
<>
{build.pipeline}
<br />
{build.author}
</>
);
return <SubvalueCell value={build.message} subvalue={subvalue} />;
};
const renderStages = (build: GoCdBuildResult): React.ReactNode => {
return build.stages.map(s => {
switch (s.status) {
case GoCdBuildResultStatus.successful: {
return (
<>
<StatusOK>{s.text}</StatusOK>
<br />
<br />
</>
);
}
case GoCdBuildResultStatus.error: {
return (
<>
<StatusError>{s.text}</StatusError>
<br />
<br />
</>
);
}
case GoCdBuildResultStatus.running: {
return (
<>
<StatusRunning>{s.text}</StatusRunning>
<br />
<br />
</>
);
}
case GoCdBuildResultStatus.aborted: {
return (
<>
<StatusAborted>{s.text}</StatusAborted>
<br />
<br />
</>
);
}
case GoCdBuildResultStatus.pending: {
return (
<>
<StatusPending>{s.text}</StatusPending>
<br />
<br />
</>
);
}
default: {
return (
<>
<StatusWarning>{s.text}</StatusWarning>
<br />
<br />
</>
);
}
}
});
};
const renderSource = (build: GoCdBuildResult): React.ReactNode => {
return (
<Button
href={build.source}
target="_blank"
rel="noreferrer"
startIcon={<GitHubIcon />}
>
{build.commitHash}
</Button>
);
};
const renderId = (
goCdBaseUrl: string,
build: GoCdBuildResult,
): React.ReactNode => {
const goCdBuildUrl = `${goCdBaseUrl}/go/pipelines/value_stream_map/${build.buildSlug}`;
return (
<SubvalueCell
value={
<Link to={goCdBuildUrl} target="_blank" rel="noreferrer">
{build.id}
</Link>
}
subvalue={
build.triggerTime &&
DateTime.fromMillis(build.triggerTime).toLocaleString(
DateTime.DATETIME_MED,
)
}
/>
);
};
const renderError = (error: Error): JSX.Element => {
return <Alert severity="error">{error.message}</Alert>;
};
const toStageStatus = (status: string): GoCdBuildResultStatus => {
switch (status.toLocaleLowerCase('en-US')) {
case 'passed':
return GoCdBuildResultStatus.successful;
case 'failed':
return GoCdBuildResultStatus.error;
case 'aborted':
return GoCdBuildResultStatus.aborted;
case 'building':
return GoCdBuildResultStatus.running;
case 'pending':
return GoCdBuildResultStatus.pending;
default:
return GoCdBuildResultStatus.aborted;
}
};
const toBuildResults = (
entity: Entity,
builds: Pipeline[],
): GoCdBuildResult[] | undefined => {
// TODO(julioz): What if not git 'type'?
const entitySourceLocation =
getEntitySourceLocation(entity).target.split('/tree')[0];
return builds.map(build => ({
id: build.counter,
source: `${entitySourceLocation}/commit/${build.build_cause?.material_revisions[0]?.modifications[0].revision}`,
stages: build.stages.map(s => ({
status: toStageStatus(s.status),
text: s.name,
})),
buildSlug: `${build.name}/${build.counter}`,
message:
build.build_cause?.material_revisions[0]?.modifications[0].comment ?? '',
pipeline: build.name,
author:
build.build_cause?.material_revisions[0]?.modifications[0].user_name,
commitHash: build.label,
triggerTime: build.scheduled_date,
}));
};
export const GoCdBuildsTable = (props: GoCdBuildsProps): JSX.Element => {
const { pipelineHistory, loading, error } = props;
const { entity } = useEntity();
const [, setSelectedSearchTerm] = useState<string>('');
const columns: TableColumn<GoCdBuildResult>[] = [
{
title: 'ID',
field: 'id',
highlight: true,
render: build => renderId(props.goCdBaseUrl, build),
},
{
title: 'Trigger',
customFilterAndSearch: (query, row: GoCdBuildResult) =>
`${row.message} ${row.pipeline} ${row.author}`
.toLocaleUpperCase('en-US')
.includes(query.toLocaleUpperCase('en-US')),
field: 'message',
highlight: true,
render: build => renderTrigger(build),
},
{
title: 'Stages',
field: 'status',
render: build => renderStages(build),
},
{
title: 'Source',
field: 'source',
render: build => renderSource(build),
},
];
return (
<>
{error && renderError(error)}
{!error && (
<Table
title="GoCD builds"
isLoading={loading}
options={{
search: true,
searchAutoFocus: true,
debounceInterval: 800,
paging: false,
showFirstLastPageButtons: false,
pageSize: 10,
}}
columns={columns}
data={
pipelineHistory
? toBuildResults(entity, pipelineHistory.pipelines) || []
: []
}
onSearchChange={(searchTerm: string) =>
setSelectedSearchTerm(searchTerm)
}
/>
)}
</>
);
};
@@ -0,0 +1,82 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 FormControl from '@material-ui/core/FormControl';
import InputLabel from '@material-ui/core/InputLabel';
import MenuItem from '@material-ui/core/MenuItem';
import Select from '@material-ui/core/Select';
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles';
import React from 'react';
export type Item = {
label: string;
value: string | number;
};
const useStyles = makeStyles((theme: Theme) =>
createStyles({
formControl: {
margin: theme.spacing(1),
minWidth: 120,
},
selectEmpty: {
marginTop: theme.spacing(2),
},
}),
);
type SelectComponentProps = {
value: string;
items: Item[];
label: string;
onChange: (value: string) => void;
};
const renderItems = (items: Item[]) => {
return items.map(item => {
return (
<MenuItem value={item.value} key={item.label}>
{item.label}
</MenuItem>
);
});
};
export const SelectComponent = ({
value,
items,
label,
onChange,
}: SelectComponentProps): JSX.Element => {
const classes = useStyles();
const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => {
const val = event.target.value as string;
onChange(val);
};
return (
<FormControl
variant="outlined"
className={classes.formControl}
disabled={items.length === 0}
>
<InputLabel>{label}</InputLabel>
<Select label={label} value={value} onChange={handleChange}>
{renderItems(items)}
</Select>
</FormControl>
);
};
@@ -0,0 +1,17 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { SelectComponent as Select } from './Select';
export type { Item } from './Select';
+34
View File
@@ -0,0 +1,34 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { gocdPlugin } from './plugin';
import { createComponentExtension } from '@backstage/core-plugin-api';
/**
* GoCD builds table component.
*
* @public
*/
export const EntityGoCdContent = gocdPlugin.provide(
createComponentExtension({
name: 'EntityGoCdContent',
component: {
lazy: () =>
import('./components/GoCdBuildsComponent').then(
m => m.GoCdBuildsComponent,
),
},
}),
);
+21
View File
@@ -0,0 +1,21 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { gocdPlugin } from './plugin';
export { EntityGoCdContent } from './extensions';
export {
GOCD_PIPELINES_ANNOTATION,
isGoCdAvailable,
} from './components/GoCdBuildsComponent';
+22
View File
@@ -0,0 +1,22 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { gocdPlugin } from './plugin';
describe('gocd', () => {
it('should export plugin', () => {
expect(gocdPlugin).toBeDefined();
});
});
+45
View File
@@ -0,0 +1,45 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { GoCdClientApi } from './api/gocdApi.client';
import { GoCdApi } from './api/gocdApi';
import {
discoveryApiRef,
createApiRef,
createApiFactory,
createPlugin,
} from '@backstage/core-plugin-api';
export const gocdApiRef = createApiRef<GoCdApi>({
id: 'plugin.gocd.service',
});
/**
* Plugin definition.
*
* @public
*/
export const gocdPlugin = createPlugin({
id: 'gocd',
apis: [
createApiFactory({
api: gocdApiRef,
deps: { discoveryApi: discoveryApiRef },
factory: ({ discoveryApi }) => {
return new GoCdClientApi(discoveryApi);
},
}),
],
});
+20
View File
@@ -0,0 +1,20 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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 { createRouteRef } from '@backstage/core-plugin-api';
export const rootRouteRef = createRouteRef({
id: 'gocd',
});
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright 2021 The Backstage Authors
*
* 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';
import 'cross-fetch/polyfill';