diff --git a/packages/app/package.json b/packages/app/package.json index d1892438d8..fe86077273 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -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", diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 62fdcf4d3f..3fe027ddad 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -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 = ( + + + + { - return await createRouter({ logger, config }); + return createRouter({ logger, config }); } diff --git a/plugins/azure-devops/.eslintrc.js b/plugins/azure-devops/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/azure-devops/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/azure-devops/README.md b/plugins/azure-devops/README.md new file mode 100644 index 0000000000..edf26ecfb3 --- /dev/null +++ b/plugins/azure-devops/README.md @@ -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 { + 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 = ( + + // ... + + // Set defaultLimit to the max number of builds you would like to be able to see + // the default if not set is 10 + + + // ... + + ``` + +### 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: / +``` + +Let's break this down a little: `` will be the name of your Team Project and `` will be the name of your repository which needs to be part of the Team Project you entered for ``. + +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 diff --git a/plugins/azure-devops/api-report.md b/plugins/azure-devops/api-report.md new file mode 100644 index 0000000000..fbe28c179c --- /dev/null +++ b/plugins/azure-devops/api-report.md @@ -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 +/// + +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; + }, + {} +>; + +// 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) +``` diff --git a/plugins/azure-devops/dev/index.tsx b/plugins/azure-devops/dev/index.tsx new file mode 100644 index 0000000000..8d1c3ef6ea --- /dev/null +++ b/plugins/azure-devops/dev/index.tsx @@ -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(); diff --git a/plugins/azure-devops/docs/azure-devops-builds.png b/plugins/azure-devops/docs/azure-devops-builds.png new file mode 100644 index 0000000000..4788a185ed Binary files /dev/null and b/plugins/azure-devops/docs/azure-devops-builds.png differ diff --git a/plugins/azure-devops/package.json b/plugins/azure-devops/package.json new file mode 100644 index 0000000000..3aa545cc27 --- /dev/null +++ b/plugins/azure-devops/package.json @@ -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" + ] +} diff --git a/plugins/azure-devops/src/api/AzureDevOpsApi.ts b/plugins/azure-devops/src/api/AzureDevOpsApi.ts new file mode 100644 index 0000000000..f0bad53b43 --- /dev/null +++ b/plugins/azure-devops/src/api/AzureDevOpsApi.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, RepoBuildOptions } from './types'; +import { createApiRef } from '@backstage/core-plugin-api'; + +export const azureDevOpsApiRef = createApiRef({ + 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[] }>; +} diff --git a/plugins/azure-devops/src/api/AzureDevOpsClient.ts b/plugins/azure-devops/src/api/AzureDevOpsClient.ts new file mode 100644 index 0000000000..e03428e511 --- /dev/null +++ b/plugins/azure-devops/src/api/AzureDevOpsClient.ts @@ -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( + `repo-builds/${projectName}/${repoName}?top=${options?.top}`, + ); + return { items }; + } + + private async get(path: string): Promise { + 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; + } +} diff --git a/plugins/azure-devops/src/api/index.ts b/plugins/azure-devops/src/api/index.ts new file mode 100644 index 0000000000..c69636222f --- /dev/null +++ b/plugins/azure-devops/src/api/index.ts @@ -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'; diff --git a/plugins/azure-devops/src/api/types.ts b/plugins/azure-devops/src/api/types.ts new file mode 100644 index 0000000000..1fbc34231e --- /dev/null +++ b/plugins/azure-devops/src/api/types.ts @@ -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; +}; diff --git a/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx b/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx new file mode 100644 index 0000000000..bead424ffb --- /dev/null +++ b/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx @@ -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 ( + + Succeeded + + ); + case BuildResult.PartiallySucceeded: + return ( + + Partially Succeeded + + ); + case BuildResult.Failed: + return ( + + Failed + + ); + case BuildResult.Canceled: + return ( + + Canceled + + ); + case BuildResult.None: + default: + return ( + + Unknown + + ); + } +}; + +const getBuildStateComponent = ( + status: number | undefined, + result: number | undefined, +) => { + switch (status) { + case BuildStatus.InProgress: + return ( + + In Progress + + ); + case BuildStatus.Completed: + return getBuildResultComponent(result); + case BuildStatus.Cancelling: + return ( + + Cancelling + + ); + case BuildStatus.Postponed: + return ( + + Postponed + + ); + case BuildStatus.NotStarted: + return ( + + Not Started + + ); + case BuildStatus.None: + default: + return ( + + Unknown + + ); + } +}; + +const columns: TableColumn[] = [ + { + title: 'ID', + field: 'id', + highlight: false, + width: 'auto', + }, + { + title: 'Build', + field: 'title', + width: 'auto', + render: (row: Partial) => ( + {row.title} + ), + }, + { + title: 'Source', + field: 'source', + width: 'auto', + }, + { + title: 'State', + width: 'auto', + render: (row: Partial) => ( + + + {getBuildStateComponent(row.status, row.result)} + + + ), + }, + { + title: 'Age', + field: 'queueTime', + width: 'auto', + render: (row: Partial) => + 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 ( +
+ +
+ ); + } + + return ( + + ); +}; diff --git a/plugins/azure-devops/src/components/BuildTable/index.ts b/plugins/azure-devops/src/components/BuildTable/index.ts new file mode 100644 index 0000000000..acf20eb523 --- /dev/null +++ b/plugins/azure-devops/src/components/BuildTable/index.ts @@ -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'; diff --git a/plugins/azure-devops/src/components/EntityPageAzurePipelines/EntityPageAzurePipelines.tsx b/plugins/azure-devops/src/components/EntityPageAzurePipelines/EntityPageAzurePipelines.tsx new file mode 100644 index 0000000000..c993922105 --- /dev/null +++ b/plugins/azure-devops/src/components/EntityPageAzurePipelines/EntityPageAzurePipelines.tsx @@ -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 ; +}; diff --git a/plugins/azure-devops/src/components/EntityPageAzurePipelines/index.ts b/plugins/azure-devops/src/components/EntityPageAzurePipelines/index.ts new file mode 100644 index 0000000000..c907c61b2c --- /dev/null +++ b/plugins/azure-devops/src/components/EntityPageAzurePipelines/index.ts @@ -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'; diff --git a/plugins/azure-devops/src/components/Router.tsx b/plugins/azure-devops/src/components/Router.tsx new file mode 100644 index 0000000000..3000deb5cc --- /dev/null +++ b/plugins/azure-devops/src/components/Router.tsx @@ -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 ; + } + + return ( + + } + /> + + ); +}; diff --git a/plugins/azure-devops/src/constants.ts b/plugins/azure-devops/src/constants.ts new file mode 100644 index 0000000000..d6e67ff7ed --- /dev/null +++ b/plugins/azure-devops/src/constants.ts @@ -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; diff --git a/plugins/azure-devops/src/hooks/useProjectRepoFromEntity.ts b/plugins/azure-devops/src/hooks/useProjectRepoFromEntity.ts new file mode 100644 index 0000000000..418badc630 --- /dev/null +++ b/plugins/azure-devops/src/hooks/useProjectRepoFromEntity.ts @@ -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: /', + ); + } + + if (!project) { + throw new Error( + 'Project Name for annotation dev.azure.com/project-repo was not found; expected format is: /', + ); + } + + if (!repo) { + throw new Error( + 'Repo Name for annotation dev.azure.com/project-repo was not found; expected format is: /', + ); + } + + return { project, repo }; +} diff --git a/plugins/azure-devops/src/hooks/useRepoBuilds.ts b/plugins/azure-devops/src/hooks/useRepoBuilds.ts new file mode 100644 index 0000000000..14a8a79428 --- /dev/null +++ b/plugins/azure-devops/src/hooks/useRepoBuilds.ts @@ -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, + }; +} diff --git a/plugins/azure-devops/src/index.ts b/plugins/azure-devops/src/index.ts new file mode 100644 index 0000000000..9a90d6ed6a --- /dev/null +++ b/plugins/azure-devops/src/index.ts @@ -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'; diff --git a/plugins/azure-devops/src/plugin.test.ts b/plugins/azure-devops/src/plugin.test.ts new file mode 100644 index 0000000000..f23639798f --- /dev/null +++ b/plugins/azure-devops/src/plugin.test.ts @@ -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(); + }); +}); diff --git a/plugins/azure-devops/src/plugin.ts b/plugins/azure-devops/src/plugin.ts new file mode 100644 index 0000000000..1cf6e680d0 --- /dev/null +++ b/plugins/azure-devops/src/plugin.ts @@ -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, + }), +); diff --git a/plugins/azure-devops/src/routes.ts b/plugins/azure-devops/src/routes.ts new file mode 100644 index 0000000000..81f4b48fda --- /dev/null +++ b/plugins/azure-devops/src/routes.ts @@ -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', +}); diff --git a/plugins/azure-devops/src/setupTests.ts b/plugins/azure-devops/src/setupTests.ts new file mode 100644 index 0000000000..fc6dbd98f8 --- /dev/null +++ b/plugins/azure-devops/src/setupTests.ts @@ -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';