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
+1
View File
@@ -436,3 +436,4 @@ azureDevOps:
host: dev.azure.com
token: my-token
organization: my-company
top: 25
+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 {
EntityAzureDevOpsContent,
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}>
<EntityAzureDevOpsContent />
</EntitySwitch.Case>
<EntitySwitch.Case>
<EmptyState
title="No CI/CD available for this entity"
+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](./doc/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
top: 50
```
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
- `top` is strictly used by the frontend to limit the number of items returned, currently only used for Builds
### 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/azuredevops.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 async function createPlugin({
logger,
config,
}: PluginEnvironment): Promise<Router> {
return await 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 `EntityAzureDevOpsContent` extension to the entity page in your app:
```tsx
// In packages/app/src/components/catalog/EntityPage.tsx
import {
EntityAzureDevOpsContent,
isAzureDevOpsAvailable,
} from '@backstage/plugin-azure-devops';
// For example in the CI/CD section
const cicdContent = (
<EntitySwitch>
// ...
<EntitySwitch.Case if={isAzureDevOpsAvailable}>
<EntityAzureDevOpsContent />
</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
+35
View File
@@ -0,0 +1,35 @@
## 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) "EntityAzureDevOpsContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const EntityAzureDevOpsContent: (_props: {
entity?: Entity | 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)
```
+26
View File
@@ -0,0 +1,26 @@
/*
* Copyright 2020 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 interface Config {
/** Configuration options for the azure-devops-backend plugin */
azureDevOps: {
/**
* The max number of items to return - applies to builds
* @visibility frontend
*/
top: number;
};
}
+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",
"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.0",
"@backstage/core-plugin-api": "^0.1.9",
"@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",
"moment": "^2.29.1",
"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.13",
"@backstage/core-app-api": "^0.1.14",
"@backstage/dev-utils": "^0.2.10",
"@backstage/test-utils": "^0.1.17",
"@testing-library/jest-dom": "^5.10.1",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^13.1.8",
"@types/jest": "*",
"@types/node": "*",
"msw": "^0.29.0",
"cross-fetch": "^3.0.6"
},
"files": [
"dist",
"config.d.ts"
],
"configSchema": "config.d.ts"
}
@@ -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';