Adding Azure DevOps frontend plugin

Signed-off-by: Andre Wanlin <awanlin@rapidrtc.com>
This commit is contained in:
Andre Wanlin
2021-10-06 14:32:58 -05:00
parent a9564f20a9
commit 38b014a5f5
27 changed files with 886 additions and 0 deletions
@@ -0,0 +1,32 @@
/*
* 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 { RepoBuild } from './types';
import { createApiRef } from '@backstage/core-plugin-api';
export const azureDevOpsApiRef = createApiRef<AzureDevOpsApi>({
id: 'plugin.azure-devops.service',
description:
'Used by the Azure DevOps plugin to make requests to accompanying backend',
});
export interface AzureDevOpsApi {
getRepoBuilds(
projectName: string,
repoName: string,
top: number,
): Promise<RepoBuild[]>;
}
@@ -0,0 +1,56 @@
/*
* 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 { AzureDevOpsApi } from './AzureDevOpsApi';
import { RepoBuild } from './types';
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
export class AzureDevOpsClient implements AzureDevOpsApi {
private readonly discoveryApi: DiscoveryApi;
private readonly identityApi: IdentityApi;
constructor(options: {
discoveryApi: DiscoveryApi;
identityApi: IdentityApi;
}) {
this.discoveryApi = options.discoveryApi;
this.identityApi = options.identityApi;
}
async getRepoBuilds(
projectName: string,
repoName: string,
top: number,
): Promise<RepoBuild[]> {
return await this.get(`/repo-builds/${projectName}/${repoName}?top=${top}`);
}
private async get(path: string): Promise<any> {
const url = `${await this.discoveryApi.getBaseUrl('azure-devops')}${path}`;
const idToken = await this.identityApi.getIdToken();
const response = await fetch(url, {
headers: idToken ? { Authorization: `Bearer ${idToken}` } : {},
});
if (!response.ok) {
const payload = await response.text();
const message = `Request failed with ${response.status} ${response.statusText}, ${payload}`;
throw new Error(message);
}
return await response.json();
}
}
+18
View File
@@ -0,0 +1,18 @@
/*
* 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 * from './AzureDevOpsApi';
export * from './AzureDevOpsClient';
+30
View File
@@ -0,0 +1,30 @@
/*
* 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 {
BuildResult,
BuildStatus,
} from 'azure-devops-node-api/interfaces/BuildInterfaces';
export type RepoBuild = {
id?: number;
title: string;
link: string;
status?: BuildStatus;
result?: BuildResult;
queueTime?: Date;
source: string;
};
@@ -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 from 'react';
import moment from 'moment';
import {
Table,
TableColumn,
StatusError,
StatusOK,
StatusWarning,
StatusAborted,
} from '@backstage/core-components';
import { Link, Box, Typography } from '@material-ui/core';
import Alert from '@material-ui/lab/Alert';
import { RepoBuild } from '../../api/types';
import {
BuildResult,
BuildStatus,
} from 'azure-devops-node-api/interfaces/BuildInterfaces';
const getBuildResultComponent = (result: number | undefined = 0) => {
switch (result) {
case 0: // None
return <StatusError />;
case 2: // Succeeded
return <StatusOK />;
case 4: // PartiallySucceeded
return <StatusWarning />;
case 8: // Failed
return <StatusError />;
case 32: // Canceled
return <StatusAborted />;
default:
return <StatusWarning />;
}
};
const columns: TableColumn[] = [
{
title: 'ID',
field: 'id',
highlight: false,
width: '80px',
},
{
title: 'Build',
field: 'title',
render: (row: Partial<RepoBuild>) => (
<Link href={row.link} target="_blank">
{row.title}
</Link>
),
},
{
title: 'Source',
field: 'source',
},
{
title: 'Status',
field: 'status',
render: (row: Partial<RepoBuild>) => (
<Box display="flex" alignItems="center">
<Box mr={1} />
<Typography variant="button">{BuildStatus[row.status || 0]}</Typography>
</Box>
),
},
{
title: 'Result',
field: 'result',
render: (row: Partial<RepoBuild>) => (
<Box display="flex" alignItems="center">
{getBuildResultComponent(row.result)}
<Box mr={1} />
<Typography variant="button">{BuildResult[row.result || 0]}</Typography>
</Box>
),
},
{
title: 'Date',
field: 'queueTime',
render: (row: Partial<RepoBuild>) => moment(row.queueTime).fromNow(),
},
];
type Props = {
items: RepoBuild[];
loading: boolean;
error?: any;
};
export const BuildTable = ({ items, loading, error }: Props) => {
if (error) {
return (
<div>
<Alert severity="error">
Error encountered while fetching Azure DevOps builds.{' '}
{error.toString()}
</Alert>
</div>
);
}
return (
<Table
isLoading={loading}
columns={columns}
options={{
search: true,
paging: true,
pageSize: 5,
showEmptyDataSourceMessage: !loading,
}}
title={`Builds (${(items && items.length) || 0})`}
data={items}
/>
);
};
@@ -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 { BuildTable } from './BuildTable';
@@ -0,0 +1,33 @@
/*
* 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 { Entity } from '@backstage/catalog-model';
import { useEntity } from '@backstage/plugin-catalog-react';
import React from 'react';
import { useRepoBuilds } from '../../hooks/useRepoBuilds';
import { BuildTable } from '../BuildTable/BuildTable';
type Props = {
/** @deprecated The entity is now grabbed from context instead */
entity?: Entity;
};
export const EntityPageAzureDevOps = (_props: Props) => {
const { entity } = useEntity();
const { items, loading, error } = useRepoBuilds(entity);
return <BuildTable items={items || []} loading={loading} error={error} />;
};
@@ -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 { EntityPageAzureDevOps } from './EntityPageAzureDevOps';
@@ -0,0 +1,48 @@
/*
* 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 { Routes, Route } from 'react-router';
import { azureDevOpsRouteRef } from '../routes';
import { EntityPageAzureDevOps } from './EntityPageAzureDevOps';
import { AZURE_DEVOPS_ANNOTATION } from '../constants';
import { Entity } from '@backstage/catalog-model';
import { useEntity } from '@backstage/plugin-catalog-react';
import { MissingAnnotationEmptyState } from '@backstage/core-components';
export const isAzureDevOpsAvailable = (entity: Entity) =>
Boolean(entity.metadata.annotations?.[AZURE_DEVOPS_ANNOTATION]);
type Props = {
/** @deprecated The entity is now grabbed from context instead */
entity?: Entity;
};
export const Router = (_props: Props) => {
const { entity } = useEntity();
if (!isAzureDevOpsAvailable(entity)) {
return <MissingAnnotationEmptyState annotation={AZURE_DEVOPS_ANNOTATION} />;
}
return (
<Routes>
<Route
path={`/${azureDevOpsRouteRef.path}`}
element={<EntityPageAzureDevOps />}
/>
</Routes>
);
};
+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.
*/
export const AZURE_DEVOPS_ANNOTATION = 'dev.azure.com/project-repo';
@@ -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.
*/
import { Entity } from '@backstage/catalog-model';
import { AZURE_DEVOPS_ANNOTATION } from '../constants';
export function useProjectRepoFromEntity(entity: Entity) {
const [project, repo] = (
entity.metadata.annotations?.[AZURE_DEVOPS_ANNOTATION] ?? ''
).split('/');
return { project, repo };
}
@@ -0,0 +1,40 @@
/*
* 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 { useAsync } from 'react-use';
import { Entity } from '@backstage/catalog-model';
import { useApi, configApiRef } from '@backstage/core-plugin-api';
import { azureDevOpsApiRef } from '../api';
import { RepoBuild } from '../api/types';
import { useProjectRepoFromEntity } from './useProjectRepoFromEntity';
const DEFAULT_TOP: number = 10;
export function useRepoBuilds(entity: Entity) {
const config = useApi(configApiRef);
const top = config.getOptionalNumber('azureDevOps.top') ?? DEFAULT_TOP;
const api = useApi(azureDevOpsApiRef);
const { project, repo } = useProjectRepoFromEntity(entity);
const { value, loading, error } = useAsync(() => {
return api.getRepoBuilds(project, repo, top);
}, [api, project, repo, entity]);
return {
items: value as RepoBuild[],
loading,
error,
};
}
+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.
*/
export { azureDevOpsPlugin, EntityAzureDevOpsContent } from './plugin';
export { isAzureDevOpsAvailable } from './components/Router';
+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 { azureDevOpsPlugin } from './plugin';
describe('azure-devops', () => {
it('should export plugin', () => {
expect(azureDevOpsPlugin).toBeDefined();
});
});
+54
View File
@@ -0,0 +1,54 @@
/*
* 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 { azureDevOpsApiRef } from './api/AzureDevOpsApi';
import { AzureDevOpsClient } from './api/AzureDevOpsClient';
import {
createApiFactory,
createPlugin,
createRoutableExtension,
createRouteRef,
discoveryApiRef,
identityApiRef,
} from '@backstage/core-plugin-api';
export const rootRouteRef = createRouteRef({
path: '',
title: 'AzureDevOps',
});
export const azureDevOpsPlugin = createPlugin({
id: 'azureDevOps',
apis: [
createApiFactory({
api: azureDevOpsApiRef,
deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },
factory: ({ discoveryApi, identityApi }) =>
new AzureDevOpsClient({ discoveryApi, identityApi }),
}),
],
routes: {
entityContent: rootRouteRef,
},
});
export const EntityAzureDevOpsContent = azureDevOpsPlugin.provide(
createRoutableExtension({
name: 'EntityAzureDevOpsContent',
component: () => import('./components/Router').then(m => m.Router),
mountPoint: rootRouteRef,
}),
);
+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 azureDevOpsRouteRef = createRouteRef({
title: 'azure-devops',
});
+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';