diff --git a/app-config.yaml b/app-config.yaml index c24ac1701b..b92595df7b 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -436,3 +436,4 @@ azureDevOps: host: dev.azure.com token: my-token organization: my-company + top: 25 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..b79bd2c4aa 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 { + EntityAzureDevOpsContent, + 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 }); + } + ``` + +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 = ( + + // ... + + + + // ... + +``` + +### 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..11de164276 --- /dev/null +++ b/plugins/azure-devops/api-report.md @@ -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 +/// + +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) "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) +``` diff --git a/plugins/azure-devops/config.d.ts b/plugins/azure-devops/config.d.ts new file mode 100644 index 0000000000..827e64d5bd --- /dev/null +++ b/plugins/azure-devops/config.d.ts @@ -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; + }; +} 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/doc/azure-devops-builds.png b/plugins/azure-devops/doc/azure-devops-builds.png new file mode 100644 index 0000000000..89a777059f Binary files /dev/null and b/plugins/azure-devops/doc/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..fb59d5eb38 --- /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", + "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" +} diff --git a/plugins/azure-devops/src/api/AzureDevOpsApi.ts b/plugins/azure-devops/src/api/AzureDevOpsApi.ts new file mode 100644 index 0000000000..424999441d --- /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 } 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, + top: number, + ): Promise; +} diff --git a/plugins/azure-devops/src/api/AzureDevOpsClient.ts b/plugins/azure-devops/src/api/AzureDevOpsClient.ts new file mode 100644 index 0000000000..c9e44109aa --- /dev/null +++ b/plugins/azure-devops/src/api/AzureDevOpsClient.ts @@ -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 { + return await this.get(`/repo-builds/${projectName}/${repoName}?top=${top}`); + } + + private async get(path: string): Promise { + 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(); + } +} 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..35d4e38ae1 --- /dev/null +++ b/plugins/azure-devops/src/api/types.ts @@ -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; +}; 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..26f365c5e9 --- /dev/null +++ b/plugins/azure-devops/src/components/BuildTable/BuildTable.tsx @@ -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 ; + case 2: // Succeeded + return ; + case 4: // PartiallySucceeded + return ; + case 8: // Failed + return ; + case 32: // Canceled + return ; + default: + return ; + } +}; + +const columns: TableColumn[] = [ + { + title: 'ID', + field: 'id', + highlight: false, + width: '80px', + }, + { + title: 'Build', + field: 'title', + render: (row: Partial) => ( + + {row.title} + + ), + }, + { + title: 'Source', + field: 'source', + }, + { + title: 'Status', + field: 'status', + render: (row: Partial) => ( + + + {BuildStatus[row.status || 0]} + + ), + }, + { + title: 'Result', + field: 'result', + render: (row: Partial) => ( + + {getBuildResultComponent(row.result)} + + {BuildResult[row.result || 0]} + + ), + }, + { + title: 'Date', + field: 'queueTime', + render: (row: Partial) => moment(row.queueTime).fromNow(), + }, +]; + +type Props = { + items: RepoBuild[]; + loading: boolean; + error?: any; +}; + +export const BuildTable = ({ items, loading, error }: Props) => { + if (error) { + return ( +
+ + Error encountered while fetching Azure DevOps builds.{' '} + {error.toString()} + +
+ ); + } + + 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/EntityPageAzureDevOps/EntityPageAzureDevOps.tsx b/plugins/azure-devops/src/components/EntityPageAzureDevOps/EntityPageAzureDevOps.tsx new file mode 100644 index 0000000000..fbeac53569 --- /dev/null +++ b/plugins/azure-devops/src/components/EntityPageAzureDevOps/EntityPageAzureDevOps.tsx @@ -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 ; +}; diff --git a/plugins/azure-devops/src/components/EntityPageAzureDevOps/index.ts b/plugins/azure-devops/src/components/EntityPageAzureDevOps/index.ts new file mode 100644 index 0000000000..89ee4f199b --- /dev/null +++ b/plugins/azure-devops/src/components/EntityPageAzureDevOps/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 { EntityPageAzureDevOps } from './EntityPageAzureDevOps'; diff --git a/plugins/azure-devops/src/components/Router.tsx b/plugins/azure-devops/src/components/Router.tsx new file mode 100644 index 0000000000..74cf0439f8 --- /dev/null +++ b/plugins/azure-devops/src/components/Router.tsx @@ -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 ; + } + + return ( + + } + /> + + ); +}; diff --git a/plugins/azure-devops/src/constants.ts b/plugins/azure-devops/src/constants.ts new file mode 100644 index 0000000000..6abc6449e9 --- /dev/null +++ b/plugins/azure-devops/src/constants.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 const AZURE_DEVOPS_ANNOTATION = 'dev.azure.com/project-repo'; diff --git a/plugins/azure-devops/src/hooks/useProjectRepoFromEntity.ts b/plugins/azure-devops/src/hooks/useProjectRepoFromEntity.ts new file mode 100644 index 0000000000..721fa52781 --- /dev/null +++ b/plugins/azure-devops/src/hooks/useProjectRepoFromEntity.ts @@ -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 }; +} diff --git a/plugins/azure-devops/src/hooks/useRepoBuilds.ts b/plugins/azure-devops/src/hooks/useRepoBuilds.ts new file mode 100644 index 0000000000..e6d2964770 --- /dev/null +++ b/plugins/azure-devops/src/hooks/useRepoBuilds.ts @@ -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, + }; +} diff --git a/plugins/azure-devops/src/index.ts b/plugins/azure-devops/src/index.ts new file mode 100644 index 0000000000..b654aebab7 --- /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, EntityAzureDevOpsContent } 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..f37fab6d62 --- /dev/null +++ b/plugins/azure-devops/src/plugin.ts @@ -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, + }), +); 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';