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
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-azure-devops': minor
---
Updated to support cases where only Azure Pipelines are being used by adding an annotation that get builds by definition name
@@ -37,6 +37,7 @@ import {
EntityAzurePipelinesContent,
EntityAzurePullRequestsContent,
isAzureDevOpsAvailable,
isAzurePipelinesAvailable,
} from '@backstage/plugin-azure-devops';
import { EntityBadgesDialog } from '@backstage/plugin-badges';
import {
@@ -202,7 +203,7 @@ export const cicdContent = (
<EntityGithubActionsContent />
</EntitySwitch.Case>
<EntitySwitch.Case if={isAzureDevOpsAvailable}>
<EntitySwitch.Case if={isAzurePipelinesAvailable}>
<EntityAzurePipelinesContent defaultLimit={25} />
</EntitySwitch.Case>
+10
View File
@@ -49,6 +49,16 @@ spec:
# ...
```
#### Azure Pipelines Only
If you are only using Azure Pipelines along with a different SCM tool then you can use the following annotation to see Builds:
```yaml
dev.azure.com/project-definition: <project-name>/<definition-name>
```
In this case `<project-name>` will be the name of your Team Project and `<definition-name>` will be the name of the Build Definition you would like to see Builds for. If the Build Definition name has spaces in it make sure to put quotes around it
### Azure Pipelines Component
To get the Azure Pipelines component working you'll need to do the following two steps:
+5
View File
@@ -199,6 +199,11 @@ export enum FilterType {
// @public (undocumented)
export const isAzureDevOpsAvailable: (entity: Entity) => boolean;
// Warning: (ae-missing-release-tag) "isAzurePipelinesAvailable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const isAzurePipelinesAvailable: (entity: Entity) => boolean;
// Warning: (ae-missing-release-tag) "PullRequestColumnConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -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',