Updated to add definition name support

Signed-off-by: Andre Wanlin <awanlin@rapidrtc.com>
This commit is contained in:
Andre Wanlin
2021-12-31 13:11:46 -06:00
parent 0127060226
commit 0f104ecc4d
15 changed files with 243 additions and 21 deletions
@@ -15,6 +15,8 @@
*/
import {
BuildRun,
BuildRunOptions,
DashboardPullRequest,
PullRequest,
PullRequestOptions,
@@ -49,4 +51,11 @@ export interface AzureDevOpsApi {
getAllTeams(): Promise<Team[]>;
getUserTeamIds(userId: string): Promise<string[]>;
getBuildRuns(
projectName: string,
repoName?: string,
definitionName?: string,
options?: BuildRunOptions,
): Promise<{ items: BuildRun[] }>;
}
@@ -15,6 +15,8 @@
*/
import {
BuildRun,
BuildRunOptions,
DashboardPullRequest,
PullRequest,
PullRequestOptions,
@@ -91,6 +93,29 @@ export class AzureDevOpsClient implements AzureDevOpsApi {
public getUserTeamIds(userId: string): Promise<string[]> {
return this.get<string[]>(`users/${userId}/team-ids`);
}
public async getBuildRuns(
projectName: string,
repoName?: string,
definitionName?: string,
options?: BuildRunOptions,
): Promise<{ items: BuildRun[] }> {
const queryString = new URLSearchParams();
if (repoName) {
queryString.append('repoName', repoName);
}
if (definitionName) {
queryString.append('definitionName', definitionName);
}
if (options?.top) {
queryString.append('top', options.top.toString());
}
const urlSegment = `builds/${encodeURIComponent(
projectName,
)}?${queryString}`;
const items = await this.get<BuildRun[]>(urlSegment);
return { items };
}
private async get<T>(path: string): Promise<T> {
const baseUrl = `${await this.discoveryApi.getBaseUrl('azure-devops')}/`;
@@ -16,8 +16,8 @@
import {
BuildResult,
BuildRun,
BuildStatus,
RepoBuild,
} from '@backstage/plugin-azure-devops-common';
import { BuildTable } from './BuildTable';
@@ -42,8 +42,8 @@ const buildStatuses: Array<[BuildStatus, BuildResult]> = [
[BuildStatus.None, BuildResult.None], // Unknown
];
const generateTestData = (rows = 10): RepoBuild[] => {
const repoBuilds: RepoBuild[] = [];
const generateTestData = (rows = 10): BuildRun[] => {
const buildRuns: BuildRun[] = [];
for (let i = 0; i < rows; i++) {
const [status, result] = buildStatuses[i] ?? [
@@ -51,7 +51,7 @@ const generateTestData = (rows = 10): RepoBuild[] => {
BuildResult.Succeeded,
];
repoBuilds.push({
buildRuns.push({
id: rows - i + 12534,
title: `backstage ci - 1.0.0-preview-${rows - i}`,
status,
@@ -62,7 +62,7 @@ const generateTestData = (rows = 10): RepoBuild[] => {
});
}
return repoBuilds;
return buildRuns;
};
export const Default = () => (
@@ -17,8 +17,8 @@
import { Box, Typography } from '@material-ui/core';
import {
BuildResult,
BuildRun,
BuildStatus,
RepoBuild,
} from '@backstage/plugin-azure-devops-common';
import {
Link,
@@ -126,7 +126,7 @@ const columns: TableColumn[] = [
title: 'Build',
field: 'title',
width: 'auto',
render: (row: Partial<RepoBuild>) => (
render: (row: Partial<BuildRun>) => (
<Link to={row.link || ''}>{row.title}</Link>
),
},
@@ -138,7 +138,7 @@ const columns: TableColumn[] = [
{
title: 'State',
width: 'auto',
render: (row: Partial<RepoBuild>) => (
render: (row: Partial<BuildRun>) => (
<Box display="flex" alignItems="center">
<Typography variant="button">
{getBuildStateComponent(row.status, row.result)}
@@ -150,7 +150,7 @@ const columns: TableColumn[] = [
title: 'Duration',
field: 'queueTime',
width: 'auto',
render: (row: Partial<RepoBuild>) => (
render: (row: Partial<BuildRun>) => (
<Box display="flex" alignItems="center">
<Typography>
{getDurationFromDates(row.startTime, row.finishTime)}
@@ -162,7 +162,7 @@ const columns: TableColumn[] = [
title: 'Age',
field: 'queueTime',
width: 'auto',
render: (row: Partial<RepoBuild>) =>
render: (row: Partial<BuildRun>) =>
(row.queueTime
? DateTime.fromISO(row.queueTime)
: DateTime.now()
@@ -171,7 +171,7 @@ const columns: TableColumn[] = [
];
type BuildTableProps = {
items?: RepoBuild[];
items?: BuildRun[];
loading: boolean;
error?: Error;
};
@@ -14,10 +14,11 @@
* limitations under the License.
*/
import { useEntity } from '@backstage/plugin-catalog-react';
import React from 'react';
import { useRepoBuilds } from '../../hooks/useRepoBuilds';
import { BuildTable } from '../BuildTable/BuildTable';
import React from 'react';
import { useAnnotationFromEntity } from '../../hooks/useAnnotationFromEntity';
import { useBuildRuns } from '../../hooks/useBuildRuns';
import { useEntity } from '@backstage/plugin-catalog-react';
export const EntityPageAzurePipelines = ({
defaultLimit,
@@ -25,7 +26,15 @@ export const EntityPageAzurePipelines = ({
defaultLimit?: number;
}) => {
const { entity } = useEntity();
const { items, loading, error } = useRepoBuilds(entity, defaultLimit);
const { project, repo, definition } = useAnnotationFromEntity(entity);
const { items, loading, error } = useBuildRuns(
project,
defaultLimit,
repo,
definition,
);
return <BuildTable items={items} loading={loading} error={error} />;
};
+3 -1
View File
@@ -14,5 +14,7 @@
* limitations under the License.
*/
export const AZURE_DEVOPS_ANNOTATION = 'dev.azure.com/project-repo';
export const AZURE_DEVOPS_REPO_ANNOTATION = 'dev.azure.com/project-repo';
export const AZURE_DEVOPS_DEFINITION_ANNOTATION =
'dev.azure.com/project-definition';
export const AZURE_DEVOPS_DEFAULT_TOP: number = 10;
@@ -0,0 +1,95 @@
/*
* 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_DEFINITION_ANNOTATION,
AZURE_DEVOPS_REPO_ANNOTATION,
} from '../constants';
export function useAnnotationFromEntity(entity: Entity): {
project: string;
repo?: string;
definition?: string;
} {
if (entity.metadata.annotations?.[AZURE_DEVOPS_DEFINITION_ANNOTATION]) {
const { project, definition } = getProjectDefinition(
entity.metadata.annotations?.[AZURE_DEVOPS_DEFINITION_ANNOTATION],
);
const repo = undefined;
return { project, repo, definition };
}
const { project, repo } = getProjectRepo(
entity.metadata.annotations?.[AZURE_DEVOPS_REPO_ANNOTATION] ?? '',
);
const definition = undefined;
return { project, repo, definition };
}
function getProjectDefinition(annotation: string): {
project: string;
definition: string;
} {
const [project, definition] = annotation.split('/');
if (!project && !definition) {
throw new Error(
'Value for annotation dev.azure.com/project-definition was not in the correct format: <project-name>/<definition-name>',
);
}
if (!project) {
throw new Error(
'Project Name for annotation dev.azure.com/project-definition was not found; expected format is: <project-name>/<definition-name>',
);
}
if (!definition) {
throw new Error(
'Definition Name for annotation dev.azure.com/project-definition was not found; expected format is: <project-name>/<definition-name>',
);
}
return { project, definition };
}
function getProjectRepo(annotation: string): {
project: string;
repo: string;
} {
const [project, repo] = annotation.split('/');
if (!project && !repo) {
throw new Error(
'Value for annotation dev.azure.com/project-repo was not in the correct format: <project-name>/<repo-name>',
);
}
if (!project) {
throw new Error(
'Project Name for annotation dev.azure.com/project-repo was not found; expected format is: <project-name>/<repo-name>',
);
}
if (!repo) {
throw new Error(
'Repo Name for annotation dev.azure.com/project-repo was not found; expected format is: <project-name>/<repo-name>',
);
}
return { project, repo };
}
@@ -0,0 +1,53 @@
/*
* 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 {
BuildRun,
BuildRunOptions,
} from '@backstage/plugin-azure-devops-common';
import { AZURE_DEVOPS_DEFAULT_TOP } from '../constants';
import { azureDevOpsApiRef } from '../api';
import { useApi } from '@backstage/core-plugin-api';
import { useAsync } from 'react-use';
export function useBuildRuns(
projectName: string,
defaultLimit?: number,
repoName?: string,
definitionName?: string,
): {
items?: BuildRun[];
loading: boolean;
error?: Error;
} {
const top = defaultLimit ?? AZURE_DEVOPS_DEFAULT_TOP;
const options: BuildRunOptions = {
top: top,
};
const api = useApi(azureDevOpsApiRef);
const { value, loading, error } = useAsync(() => {
return api.getBuildRuns(projectName, repoName, definitionName, options);
}, [api, projectName, repoName, definitionName]);
return {
items: value?.items,
loading,
error,
};
}
@@ -15,14 +15,14 @@
*/
import { Entity } from '@backstage/catalog-model';
import { AZURE_DEVOPS_ANNOTATION } from '../constants';
import { AZURE_DEVOPS_REPO_ANNOTATION } from '../constants';
export function useProjectRepoFromEntity(entity: Entity): {
project: string;
repo: string;
} {
const [project, repo] = (
entity.metadata.annotations?.[AZURE_DEVOPS_ANNOTATION] ?? ''
entity.metadata.annotations?.[AZURE_DEVOPS_REPO_ANNOTATION] ?? ''
).split('/');
if (!project && !repo) {
+1
View File
@@ -19,6 +19,7 @@ export {
EntityAzurePipelinesContent,
EntityAzurePullRequestsContent,
isAzureDevOpsAvailable,
isAzurePipelinesAvailable,
AzurePullRequestsPage,
} from './plugin';
+9 -2
View File
@@ -14,6 +14,10 @@
* limitations under the License.
*/
import {
AZURE_DEVOPS_DEFINITION_ANNOTATION,
AZURE_DEVOPS_REPO_ANNOTATION,
} from './constants';
import {
azurePipelinesEntityContentRouteRef,
azurePullRequestDashboardRouteRef,
@@ -27,13 +31,16 @@ import {
identityApiRef,
} from '@backstage/core-plugin-api';
import { AZURE_DEVOPS_ANNOTATION } from './constants';
import { AzureDevOpsClient } from './api/AzureDevOpsClient';
import { Entity } from '@backstage/catalog-model';
import { azureDevOpsApiRef } from './api/AzureDevOpsApi';
export const isAzureDevOpsAvailable = (entity: Entity) =>
Boolean(entity.metadata.annotations?.[AZURE_DEVOPS_ANNOTATION]);
Boolean(entity.metadata.annotations?.[AZURE_DEVOPS_REPO_ANNOTATION]);
export const isAzurePipelinesAvailable = (entity: Entity) =>
Boolean(entity.metadata.annotations?.[AZURE_DEVOPS_REPO_ANNOTATION]) ||
Boolean(entity.metadata.annotations?.[AZURE_DEVOPS_DEFINITION_ANNOTATION]);
export const azureDevOpsPlugin = createPlugin({
id: 'azureDevOps',