github-deployments plugin

Signed-off-by: Andrew Johnson <ajohnson@gocardless.com>
This commit is contained in:
Andrew Johnson
2021-03-25 16:01:10 +00:00
parent f7a796e8aa
commit ec023e8286
17 changed files with 688 additions and 0 deletions
@@ -0,0 +1,89 @@
/*
* Copyright 2021 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, DiscoveryApi } from '@backstage/core';
import { graphql } from '@octokit/graphql';
export type GithubDeployment = {
environment: string;
state: string;
updatedAt: string;
commit: {
abbreviatedOid: string;
commitUrl: string;
};
};
export interface GithubDeploymentsApi {
listDeployments(options: {
owner: string;
repo: string;
last: number;
}): Promise<GithubDeployment[]>;
}
export const githubDeploymentsApiRef = createApiRef<GithubDeploymentsApi>({
id: 'plugin.github-deployments.service',
description: 'Used by the Github Deployments plugin to make requests',
});
export type Options = {
discoveryApi: DiscoveryApi;
proxyPath?: string;
};
const deploymentsQuery = `
query lastDeployments($owner: String!, $repo: String!, $last: Int) {
repository(owner: $owner, name: $repo) {
deployments(last: $last) {
nodes {
state
environment
updatedAt
commit {
abbreviatedOid
commitUrl
}
}
}
}
}
`;
export class GithubDeploymentsApiClient implements GithubDeploymentsApi {
private readonly discoveryApi: DiscoveryApi;
constructor(options: Options) {
this.discoveryApi = options.discoveryApi;
}
private async getProxyUrl() {
return await this.discoveryApi.getBaseUrl('proxy');
}
async listDeployments(options: {
owner: string;
repo: string;
last: number;
}): Promise<GithubDeployment[]> {
const proxyUrl = await this.getProxyUrl();
const graphQlWithBaseURL = graphql.defaults({
baseUrl: `${proxyUrl}/github/api`,
});
const response: any = await graphQlWithBaseURL(deploymentsQuery, options);
return response.repository?.deployments?.nodes?.reverse() || [];
}
}
@@ -0,0 +1,85 @@
/*
* Copyright 2021 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 {
ApiProvider,
ApiRegistry,
errorApiRef,
UrlPatternDiscovery,
configApiRef,
ConfigReader,
} from '@backstage/core';
import { render } from '@testing-library/react';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { GithubDeploymentsApiClient, githubDeploymentsApiRef } from '../api';
import { githubDeploymentsPlugin } from '../plugin';
import { GithubDeploymentsCard } from './GithubDeploymentsCard';
import { entityStub, responseStub } from '../mocks/mocks';
const discoveryApi = UrlPatternDiscovery.compile('http://exampleapi.com');
const errorApiMock = { post: jest.fn(), error$: jest.fn() };
const apis = ApiRegistry.from([
[configApiRef, new ConfigReader({})],
[errorApiRef, errorApiMock],
[githubDeploymentsApiRef, new GithubDeploymentsApiClient({ discoveryApi })],
]);
describe('github-deployments', () => {
const worker = setupServer();
beforeAll(() => worker.listen());
afterAll(() => worker.close());
afterEach(() => worker.resetHandlers());
beforeEach(() => {
jest.resetAllMocks();
});
describe('export-plugin', () => {
it('should export plugin', () => {
expect(githubDeploymentsPlugin).toBeDefined();
});
});
describe('GithubDeploymentsCard', () => {
it('should display fetched data', async () => {
worker.use(rest.post('*', (_, res, ctx) => res(ctx.json(responseStub))));
const rendered = render(
<ApiProvider apis={apis}>
<GithubDeploymentsCard entity={entityStub} />
</ApiProvider>,
);
expect(await rendered.findByText('active')).toBeInTheDocument();
expect(await rendered.findByText('prd')).toBeInTheDocument();
expect(await rendered.findByText('12345')).toHaveAttribute(
'href',
'https://exampleapi.com/123456789',
);
expect(await rendered.findByText('pending')).toBeInTheDocument();
expect(await rendered.findByText('lab')).toBeInTheDocument();
expect(await rendered.findByText('54321')).toHaveAttribute(
'href',
'https://exampleapi.com/543212345',
);
});
});
});
@@ -0,0 +1,75 @@
/*
* Copyright 2021 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 { LinearProgress } from '@material-ui/core';
import { Entity } from '@backstage/catalog-model';
import { InfoCard, MissingAnnotationEmptyState, useApi } from '@backstage/core';
import { useAsync } from 'react-use';
import { githubDeploymentsApiRef } from '../api';
import GithubDeploymentsTable from './GithubDeploymentsTable/GithubDeploymentsTable';
export const GITHUB_PROJECT_SLUG_ANNOTATION = 'github.com/project-slug';
export const isGithubDeploymentsAvailable = (entity: Entity) =>
Boolean(entity?.metadata.annotations?.[GITHUB_PROJECT_SLUG_ANNOTATION]);
const GithubDeploymentsComponent = ({
entity,
last,
}: {
entity: Entity;
last: number;
}) => {
const api = useApi(githubDeploymentsApiRef);
const annotation =
entity.metadata.annotations?.[GITHUB_PROJECT_SLUG_ANNOTATION] ?? '';
const [owner, repo] = annotation.split('/');
const { loading, value, error } = useAsync(
async () => await api.listDeployments({ owner, repo, last }),
);
if (loading) {
return (
<InfoCard title="Github Deployments">
<LinearProgress />
</InfoCard>
);
}
if (error) {
return (
<InfoCard title="Github Deployments">
Error occurred while fetching data.
</InfoCard>
);
}
return <GithubDeploymentsTable deployments={value || []} />;
};
export const GithubDeploymentsCard = ({
entity,
last,
}: {
entity: Entity;
last?: number;
}) => {
return !isGithubDeploymentsAvailable(entity) ? (
<MissingAnnotationEmptyState annotation={GITHUB_PROJECT_SLUG_ANNOTATION} />
) : (
<GithubDeploymentsComponent entity={entity} last={last || 10} />
);
};
@@ -0,0 +1,90 @@
/*
* Copyright 2021 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 { Table, TableColumn } from '@backstage/core';
import { GithubDeployment } from '../../api';
import moment from 'moment';
import { Box, Typography, Link } from '@material-ui/core';
const lastUpdated = (start: string): string => moment(start).fromNow();
const State = ({ value }: { value: string }) => {
const colorMap: Record<string, string> = {
PENDING: 'orange',
IN_PROGRESS: 'orange',
ACTIVE: 'green',
};
return (
<Box display="flex" alignItems="center">
<span
style={{
display: 'block',
width: '8px',
height: '8px',
borderRadius: '50%',
backgroundColor: colorMap[value] || 'grey',
marginRight: '5px',
}}
/>
<Typography variant="caption">{value}</Typography>
</Box>
);
};
const columns: TableColumn[] = [
{
title: 'Environment',
field: 'environment',
highlight: true,
},
{
title: 'Status',
field: 'environment',
render: (row: any): React.ReactNode => <State value={row.state} />,
},
{
title: 'Commit',
render: (row: any): React.ReactNode => (
<Link href={row.commit.commitUrl} target="_blank" rel="noopener">
{row.commit.abbreviatedOid}
</Link>
),
},
{
title: 'Last Updated',
render: (row: any): React.ReactNode => lastUpdated(row.updatedAt),
},
];
type GithubDeploymentsTableProps = {
deployments: GithubDeployment[];
};
const GithubDeploymentsTable = ({
deployments,
}: GithubDeploymentsTableProps) => {
return (
<Table
columns={columns}
options={{ padding: 'dense', paging: true, search: false, pageSize: 5 }}
title="Github Deployments"
data={deployments}
/>
);
};
export default GithubDeploymentsTable;
+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright 2021 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 {
githubDeploymentsPlugin as plugin,
EntityGithubDeploymentsCard,
} from './plugin';
@@ -0,0 +1,67 @@
/*
* Copyright 2021 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 const entityStub = {
metadata: {
namespace: 'default',
annotations: {
'github.com/project-slug': 'org/repo',
},
name: 'sample-service',
description: 'Sample service',
uid: 'g0h33dd9-56h7-835b-b63v-7x5da3j64851',
generation: 1,
},
apiVersion: 'backstage.io/v1alpha1',
kind: 'Component',
spec: {
type: 'service',
lifecycle: 'experimental',
},
relations: [],
};
export const responseStub = {
data: {
repository: {
deployments: {
nodes: [
{
state: 'active',
environment: 'prd',
updatedAt: '2021-03-25T12:08:45Z',
commit: {
commitUrl: 'https://exampleapi.com/123456789',
abbreviatedOid: '12345',
},
},
{
state: 'pending',
environment: 'lab',
updatedAt: '2021-03-25T12:08:47Z',
commit: {
commitUrl: 'https://exampleapi.com/543212345',
abbreviatedOid: '54321',
},
},
],
},
},
},
};
export const noDataResponseStub = {
data: {},
};
@@ -0,0 +1,22 @@
/*
* Copyright 2021 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 { githubDeploymentsPlugin } from './plugin';
describe('graphiql', () => {
it('should export plugin', () => {
expect(githubDeploymentsPlugin).toBeDefined();
});
});
+45
View File
@@ -0,0 +1,45 @@
/*
* Copyright 2021 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 {
createApiFactory,
createComponentExtension,
createPlugin,
discoveryApiRef,
} from '@backstage/core';
import { githubDeploymentsApiRef, GithubDeploymentsApiClient } from './api';
export const githubDeploymentsPlugin = createPlugin({
id: 'github-deployments',
apis: [
createApiFactory({
api: githubDeploymentsApiRef,
deps: { discoveryApi: discoveryApiRef },
factory: ({ discoveryApi }) =>
new GithubDeploymentsApiClient({ discoveryApi }),
}),
],
});
export const EntityGithubDeploymentsCard = githubDeploymentsPlugin.provide(
createComponentExtension({
component: {
lazy: () =>
import('./components/GithubDeploymentsCard').then(
m => m.GithubDeploymentsCard,
),
},
}),
);
@@ -0,0 +1,17 @@
/*
* Copyright 2021 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';
import 'cross-fetch/polyfill';