@@ -32,7 +32,7 @@ Configuration Details:
|
||||
|
||||
Here's how to get the backend up and running:
|
||||
|
||||
1. First we need to add the @backstage/plugin-azure-devops-backend package to your backend:
|
||||
1. First we need to add the `@backstage/plugin-azure-devops-backend` package to your backend:
|
||||
|
||||
```sh
|
||||
# From the Backstage root directory
|
||||
|
||||
@@ -22,9 +22,7 @@ export const azureDevOpsPlugin: BackstagePlugin<
|
||||
// Warning: (ae-missing-release-tag) "EntityAzurePipelinesContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
// @public (undocumented)
|
||||
export const EntityAzurePipelinesContent: (_props: {
|
||||
entity?: Entity | undefined;
|
||||
}) => JSX.Element;
|
||||
export const EntityAzurePipelinesContent: () => JSX.Element;
|
||||
|
||||
// Warning: (ae-missing-release-tag) "isAzureDevOpsAvailable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
|
||||
//
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"main": "src/index.ts",
|
||||
"types": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
"private": false,
|
||||
"publishConfig": {
|
||||
"access": "public",
|
||||
"main": "dist/index.esm.js",
|
||||
@@ -29,6 +30,7 @@
|
||||
"@backstage/catalog-model": "^0.9.3",
|
||||
"@backstage/core-components": "^0.6.1",
|
||||
"@backstage/core-plugin-api": "^0.1.10",
|
||||
"@backstage/errors": "^0.1.2",
|
||||
"@backstage/plugin-catalog-react": "^0.5.1",
|
||||
"@backstage/theme": "^0.2.10",
|
||||
"@material-ui/core": "^4.12.2",
|
||||
@@ -51,8 +53,8 @@
|
||||
"@testing-library/user-event": "^13.1.8",
|
||||
"@types/jest": "^26.0.7",
|
||||
"@types/node": "^14.14.32",
|
||||
"msw": "^0.29.0",
|
||||
"cross-fetch": "^3.0.6"
|
||||
"cross-fetch": "^3.0.6",
|
||||
"msw": "^0.29.0"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RepoBuild } from './types';
|
||||
import { RepoBuild, RepoBuildOptions } from './types';
|
||||
import { createApiRef } from '@backstage/core-plugin-api';
|
||||
|
||||
export const azureDevOpsApiRef = createApiRef<AzureDevOpsApi>({
|
||||
@@ -27,6 +27,6 @@ export interface AzureDevOpsApi {
|
||||
getRepoBuilds(
|
||||
projectName: string,
|
||||
repoName: string,
|
||||
top: number,
|
||||
): Promise<RepoBuild[]>;
|
||||
options?: RepoBuildOptions,
|
||||
): Promise<{ items: RepoBuild[] }>;
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
*/
|
||||
|
||||
import { AzureDevOpsApi } from './AzureDevOpsApi';
|
||||
import { RepoBuild } from './types';
|
||||
import { RepoBuild, RepoBuildOptions } from './types';
|
||||
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
|
||||
import { AzureDevOpsClientError } from './AzureDevOpsClientError';
|
||||
import { ResponseError } from '@backstage/errors';
|
||||
|
||||
export class AzureDevOpsClient implements AzureDevOpsApi {
|
||||
private readonly discoveryApi: DiscoveryApi;
|
||||
@@ -34,12 +34,14 @@ export class AzureDevOpsClient implements AzureDevOpsApi {
|
||||
getRepoBuilds(
|
||||
projectName: string,
|
||||
repoName: string,
|
||||
top: number,
|
||||
): Promise<RepoBuild[]> {
|
||||
return this.get(`repo-builds/${projectName}/${repoName}?top=${top}`);
|
||||
options?: RepoBuildOptions,
|
||||
): Promise<{ items: RepoBuild[] }> {
|
||||
return this.get(
|
||||
`repo-builds/${projectName}/${repoName}?top=${options?.top}`,
|
||||
);
|
||||
}
|
||||
|
||||
private async get(path: string): Promise<any> {
|
||||
private async get(path: string): Promise<{ items: any }> {
|
||||
const baseUrl = `${await this.discoveryApi.getBaseUrl('azure-devops')}/`;
|
||||
const url = new URL(path, baseUrl);
|
||||
|
||||
@@ -49,10 +51,9 @@ export class AzureDevOpsClient implements AzureDevOpsApi {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const payload = await response.text();
|
||||
throw new AzureDevOpsClientError(response, payload);
|
||||
throw await ResponseError.fromResponse(response);
|
||||
}
|
||||
|
||||
return response.json() as Promise<any>;
|
||||
return response.json() as Promise<{ items: any }>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
/*
|
||||
* 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 class AzureDevOpsClientError extends Error {
|
||||
public constructor({ status, statusText }: Response, payload: string) {
|
||||
super(`Request failed with ${status} ${statusText}, ${payload}`);
|
||||
}
|
||||
}
|
||||
@@ -28,3 +28,7 @@ export type RepoBuild = {
|
||||
queueTime?: Date;
|
||||
source: string;
|
||||
};
|
||||
|
||||
export type RepoBuildOptions = {
|
||||
top?: number;
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import React from 'react';
|
||||
import { DateTime } from 'luxon';
|
||||
import {
|
||||
Link,
|
||||
Table,
|
||||
TableColumn,
|
||||
StatusError,
|
||||
@@ -25,9 +26,9 @@ import {
|
||||
StatusAborted,
|
||||
StatusRunning,
|
||||
StatusPending,
|
||||
ResponseErrorPanel,
|
||||
} from '@backstage/core-components';
|
||||
import { Link, Box, Typography } from '@material-ui/core';
|
||||
import Alert from '@material-ui/lab/Alert';
|
||||
import { Box, Typography } from '@material-ui/core';
|
||||
import { RepoBuild } from '../../api/types';
|
||||
import {
|
||||
BuildResult,
|
||||
@@ -39,25 +40,25 @@ const getBuildResultComponent = (result: number | undefined) => {
|
||||
case BuildResult.Succeeded:
|
||||
return (
|
||||
<span>
|
||||
<StatusOK /> Succeeded{' '}
|
||||
<StatusOK /> Succeeded
|
||||
</span>
|
||||
);
|
||||
case BuildResult.PartiallySucceeded:
|
||||
return (
|
||||
<span>
|
||||
<StatusWarning /> Partially Succeeded{' '}
|
||||
<StatusWarning /> Partially Succeeded
|
||||
</span>
|
||||
);
|
||||
case BuildResult.Failed:
|
||||
return (
|
||||
<span>
|
||||
<StatusError /> Failed{' '}
|
||||
<StatusError /> Failed
|
||||
</span>
|
||||
);
|
||||
case BuildResult.Canceled:
|
||||
return (
|
||||
<span>
|
||||
<StatusAborted /> Canceled{' '}
|
||||
<StatusAborted /> Canceled
|
||||
</span>
|
||||
);
|
||||
case BuildResult.None:
|
||||
@@ -123,9 +124,7 @@ const columns: TableColumn[] = [
|
||||
field: 'title',
|
||||
width: 'auto',
|
||||
render: (row: Partial<RepoBuild>) => (
|
||||
<Link href={row.link} target="_blank">
|
||||
{row.title}
|
||||
</Link>
|
||||
<Link to={row.link || ''}>{row.title}</Link>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -165,10 +164,7 @@ export const BuildTable = ({ items, loading, error }: Props) => {
|
||||
if (error) {
|
||||
return (
|
||||
<div>
|
||||
<Alert severity="error">
|
||||
Error encountered while fetching Azure DevOps builds.{' '}
|
||||
{error.toString()}
|
||||
</Alert>
|
||||
<ResponseErrorPanel error={error} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-7
@@ -14,18 +14,12 @@
|
||||
* 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 EntityPageAzurePipelines = (_props: Props) => {
|
||||
export const EntityPageAzurePipelines = () => {
|
||||
const { entity } = useEntity();
|
||||
const { items, loading, error } = useRepoBuilds(entity);
|
||||
|
||||
|
||||
@@ -25,12 +25,7 @@ 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) => {
|
||||
export const Router = () => {
|
||||
const { entity } = useEntity();
|
||||
|
||||
if (!isAzureDevOpsAvailable(entity)) {
|
||||
|
||||
@@ -18,7 +18,7 @@ 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 { RepoBuild, RepoBuildOptions } from '../api/types';
|
||||
import { useProjectRepoFromEntity } from './useProjectRepoFromEntity';
|
||||
import { AZURE_DEVOPS_DEFAULT_TOP } from '../constants';
|
||||
|
||||
@@ -31,14 +31,17 @@ export function useRepoBuilds(entity: Entity): {
|
||||
const top =
|
||||
config.getOptionalNumber('azureDevOps.azurePipelines.top') ??
|
||||
AZURE_DEVOPS_DEFAULT_TOP;
|
||||
const options: RepoBuildOptions = {
|
||||
top: top,
|
||||
};
|
||||
const api = useApi(azureDevOpsApiRef);
|
||||
const { project, repo } = useProjectRepoFromEntity(entity);
|
||||
const { value, loading, error } = useAsync(() => {
|
||||
return api.getRepoBuilds(project, repo, top);
|
||||
return api.getRepoBuilds(project, repo, options);
|
||||
}, [api, project, repo, entity]);
|
||||
|
||||
return {
|
||||
items: value as RepoBuild[],
|
||||
items: value?.items as RepoBuild[],
|
||||
loading,
|
||||
error,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user