Merge pull request #7484 from awanlin/awanlin/azure-devops-frontend-plugin

Adding Azure DevOps frontend plugin
This commit is contained in:
Fredrik Adelöw
2021-10-13 12:30:39 +02:00
committed by GitHub
27 changed files with 945 additions and 3 deletions
+1
View File
@@ -11,6 +11,7 @@
"@backstage/core-plugin-api": "^0.1.10",
"@backstage/integration-react": "^0.1.11",
"@backstage/plugin-api-docs": "^0.6.11",
"@backstage/plugin-azure-devops": "^0.1.0",
"@backstage/plugin-badges": "^0.2.12",
"@backstage/plugin-catalog": "^0.7.0",
"@backstage/plugin-catalog-graph": "^0.1.3",
@@ -33,6 +33,10 @@ import {
EntityProvidedApisCard,
EntityProvidingComponentsCard,
} from '@backstage/plugin-api-docs';
import {
EntityAzurePipelinesContent,
isAzureDevOpsAvailable,
} from '@backstage/plugin-azure-devops';
import { EntityBadgesDialog } from '@backstage/plugin-badges';
import {
EntityAboutCard,
@@ -183,6 +187,10 @@ export const cicdContent = (
<EntityGithubActionsContent />
</EntitySwitch.Case>
<EntitySwitch.Case if={isAzureDevOpsAvailable}>
<EntityAzurePipelinesContent defaultLimit={25} />
</EntitySwitch.Case>
<EntitySwitch.Case>
<EmptyState
title="No CI/CD available for this entity"
+1 -1
View File
@@ -38,7 +38,7 @@ import { Config } from '@backstage/config';
import healthcheck from './plugins/healthcheck';
import { metricsInit, metricsHandler } from './metrics';
import auth from './plugins/auth';
import azureDevOps from './plugins/azuredevops';
import azureDevOps from './plugins/azure-devops';
import catalog from './plugins/catalog';
import codeCoverage from './plugins/codecoverage';
import kubernetes from './plugins/kubernetes';
@@ -18,9 +18,9 @@ import { createRouter } from '@backstage/plugin-azure-devops-backend';
import { Router } from 'express';
import type { PluginEnvironment } from '../types';
export default async function createPlugin({
export default function createPlugin({
logger,
config,
}: PluginEnvironment): Promise<Router> {
return await createRouter({ logger, config });
return createRouter({ logger, config });
}
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
extends: [require.resolve('@backstage/cli/config/eslint')],
};
+135
View File
@@ -0,0 +1,135 @@
# Azure DevOps Plugin
Website: [https://dev.azure.com/](https://dev.azure.com/)
![Azure DevOps Builds Example](./docs/azure-devops-builds.png)
## Setup
The following sections will help you get the Azure DevOps plugin setup and running
### Configuration
The Azure DevOps plugin requires the following YAML to be added to your app-config.yaml:
```yaml
azureDevOps:
host: dev.azure.com
token: ${AZURE_TOKEN}
organization: my-company
```
Configuration Details:
- `host` and `token` can be the same as the ones used for the `integration` section
- `AZURE_TOKEN` environment variable must be set to a [Personal Access Token](https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page) with read access to both Code and Build
- `organization` is your Azure DevOps Organization name or for Azure DevOps Server (on-premise) this will be your Collection name
### Backend
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:
```sh
# From the Backstage root directory
cd packages/backend
yarn add @backstage/plugin-azure-devops-backend
```
2. Then we will create a new file named `packages/backend/src/plugins/azure-devops.ts`, and add the
following to it:
```ts
import { createRouter } from '@backstage/plugin-azure-devops-backend';
import { Router } from 'express';
import type { PluginEnvironment } from '../types';
export default function createPlugin({
logger,
config,
}: PluginEnvironment): Promise<Router> {
return createRouter({ logger, config });
}
```
3. Next we wire this into the overall backend router, edit `packages/backend/src/index.ts`:
```ts
import azureDevOps from './plugins/azuredevops';
// ...
async function main() {
// ...
const azureDevOpsEnv = useHotMemoize(module, () => createEnv('azure-devops'));
apiRouter.use('/azure-devops', await azureDevOps(azureDevOpsEnv));
```
4. Now run `yarn start-backend` from the repo root
5. Finally open `http://localhost:7000/api/azure-devops/health` in a browser and it should return `{"status":"ok"}`
### Frontend
To get the frontend working you'll need to do the following two steps:
1. First we need to add the @backstage/plugin-azure-devops package to your frontend app:
```bash
# From your Backstage root directory
cd packages/app
yarn add @backstage/plugin-azure-devops
```
2. Second we need to add the `EntityAzurePipelinesContent` extension to the entity page in your app:
```tsx
// In packages/app/src/components/catalog/EntityPage.tsx
import {
EntityAzurePipelinesContent,
isAzureDevOpsAvailable,
} from '@backstage/plugin-azure-devops';
// For example in the CI/CD section
const cicdContent = (
<EntitySwitch>
// ...
<EntitySwitch.Case if={isAzureDevOpsAvailable}>
// Set defaultLimit to the max number of builds you would like to be able to see
// the default if not set is 10
<EntityAzurePipelinesContent defaultLimit={25} />
</EntitySwitch.Case>
// ...
</EntitySwitch>
```
### Entity Annotation
You need to add the following annotation to any entities you want to be able to use the Azure Devops plugin with:
```yaml
dev.azure.com/project-repo: <project-name>/<repo-name>
```
Let's break this down a little: `<project-name>` will be the name of your Team Project and `<repo-name>` will be the name of your repository which needs to be part of the Team Project you entered for `<project-name>`.
Here's what that will look like in action:
```yaml
# Example catalog-info.yaml entity definition file
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
# ...
annotations:
dev.azure.com/project-repo: my-project/my-repo
spec:
type: service
# ...
```
## Features
- Lists the top _n_ builds for a given repository where _n_ is the value configured for `top`
## Limitations
- Currently multiple organizations is not supported
+37
View File
@@ -0,0 +1,37 @@
## API Report File for "@backstage/plugin-azure-devops"
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
/// <reference types="react" />
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { Entity } from '@backstage/catalog-model';
import { RouteRef } from '@backstage/core-plugin-api';
// Warning: (ae-missing-release-tag) "azureDevOpsPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const azureDevOpsPlugin: BackstagePlugin<
{
entityContent: RouteRef<undefined>;
},
{}
>;
// 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: ({
defaultLimit,
}: {
defaultLimit?: number | undefined;
}) => 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)
//
// @public (undocumented)
export const isAzureDevOpsAvailable: (entity: Entity) => boolean;
// (No @packageDocumentation comment for this package)
```
+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 { createDevApp } from '@backstage/dev-utils';
import { azureDevOpsPlugin } from '../src/plugin';
createDevApp().registerPlugin(azureDevOpsPlugin).render();
Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

+62
View File
@@ -0,0 +1,62 @@
{
"name": "@backstage/plugin-azure-devops",
"version": "0.1.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"private": false,
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"homepage": "https://backstage.io",
"repository": {
"type": "git",
"url": "https://github.com/backstage/backstage",
"directory": "plugins/azure-devops"
},
"scripts": {
"build": "backstage-cli plugin:build",
"start": "backstage-cli plugin:serve",
"lint": "backstage-cli lint",
"test": "backstage-cli test",
"diff": "backstage-cli plugin:diff",
"prepack": "backstage-cli prepack",
"postpack": "backstage-cli postpack",
"clean": "backstage-cli clean"
},
"dependencies": {
"@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",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.57",
"azure-devops-node-api": "^11.0.1",
"luxon": "^2.0.2",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-router": "6.0.0-beta.0",
"react-use": "^17.2.4"
},
"devDependencies": {
"@backstage/cli": "^0.7.15",
"@backstage/core-app-api": "^0.1.16",
"@backstage/dev-utils": "^0.2.11",
"@backstage/test-utils": "^0.1.18",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^13.1.8",
"@types/jest": "^26.0.7",
"@types/node": "^14.14.32",
"cross-fetch": "^3.0.6",
"msw": "^0.29.0"
},
"files": [
"dist"
]
}
@@ -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, RepoBuildOptions } 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,
options?: RepoBuildOptions,
): Promise<{ items: RepoBuild[] }>;
}
@@ -0,0 +1,60 @@
/*
* 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, RepoBuildOptions } from './types';
import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
import { ResponseError } from '@backstage/errors';
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,
options?: RepoBuildOptions,
): Promise<{ items: RepoBuild[] }> {
const items = await this.get<RepoBuild[]>(
`repo-builds/${projectName}/${repoName}?top=${options?.top}`,
);
return { items };
}
private async get<T>(path: string): Promise<T> {
const baseUrl = `${await this.discoveryApi.getBaseUrl('azure-devops')}/`;
const url = new URL(path, baseUrl);
const idToken = await this.identityApi.getIdToken();
const response = await fetch(url.toString(), {
headers: idToken ? { Authorization: `Bearer ${idToken}` } : {},
});
if (!response.ok) {
throw await ResponseError.fromResponse(response);
}
return response.json() as Promise<T>;
}
}
+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';
+34
View File
@@ -0,0 +1,34 @@
/*
* 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;
};
export type RepoBuildOptions = {
top?: number;
};
@@ -0,0 +1,186 @@
/*
* 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 { DateTime } from 'luxon';
import {
Link,
Table,
TableColumn,
StatusError,
StatusOK,
StatusWarning,
StatusAborted,
StatusRunning,
StatusPending,
ResponseErrorPanel,
} from '@backstage/core-components';
import { Box, Typography } from '@material-ui/core';
import { RepoBuild } from '../../api/types';
import {
BuildResult,
BuildStatus,
} from 'azure-devops-node-api/interfaces/BuildInterfaces';
const getBuildResultComponent = (result: number | undefined) => {
switch (result) {
case BuildResult.Succeeded:
return (
<span>
<StatusOK /> Succeeded
</span>
);
case BuildResult.PartiallySucceeded:
return (
<span>
<StatusWarning /> Partially Succeeded
</span>
);
case BuildResult.Failed:
return (
<span>
<StatusError /> Failed
</span>
);
case BuildResult.Canceled:
return (
<span>
<StatusAborted /> Canceled
</span>
);
case BuildResult.None:
default:
return (
<span>
<StatusWarning /> Unknown
</span>
);
}
};
const getBuildStateComponent = (
status: number | undefined,
result: number | undefined,
) => {
switch (status) {
case BuildStatus.InProgress:
return (
<span>
<StatusRunning /> In Progress
</span>
);
case BuildStatus.Completed:
return getBuildResultComponent(result);
case BuildStatus.Cancelling:
return (
<span>
<StatusAborted /> Cancelling
</span>
);
case BuildStatus.Postponed:
return (
<span>
<StatusPending /> Postponed
</span>
);
case BuildStatus.NotStarted:
return (
<span>
<StatusAborted /> Not Started
</span>
);
case BuildStatus.None:
default:
return (
<span>
<StatusWarning /> Unknown
</span>
);
}
};
const columns: TableColumn[] = [
{
title: 'ID',
field: 'id',
highlight: false,
width: 'auto',
},
{
title: 'Build',
field: 'title',
width: 'auto',
render: (row: Partial<RepoBuild>) => (
<Link to={row.link || ''}>{row.title}</Link>
),
},
{
title: 'Source',
field: 'source',
width: 'auto',
},
{
title: 'State',
width: 'auto',
render: (row: Partial<RepoBuild>) => (
<Box display="flex" alignItems="center">
<Typography variant="button">
{getBuildStateComponent(row.status, row.result)}
</Typography>
</Box>
),
},
{
title: 'Age',
field: 'queueTime',
width: 'auto',
render: (row: Partial<RepoBuild>) =>
DateTime.fromISO(
row.queueTime ? row.queueTime.toString() : new Date().toString(),
).toRelative(),
},
];
type Props = {
items?: RepoBuild[];
loading: boolean;
error?: any;
};
export const BuildTable = ({ items, loading, error }: Props) => {
if (error) {
return (
<div>
<ResponseErrorPanel error={error} />
</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,31 @@
/*
* 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 { useEntity } from '@backstage/plugin-catalog-react';
import React from 'react';
import { useRepoBuilds } from '../../hooks/useRepoBuilds';
import { BuildTable } from '../BuildTable/BuildTable';
export const EntityPageAzurePipelines = ({
defaultLimit,
}: {
defaultLimit?: number;
}) => {
const { entity } = useEntity();
const { items, loading, error } = useRepoBuilds(entity, defaultLimit);
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 { EntityPageAzurePipelines } from './EntityPageAzurePipelines';
@@ -0,0 +1,43 @@
/*
* 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 { EntityPageAzurePipelines } from './EntityPageAzurePipelines';
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]);
export const Router = ({ defaultLimit }: { defaultLimit?: number }) => {
const { entity } = useEntity();
if (!isAzureDevOpsAvailable(entity)) {
return <MissingAnnotationEmptyState annotation={AZURE_DEVOPS_ANNOTATION} />;
}
return (
<Routes>
<Route
path={`/${azureDevOpsRouteRef.path}`}
element={<EntityPageAzurePipelines defaultLimit={defaultLimit} />}
/>
</Routes>
);
};
+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 const AZURE_DEVOPS_ANNOTATION = 'dev.azure.com/project-repo';
export const AZURE_DEVOPS_DEFAULT_TOP: number = 10;
@@ -0,0 +1,47 @@
/*
* 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): {
project: string;
repo: string;
} {
const [project, repo] = (
entity.metadata.annotations?.[AZURE_DEVOPS_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,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 { useAsync } from 'react-use';
import { Entity } from '@backstage/catalog-model';
import { useApi } from '@backstage/core-plugin-api';
import { azureDevOpsApiRef } from '../api';
import { RepoBuild, RepoBuildOptions } from '../api/types';
import { useProjectRepoFromEntity } from './useProjectRepoFromEntity';
import { AZURE_DEVOPS_DEFAULT_TOP } from '../constants';
export function useRepoBuilds(
entity: Entity,
defaultLimit?: number,
): {
items: RepoBuild[] | undefined;
loading: boolean;
error: any;
} {
const top = defaultLimit ?? 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, options);
}, [api, project, repo, entity]);
return {
items: value?.items,
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, EntityAzurePipelinesContent } 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();
});
});
+49
View File
@@ -0,0 +1,49 @@
/*
* 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,
discoveryApiRef,
identityApiRef,
} from '@backstage/core-plugin-api';
import { azureDevOpsRouteRef } from './routes';
export const azureDevOpsPlugin = createPlugin({
id: 'azureDevOps',
apis: [
createApiFactory({
api: azureDevOpsApiRef,
deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },
factory: ({ discoveryApi, identityApi }) =>
new AzureDevOpsClient({ discoveryApi, identityApi }),
}),
],
routes: {
entityContent: azureDevOpsRouteRef,
},
});
export const EntityAzurePipelinesContent = azureDevOpsPlugin.provide(
createRoutableExtension({
name: 'EntityAzurePipelinesContent',
component: () => import('./components/Router').then(m => m.Router),
mountPoint: azureDevOpsRouteRef,
}),
);
+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';