From 361bb34d8eeb6c2d1bdfcd5521744ca2f0594eef Mon Sep 17 00:00:00 2001 From: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Date: Fri, 25 Aug 2023 14:03:17 -0500 Subject: [PATCH 1/7] Refactored annotation values Signed-off-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> --- .changeset/poor-seahorses-rush.md | 5 + .../EntityPageAzurePipelines.tsx | 4 +- .../src/components/ReadmeCard/ReadmeCard.tsx | 18 +- plugins/azure-devops/src/constants.ts | 2 + plugins/azure-devops/src/hooks/index.ts | 1 - plugins/azure-devops/src/hooks/useGitTags.ts | 11 +- .../src/hooks/useProjectRepoFromEntity.ts | 47 ---- .../azure-devops/src/hooks/usePullRequests.ts | 11 +- .../azure-devops/src/hooks/useRepoBuilds.ts | 11 +- .../getAnnotationValuesFromEntity.test.ts | 230 ++++++++++++++++++ ...ty.ts => getAnnotationValuesFromEntity.ts} | 29 ++- plugins/azure-devops/src/utils/index.ts | 2 +- 12 files changed, 292 insertions(+), 79 deletions(-) create mode 100644 .changeset/poor-seahorses-rush.md delete mode 100644 plugins/azure-devops/src/hooks/useProjectRepoFromEntity.ts create mode 100644 plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts rename plugins/azure-devops/src/utils/{getAnnotationFromEntity.ts => getAnnotationValuesFromEntity.ts} (72%) diff --git a/.changeset/poor-seahorses-rush.md b/.changeset/poor-seahorses-rush.md new file mode 100644 index 0000000000..55313f706d --- /dev/null +++ b/.changeset/poor-seahorses-rush.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-azure-devops': patch +--- + +Consolidated getting the annotation values into a single function to help with future changes diff --git a/plugins/azure-devops/src/components/EntityPageAzurePipelines/EntityPageAzurePipelines.tsx b/plugins/azure-devops/src/components/EntityPageAzurePipelines/EntityPageAzurePipelines.tsx index ad278e3fc4..02a61a9a9a 100644 --- a/plugins/azure-devops/src/components/EntityPageAzurePipelines/EntityPageAzurePipelines.tsx +++ b/plugins/azure-devops/src/components/EntityPageAzurePipelines/EntityPageAzurePipelines.tsx @@ -16,14 +16,14 @@ import { BuildTable } from '../BuildTable/BuildTable'; import React from 'react'; -import { getAnnotationFromEntity } from '../../utils/getAnnotationFromEntity'; +import { getAnnotationValuesFromEntity } from '../../utils/getAnnotationValuesFromEntity'; import { useBuildRuns } from '../../hooks/useBuildRuns'; import { useEntity } from '@backstage/plugin-catalog-react'; export const EntityPageAzurePipelines = (props: { defaultLimit?: number }) => { const { entity } = useEntity(); - const { project, repo, definition } = getAnnotationFromEntity(entity); + const { project, repo, definition } = getAnnotationValuesFromEntity(entity); const { items, loading, error } = useBuildRuns( project, diff --git a/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx b/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx index d06726e929..d9afe8db5a 100644 --- a/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx +++ b/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx @@ -23,11 +23,11 @@ import { ErrorPanel, } from '@backstage/core-components'; import { useEntity } from '@backstage/plugin-catalog-react'; -import { useProjectRepoFromEntity } from '../../hooks'; import { useApi } from '@backstage/core-plugin-api'; import React from 'react'; import { azureDevOpsApiRef } from '../../api'; import useAsync from 'react-use/lib/useAsync'; +import { getAnnotationValuesFromEntity } from '../../utils'; const useStyles = makeStyles(theme => ({ readMe: { @@ -87,16 +87,14 @@ export const ReadmeCard = (props: Props) => { const classes = useStyles(); const api = useApi(azureDevOpsApiRef); const { entity } = useEntity(); - const { project, repo } = useProjectRepoFromEntity(entity); + const { project, repo } = getAnnotationValuesFromEntity(entity); - const { loading, error, value } = useAsync( - () => - api.getReadme({ - project, - repo, - }), - [api, project, repo, entity], - ); + const { loading, error, value } = useAsync(async () => { + if (repo) { + return await api.getReadme({ project, repo }); + } + return undefined; + }, [api, project, repo, entity]); if (loading) { return ; diff --git a/plugins/azure-devops/src/constants.ts b/plugins/azure-devops/src/constants.ts index 95e5be4930..765f5a965b 100644 --- a/plugins/azure-devops/src/constants.ts +++ b/plugins/azure-devops/src/constants.ts @@ -16,6 +16,8 @@ export const AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION = 'dev.azure.com/build-definition'; +export const AZURE_DEVOPS_ORGANIZATION_ANNOTATION = + 'dev.azure.com/organization'; export const AZURE_DEVOPS_PROJECT_ANNOTATION = 'dev.azure.com/project'; export const AZURE_DEVOPS_REPO_ANNOTATION = 'dev.azure.com/project-repo'; export const AZURE_DEVOPS_DEFAULT_TOP: number = 10; diff --git a/plugins/azure-devops/src/hooks/index.ts b/plugins/azure-devops/src/hooks/index.ts index a3864c21f7..0b745ba586 100644 --- a/plugins/azure-devops/src/hooks/index.ts +++ b/plugins/azure-devops/src/hooks/index.ts @@ -16,7 +16,6 @@ export * from './useAllTeams'; export * from './useDashboardPullRequests'; -export * from './useProjectRepoFromEntity'; export * from './usePullRequests'; export * from './useRepoBuilds'; export * from './useUserEmail'; diff --git a/plugins/azure-devops/src/hooks/useGitTags.ts b/plugins/azure-devops/src/hooks/useGitTags.ts index b940a7105d..f36f9224aa 100644 --- a/plugins/azure-devops/src/hooks/useGitTags.ts +++ b/plugins/azure-devops/src/hooks/useGitTags.ts @@ -20,7 +20,7 @@ import { Entity } from '@backstage/catalog-model'; import { azureDevOpsApiRef } from '../api'; import { useApi } from '@backstage/core-plugin-api'; import useAsync from 'react-use/lib/useAsync'; -import { useProjectRepoFromEntity } from './useProjectRepoFromEntity'; +import { getAnnotationValuesFromEntity } from '../utils'; export function useGitTags(entity: Entity): { items?: GitTag[]; @@ -28,10 +28,13 @@ export function useGitTags(entity: Entity): { error?: Error; } { const api = useApi(azureDevOpsApiRef); - const { project, repo } = useProjectRepoFromEntity(entity); + const { project, repo } = getAnnotationValuesFromEntity(entity); - const { value, loading, error } = useAsync(() => { - return api.getGitTags(project, repo); + const { value, loading, error } = useAsync(async () => { + if (repo) { + return await api.getGitTags(project, repo); + } + return undefined; }, [api, project, repo]); return { diff --git a/plugins/azure-devops/src/hooks/useProjectRepoFromEntity.ts b/plugins/azure-devops/src/hooks/useProjectRepoFromEntity.ts deleted file mode 100644 index b484bca009..0000000000 --- a/plugins/azure-devops/src/hooks/useProjectRepoFromEntity.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * 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_REPO_ANNOTATION } from '../constants'; - -export function useProjectRepoFromEntity(entity: Entity): { - project: string; - repo: string; -} { - const [project, repo] = ( - entity.metadata.annotations?.[AZURE_DEVOPS_REPO_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/usePullRequests.ts b/plugins/azure-devops/src/hooks/usePullRequests.ts index 9ddca9650b..c412836ae5 100644 --- a/plugins/azure-devops/src/hooks/usePullRequests.ts +++ b/plugins/azure-devops/src/hooks/usePullRequests.ts @@ -25,7 +25,7 @@ import { Entity } from '@backstage/catalog-model'; import { azureDevOpsApiRef } from '../api'; import { useApi } from '@backstage/core-plugin-api'; import useAsync from 'react-use/lib/useAsync'; -import { useProjectRepoFromEntity } from './useProjectRepoFromEntity'; +import { getAnnotationValuesFromEntity } from '../utils'; export function usePullRequests( entity: Entity, @@ -44,10 +44,13 @@ export function usePullRequests( }; const api = useApi(azureDevOpsApiRef); - const { project, repo } = useProjectRepoFromEntity(entity); + const { project, repo } = getAnnotationValuesFromEntity(entity); - const { value, loading, error } = useAsync(() => { - return api.getPullRequests(project, repo, options); + const { value, loading, error } = useAsync(async () => { + if (repo) { + return await api.getPullRequests(project, repo, options); + } + return undefined; }, [api, project, repo, top, status]); return { diff --git a/plugins/azure-devops/src/hooks/useRepoBuilds.ts b/plugins/azure-devops/src/hooks/useRepoBuilds.ts index e0fe8c2093..4ba88bd5fb 100644 --- a/plugins/azure-devops/src/hooks/useRepoBuilds.ts +++ b/plugins/azure-devops/src/hooks/useRepoBuilds.ts @@ -24,7 +24,7 @@ import { Entity } from '@backstage/catalog-model'; import { azureDevOpsApiRef } from '../api'; import { useApi } from '@backstage/core-plugin-api'; import useAsync from 'react-use/lib/useAsync'; -import { useProjectRepoFromEntity } from './useProjectRepoFromEntity'; +import { getAnnotationValuesFromEntity } from '../utils'; export function useRepoBuilds( entity: Entity, @@ -40,10 +40,13 @@ export function useRepoBuilds( }; const api = useApi(azureDevOpsApiRef); - const { project, repo } = useProjectRepoFromEntity(entity); + const { project, repo } = getAnnotationValuesFromEntity(entity); - const { value, loading, error } = useAsync(() => { - return api.getRepoBuilds(project, repo, options); + const { value, loading, error } = useAsync(async () => { + if (repo) { + return await api.getRepoBuilds(project, repo, options); + } + return undefined; }, [api, project, repo, entity]); return { diff --git a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts new file mode 100644 index 0000000000..a34dab9db5 --- /dev/null +++ b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts @@ -0,0 +1,230 @@ +/* + * 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 { getAnnotationValuesFromEntity } from './getAnnotationValuesFromEntity'; + +describe('getAnnotationValuesFromEntity', () => { + describe('with valid project-repo annotation', () => { + it('should return project and repo', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project-repo': 'projectName/repoName', + }, + }, + }; + const { project, repo, definition, org } = + getAnnotationValuesFromEntity(entity); + expect(project).toEqual('projectName'); + expect(repo).toEqual('repoName'); + expect(definition).toEqual(undefined); + expect(org).toEqual(undefined); + }); + }); + + describe('with invalid project-repo annotation', () => { + it('should throw incorrect format error', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project-repo': 'project', + }, + }, + }; + + const test = () => { + return getAnnotationValuesFromEntity(entity); + }; + + expect(test).toThrow( + 'Value for annotation dev.azure.com/project-repo was not in the correct format: /', + ); + }); + }); + + describe('with project-repo annotation missing project', () => { + it('should throw missing project error', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project-repo': '/repo', + }, + }, + }; + + const test = () => { + return getAnnotationValuesFromEntity(entity); + }; + + expect(test).toThrow( + 'Project Name for annotation dev.azure.com/project-repo was not found; expected format is: /', + ); + }); + }); + + describe('with project-repo annotation missing repo', () => { + it('should throw missing repo error', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project-repo': 'project/', + }, + }, + }; + + const test = () => { + return getAnnotationValuesFromEntity(entity); + }; + + expect(test).toThrow( + 'Repo Name for annotation dev.azure.com/project-repo was not found; expected format is: /', + ); + }); + }); + + describe('with valid project and build-definition annotations', () => { + it('should return project and definition', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-build-definition', + annotations: { + 'dev.azure.com/project': 'projectName', + 'dev.azure.com/build-definition': 'buildDefinitionName', + }, + }, + }; + const { project, repo, definition, org } = + getAnnotationValuesFromEntity(entity); + expect(project).toEqual('projectName'); + expect(repo).toEqual(undefined); + expect(definition).toEqual('buildDefinitionName'); + expect(org).toEqual(undefined); + }); + }); + + describe('with only project annotation', () => { + it('should return project and definition', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project', + annotations: { + 'dev.azure.com/project': 'projectName', + }, + }, + }; + const test = () => { + return getAnnotationValuesFromEntity(entity); + }; + + expect(test).toThrow( + 'Value for annotation dev.azure.com/build-definition was not found', + ); + }); + }); + + describe('with only build-definition annotation', () => { + it('should return project and definition', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'build-definition', + annotations: { + 'dev.azure.com/build-definition': 'buildDefinitionName', + }, + }, + }; + const test = () => { + return getAnnotationValuesFromEntity(entity); + }; + + expect(test).toThrow( + 'Value for annotation dev.azure.com/project was not found', + ); + }); + }); + + describe('with valid project-repo and org annotations', () => { + it('should return project, repo, and org', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project-repo': 'projectName/repoName', + 'dev.azure.com/organization': 'organizationName', + }, + }, + }; + const { project, repo, definition, org } = + getAnnotationValuesFromEntity(entity); + expect(project).toEqual('projectName'); + expect(repo).toEqual('repoName'); + expect(definition).toEqual(undefined); + expect(org).toEqual('organizationName'); + }); + }); + + describe('with valid project, build-definition, and org annotations', () => { + it('should return project, definition, and org', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-build-definition', + annotations: { + 'dev.azure.com/project': 'projectName', + 'dev.azure.com/build-definition': 'buildDefinitionName', + 'dev.azure.com/organization': 'organizationName', + }, + }, + }; + const { project, repo, definition, org } = + getAnnotationValuesFromEntity(entity); + expect(project).toEqual('projectName'); + expect(repo).toEqual(undefined); + expect(definition).toEqual('buildDefinitionName'); + expect(org).toEqual('organizationName'); + }); + }); +}); diff --git a/plugins/azure-devops/src/utils/getAnnotationFromEntity.ts b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts similarity index 72% rename from plugins/azure-devops/src/utils/getAnnotationFromEntity.ts rename to plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts index 41cc1e6628..1070983b27 100644 --- a/plugins/azure-devops/src/utils/getAnnotationFromEntity.ts +++ b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts @@ -16,23 +16,28 @@ import { AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION, + AZURE_DEVOPS_ORGANIZATION_ANNOTATION, AZURE_DEVOPS_PROJECT_ANNOTATION, AZURE_DEVOPS_REPO_ANNOTATION, } from '../constants'; import { Entity } from '@backstage/catalog-model'; -export function getAnnotationFromEntity(entity: Entity): { +export function getAnnotationValuesFromEntity(entity: Entity): { project: string; repo?: string; definition?: string; + org?: string; } { + const org = + entity.metadata.annotations?.[AZURE_DEVOPS_ORGANIZATION_ANNOTATION]; + const annotation = entity.metadata.annotations?.[AZURE_DEVOPS_REPO_ANNOTATION]; if (annotation) { const { project, repo } = getProjectRepo(annotation); const definition = undefined; - return { project, repo, definition }; + return { project, repo, definition, org }; } const project = @@ -50,20 +55,32 @@ export function getAnnotationFromEntity(entity: Entity): { } const repo = undefined; - return { project, repo, definition }; + return { project, repo, definition, org }; } function getProjectRepo(annotation: string): { project: string; repo: string; } { - const [project, repo] = annotation.split('/'); - - if (!project && !repo) { + if (!annotation.includes('/')) { throw new Error( 'Value for annotation dev.azure.com/project-repo was not in the correct format: /', ); } + const [project, repo] = annotation.split('/'); + + 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/utils/index.ts b/plugins/azure-devops/src/utils/index.ts index 8655b261c3..998bb0e23d 100644 --- a/plugins/azure-devops/src/utils/index.ts +++ b/plugins/azure-devops/src/utils/index.ts @@ -17,4 +17,4 @@ export * from './arrayHas'; export * from './equalsIgnoreCase'; export * from './getDurationFromDates'; -export * from './getAnnotationFromEntity'; +export * from './getAnnotationValuesFromEntity'; From 9987f11687fe9ce0d4f95dfd131f1779f5b7d517 Mon Sep 17 00:00:00 2001 From: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Date: Sat, 26 Aug 2023 17:12:48 -0500 Subject: [PATCH 2/7] Refactor to also return host Signed-off-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> --- plugins/azure-devops/src/constants.ts | 3 +- .../getAnnotationValuesFromEntity.test.ts | 100 +++++++++++++++--- .../utils/getAnnotationValuesFromEntity.ts | 61 ++++++++--- 3 files changed, 137 insertions(+), 27 deletions(-) diff --git a/plugins/azure-devops/src/constants.ts b/plugins/azure-devops/src/constants.ts index 765f5a965b..aba0ec093a 100644 --- a/plugins/azure-devops/src/constants.ts +++ b/plugins/azure-devops/src/constants.ts @@ -16,8 +16,7 @@ export const AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION = 'dev.azure.com/build-definition'; -export const AZURE_DEVOPS_ORGANIZATION_ANNOTATION = - 'dev.azure.com/organization'; +export const AZURE_DEVOPS_HOST_ORG_ANNOTATION = 'dev.azure.com/host-org'; export const AZURE_DEVOPS_PROJECT_ANNOTATION = 'dev.azure.com/project'; export const AZURE_DEVOPS_REPO_ANNOTATION = 'dev.azure.com/project-repo'; export const AZURE_DEVOPS_DEFAULT_TOP: number = 10; diff --git a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts index a34dab9db5..5584567188 100644 --- a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts +++ b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts @@ -31,11 +31,12 @@ describe('getAnnotationValuesFromEntity', () => { }, }, }; - const { project, repo, definition, org } = + const { project, repo, definition, host, org } = getAnnotationValuesFromEntity(entity); expect(project).toEqual('projectName'); expect(repo).toEqual('repoName'); expect(definition).toEqual(undefined); + expect(host).toEqual(undefined); expect(org).toEqual(undefined); }); }); @@ -126,17 +127,18 @@ describe('getAnnotationValuesFromEntity', () => { }, }, }; - const { project, repo, definition, org } = + const { project, repo, definition, host, org } = getAnnotationValuesFromEntity(entity); expect(project).toEqual('projectName'); expect(repo).toEqual(undefined); expect(definition).toEqual('buildDefinitionName'); + expect(host).toEqual(undefined); expect(org).toEqual(undefined); }); }); describe('with only project annotation', () => { - it('should return project and definition', () => { + it('should should throw annotation not found error', () => { const entity: Entity = { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', @@ -159,7 +161,7 @@ describe('getAnnotationValuesFromEntity', () => { }); describe('with only build-definition annotation', () => { - it('should return project and definition', () => { + it('should should throw annotation not found error', () => { const entity: Entity = { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', @@ -181,8 +183,8 @@ describe('getAnnotationValuesFromEntity', () => { }); }); - describe('with valid project-repo and org annotations', () => { - it('should return project, repo, and org', () => { + describe('with valid project-repo and host-org annotations', () => { + it('should return project, repo, host, and org', () => { const entity: Entity = { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', @@ -191,21 +193,22 @@ describe('getAnnotationValuesFromEntity', () => { name: 'project-repo', annotations: { 'dev.azure.com/project-repo': 'projectName/repoName', - 'dev.azure.com/organization': 'organizationName', + 'dev.azure.com/host-org': 'hostName/organizationName', }, }, }; - const { project, repo, definition, org } = + const { project, repo, definition, host, org } = getAnnotationValuesFromEntity(entity); expect(project).toEqual('projectName'); expect(repo).toEqual('repoName'); expect(definition).toEqual(undefined); + expect(host).toEqual('hostName'); expect(org).toEqual('organizationName'); }); }); - describe('with valid project, build-definition, and org annotations', () => { - it('should return project, definition, and org', () => { + describe('with valid project, build-definition, and host-org annotations', () => { + it('should return project, definition, host and org', () => { const entity: Entity = { apiVersion: 'backstage.io/v1alpha1', kind: 'Component', @@ -215,16 +218,89 @@ describe('getAnnotationValuesFromEntity', () => { annotations: { 'dev.azure.com/project': 'projectName', 'dev.azure.com/build-definition': 'buildDefinitionName', - 'dev.azure.com/organization': 'organizationName', + 'dev.azure.com/host-org': 'hostName/organizationName', }, }, }; - const { project, repo, definition, org } = + const { project, repo, definition, host, org } = getAnnotationValuesFromEntity(entity); expect(project).toEqual('projectName'); expect(repo).toEqual(undefined); expect(definition).toEqual('buildDefinitionName'); + expect(host).toEqual('hostName'); expect(org).toEqual('organizationName'); }); }); + + describe('with invalid host-org annotation', () => { + it('should throw incorrect format error', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'host-org', + annotations: { + 'dev.azure.com/host-org': 'host', + }, + }, + }; + + const test = () => { + return getAnnotationValuesFromEntity(entity); + }; + + expect(test).toThrow( + 'Value for annotation dev.azure.com/host-org was not in the correct format: /', + ); + }); + }); + + describe('with host-rg annotation missing host', () => { + it('should throw missing project error', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'host-org', + annotations: { + 'dev.azure.com/host-org': '/org', + }, + }, + }; + + const test = () => { + return getAnnotationValuesFromEntity(entity); + }; + + expect(test).toThrow( + 'Host for annotation dev.azure.com/host-org was not found; expected format is: /', + ); + }); + }); + + describe('with host-org annotation missing org', () => { + it('should throw missing repo error', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'host-org', + annotations: { + 'dev.azure.com/host-org': 'host/', + }, + }, + }; + + const test = () => { + return getAnnotationValuesFromEntity(entity); + }; + + expect(test).toThrow( + 'Organization for annotation dev.azure.com/host-org was not found; expected format is: /', + ); + }); + }); }); diff --git a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts index 1070983b27..8ce096d3c5 100644 --- a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts +++ b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts @@ -16,7 +16,7 @@ import { AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION, - AZURE_DEVOPS_ORGANIZATION_ANNOTATION, + AZURE_DEVOPS_HOST_ORG_ANNOTATION, AZURE_DEVOPS_PROJECT_ANNOTATION, AZURE_DEVOPS_REPO_ANNOTATION, } from '../constants'; @@ -27,35 +27,39 @@ export function getAnnotationValuesFromEntity(entity: Entity): { project: string; repo?: string; definition?: string; + host?: string; org?: string; } { - const org = - entity.metadata.annotations?.[AZURE_DEVOPS_ORGANIZATION_ANNOTATION]; + const hostOrgAnnotation = + entity.metadata.annotations?.[AZURE_DEVOPS_HOST_ORG_ANNOTATION]; + const { host, org } = getHostOrg(hostOrgAnnotation); - const annotation = + const projectRepoAnnotation = entity.metadata.annotations?.[AZURE_DEVOPS_REPO_ANNOTATION]; - if (annotation) { - const { project, repo } = getProjectRepo(annotation); + if (projectRepoAnnotation) { + const { project, repo } = getProjectRepo(projectRepoAnnotation); const definition = undefined; - return { project, repo, definition, org }; + return { project, repo, definition, host, org }; } const project = entity.metadata.annotations?.[AZURE_DEVOPS_PROJECT_ANNOTATION]; if (!project) { - throw new Error('Value for annotation dev.azure.com/project was not found'); + throw new Error( + `Value for annotation ${AZURE_DEVOPS_PROJECT_ANNOTATION} was not found`, + ); } const definition = entity.metadata.annotations?.[AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION]; if (!definition) { throw new Error( - 'Value for annotation dev.azure.com/build-definition was not found', + `Value for annotation ${AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION} was not found`, ); } const repo = undefined; - return { project, repo, definition, org }; + return { project, repo, definition, host, org }; } function getProjectRepo(annotation: string): { @@ -64,7 +68,7 @@ function getProjectRepo(annotation: string): { } { if (!annotation.includes('/')) { throw new Error( - 'Value for annotation dev.azure.com/project-repo was not in the correct format: /', + `Value for annotation ${AZURE_DEVOPS_REPO_ANNOTATION} was not in the correct format: /`, ); } @@ -72,15 +76,46 @@ function getProjectRepo(annotation: string): { if (!project) { throw new Error( - 'Project Name for annotation dev.azure.com/project-repo was not found; expected format is: /', + `Project Name for annotation ${AZURE_DEVOPS_REPO_ANNOTATION} 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: /', + `Repo Name for annotation ${AZURE_DEVOPS_REPO_ANNOTATION} was not found; expected format is: /`, ); } return { project, repo }; } + +function getHostOrg(annotation?: string): { + host?: string; + org?: string; +} { + if (!annotation) { + return { host: undefined, org: undefined }; + } + + if (!annotation.includes('/')) { + throw new Error( + `Value for annotation ${AZURE_DEVOPS_HOST_ORG_ANNOTATION} was not in the correct format: /`, + ); + } + + const [host, org] = annotation.split('/'); + + if (!host) { + throw new Error( + `Host for annotation ${AZURE_DEVOPS_HOST_ORG_ANNOTATION} was not found; expected format is: /`, + ); + } + + if (!org) { + throw new Error( + `Organization for annotation ${AZURE_DEVOPS_HOST_ORG_ANNOTATION} was not found; expected format is: /`, + ); + } + + return { host, org }; +} From 904d7f12408d24e98e26df5a116cbb36be0a93c1 Mon Sep 17 00:00:00 2001 From: Andre Wanlin <67169551+awanlin@users.noreply.github.com> Date: Fri, 1 Sep 2023 12:59:49 -0500 Subject: [PATCH 3/7] Initial improvements from feedback Signed-off-by: Andre Wanlin <67169551+awanlin@users.noreply.github.com> --- .../getAnnotationValuesFromEntity.test.ts | 62 ++++++++++--------- .../utils/getAnnotationValuesFromEntity.ts | 36 ++++++----- 2 files changed, 53 insertions(+), 45 deletions(-) diff --git a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts index 5584567188..0d7a11f48b 100644 --- a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts +++ b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts @@ -31,13 +31,14 @@ describe('getAnnotationValuesFromEntity', () => { }, }, }; - const { project, repo, definition, host, org } = - getAnnotationValuesFromEntity(entity); - expect(project).toEqual('projectName'); - expect(repo).toEqual('repoName'); - expect(definition).toEqual(undefined); - expect(host).toEqual(undefined); - expect(org).toEqual(undefined); + const values = getAnnotationValuesFromEntity(entity); + expect(values).toEqual({ + project: 'projectName', + repo: 'repoName', + definition: undefined, + host: undefined, + org: undefined, + }); }); }); @@ -127,13 +128,14 @@ describe('getAnnotationValuesFromEntity', () => { }, }, }; - const { project, repo, definition, host, org } = - getAnnotationValuesFromEntity(entity); - expect(project).toEqual('projectName'); - expect(repo).toEqual(undefined); - expect(definition).toEqual('buildDefinitionName'); - expect(host).toEqual(undefined); - expect(org).toEqual(undefined); + const values = getAnnotationValuesFromEntity(entity); + expect(values).toEqual({ + project: 'projectName', + repo: undefined, + definition: 'buildDefinitionName', + host: undefined, + org: undefined, + }); }); }); @@ -197,13 +199,14 @@ describe('getAnnotationValuesFromEntity', () => { }, }, }; - const { project, repo, definition, host, org } = - getAnnotationValuesFromEntity(entity); - expect(project).toEqual('projectName'); - expect(repo).toEqual('repoName'); - expect(definition).toEqual(undefined); - expect(host).toEqual('hostName'); - expect(org).toEqual('organizationName'); + const values = getAnnotationValuesFromEntity(entity); + expect(values).toEqual({ + project: 'projectName', + repo: 'repoName', + definition: undefined, + host: 'hostName', + org: 'organizationName', + }); }); }); @@ -222,13 +225,14 @@ describe('getAnnotationValuesFromEntity', () => { }, }, }; - const { project, repo, definition, host, org } = - getAnnotationValuesFromEntity(entity); - expect(project).toEqual('projectName'); - expect(repo).toEqual(undefined); - expect(definition).toEqual('buildDefinitionName'); - expect(host).toEqual('hostName'); - expect(org).toEqual('organizationName'); + const values = getAnnotationValuesFromEntity(entity); + expect(values).toEqual({ + project: 'projectName', + repo: undefined, + definition: 'buildDefinitionName', + host: 'hostName', + org: 'organizationName', + }); }); }); @@ -256,7 +260,7 @@ describe('getAnnotationValuesFromEntity', () => { }); }); - describe('with host-rg annotation missing host', () => { + describe('with host-org annotation missing host', () => { it('should throw missing project error', () => { const entity: Entity = { apiVersion: 'backstage.io/v1alpha1', diff --git a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts index 8ce096d3c5..7099352524 100644 --- a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts +++ b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts @@ -30,16 +30,16 @@ export function getAnnotationValuesFromEntity(entity: Entity): { host?: string; org?: string; } { - const hostOrgAnnotation = - entity.metadata.annotations?.[AZURE_DEVOPS_HOST_ORG_ANNOTATION]; - const { host, org } = getHostOrg(hostOrgAnnotation); + const { host, org } = getHostOrg(entity.metadata.annotations); - const projectRepoAnnotation = - entity.metadata.annotations?.[AZURE_DEVOPS_REPO_ANNOTATION]; - if (projectRepoAnnotation) { - const { project, repo } = getProjectRepo(projectRepoAnnotation); - const definition = undefined; - return { project, repo, definition, host, org }; + const projectRepoValues = getProjectRepo(entity.metadata.annotations); + if (projectRepoValues.project && projectRepoValues.repo) { + return { + project: projectRepoValues.project, + repo: projectRepoValues.repo, + host, + org, + }; } const project = @@ -57,15 +57,18 @@ export function getAnnotationValuesFromEntity(entity: Entity): { `Value for annotation ${AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION} was not found`, ); } - - const repo = undefined; - return { project, repo, definition, host, org }; + return { project, definition, host, org }; } -function getProjectRepo(annotation: string): { - project: string; - repo: string; +function getProjectRepo(annotations?: Record): { + project?: string; + repo?: string; } { + const annotation = annotations?.[AZURE_DEVOPS_REPO_ANNOTATION]; + if (!annotation) { + return { project: undefined, repo: undefined }; + } + if (!annotation.includes('/')) { throw new Error( `Value for annotation ${AZURE_DEVOPS_REPO_ANNOTATION} was not in the correct format: /`, @@ -89,10 +92,11 @@ function getProjectRepo(annotation: string): { return { project, repo }; } -function getHostOrg(annotation?: string): { +function getHostOrg(annotations?: Record): { host?: string; org?: string; } { + const annotation = annotations?.[AZURE_DEVOPS_HOST_ORG_ANNOTATION]; if (!annotation) { return { host: undefined, org: undefined }; } From 9bc4d2b4497095da1a0142949339548c07679b8f Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Fri, 20 Oct 2023 16:05:46 -0500 Subject: [PATCH 4/7] Added EntityAzureReadmeCard to help with testing Signed-off-by: Andre Wanlin --- packages/app/src/components/catalog/EntityPage.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 81af3b91e7..ecc1f008b5 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -41,6 +41,7 @@ import { EntityAzurePullRequestsContent, isAzureDevOpsAvailable, isAzurePipelinesAvailable, + EntityAzureReadmeCard, } from '@backstage/plugin-azure-devops'; import { isOctopusDeployAvailable, @@ -415,6 +416,14 @@ const overviewContent = ( + + + + + + + + From 0a7c9ede2e7203bde1168a994972c9e57e342af4 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sat, 21 Oct 2023 10:41:32 -0500 Subject: [PATCH 5/7] Refactored to use positive return Signed-off-by: Andre Wanlin --- .../getAnnotationValuesFromEntity.test.ts | 12 ++--- .../utils/getAnnotationValuesFromEntity.ts | 54 ++++++------------- 2 files changed, 22 insertions(+), 44 deletions(-) diff --git a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts index 0d7a11f48b..a5d3725716 100644 --- a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts +++ b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts @@ -61,7 +61,7 @@ describe('getAnnotationValuesFromEntity', () => { }; expect(test).toThrow( - 'Value for annotation dev.azure.com/project-repo was not in the correct format: /', + 'Invalid value for annotation "dev.azure.com/project-repo"; expected format is: /, found: "project"', ); }); }); @@ -85,7 +85,7 @@ describe('getAnnotationValuesFromEntity', () => { }; expect(test).toThrow( - 'Project Name for annotation dev.azure.com/project-repo was not found; expected format is: /', + 'Invalid value for annotation "dev.azure.com/project-repo"; expected format is: /, found: "/repo"', ); }); }); @@ -109,7 +109,7 @@ describe('getAnnotationValuesFromEntity', () => { }; expect(test).toThrow( - 'Repo Name for annotation dev.azure.com/project-repo was not found; expected format is: /', + 'Invalid value for annotation "dev.azure.com/project-repo"; expected format is: /, found: "project/"', ); }); }); @@ -255,7 +255,7 @@ describe('getAnnotationValuesFromEntity', () => { }; expect(test).toThrow( - 'Value for annotation dev.azure.com/host-org was not in the correct format: /', + 'Invalid value for annotation "dev.azure.com/host-org"; expected format is: /, found: "host"', ); }); }); @@ -279,7 +279,7 @@ describe('getAnnotationValuesFromEntity', () => { }; expect(test).toThrow( - 'Host for annotation dev.azure.com/host-org was not found; expected format is: /', + 'Invalid value for annotation "dev.azure.com/host-org"; expected format is: /, found: "/org"', ); }); }); @@ -303,7 +303,7 @@ describe('getAnnotationValuesFromEntity', () => { }; expect(test).toThrow( - 'Organization for annotation dev.azure.com/host-org was not found; expected format is: /', + 'Invalid value for annotation "dev.azure.com/host-org"; expected format is: /, found: "host/"', ); }); }); diff --git a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts index 7099352524..99b194a834 100644 --- a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts +++ b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts @@ -69,27 +69,16 @@ function getProjectRepo(annotations?: Record): { return { project: undefined, repo: undefined }; } - if (!annotation.includes('/')) { - throw new Error( - `Value for annotation ${AZURE_DEVOPS_REPO_ANNOTATION} was not in the correct format: /`, - ); + if (annotation.includes('/')) { + const [project, repo] = annotation.split('/'); + if (project && repo) { + return { project, repo }; + } } - const [project, repo] = annotation.split('/'); - - if (!project) { - throw new Error( - `Project Name for annotation ${AZURE_DEVOPS_REPO_ANNOTATION} was not found; expected format is: /`, - ); - } - - if (!repo) { - throw new Error( - `Repo Name for annotation ${AZURE_DEVOPS_REPO_ANNOTATION} was not found; expected format is: /`, - ); - } - - return { project, repo }; + throw new Error( + `Invalid value for annotation "${AZURE_DEVOPS_REPO_ANNOTATION}"; expected format is: /, found: "${annotation}"`, + ); } function getHostOrg(annotations?: Record): { @@ -101,25 +90,14 @@ function getHostOrg(annotations?: Record): { return { host: undefined, org: undefined }; } - if (!annotation.includes('/')) { - throw new Error( - `Value for annotation ${AZURE_DEVOPS_HOST_ORG_ANNOTATION} was not in the correct format: /`, - ); + if (annotation.includes('/')) { + const [host, org] = annotation.split('/'); + if (host && org) { + return { host, org }; + } } - const [host, org] = annotation.split('/'); - - if (!host) { - throw new Error( - `Host for annotation ${AZURE_DEVOPS_HOST_ORG_ANNOTATION} was not found; expected format is: /`, - ); - } - - if (!org) { - throw new Error( - `Organization for annotation ${AZURE_DEVOPS_HOST_ORG_ANNOTATION} was not found; expected format is: /`, - ); - } - - return { host, org }; + throw new Error( + `Invalid value for annotation "${AZURE_DEVOPS_HOST_ORG_ANNOTATION}"; expected format is: /, found: "${annotation}"`, + ); } From 5578328e57908f4e0b4a809234b311688ad41fe3 Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sat, 21 Oct 2023 12:14:10 -0500 Subject: [PATCH 6/7] Added tests for changed hooks Signed-off-by: Andre Wanlin --- .../src/hooks/useGitTags.test.tsx | 130 ++++++++++++++++++ plugins/azure-devops/src/hooks/useGitTags.ts | 5 +- .../src/hooks/usePullRequests.test.tsx | 129 +++++++++++++++++ .../azure-devops/src/hooks/usePullRequests.ts | 5 +- .../src/hooks/useRepoBuilds.test.tsx | 126 +++++++++++++++++ .../azure-devops/src/hooks/useRepoBuilds.ts | 5 +- .../getAnnotationValuesFromEntity.test.ts | 4 +- .../utils/getAnnotationValuesFromEntity.ts | 4 +- 8 files changed, 392 insertions(+), 16 deletions(-) create mode 100644 plugins/azure-devops/src/hooks/useGitTags.test.tsx create mode 100644 plugins/azure-devops/src/hooks/usePullRequests.test.tsx create mode 100644 plugins/azure-devops/src/hooks/useRepoBuilds.test.tsx diff --git a/plugins/azure-devops/src/hooks/useGitTags.test.tsx b/plugins/azure-devops/src/hooks/useGitTags.test.tsx new file mode 100644 index 0000000000..1a5620e16e --- /dev/null +++ b/plugins/azure-devops/src/hooks/useGitTags.test.tsx @@ -0,0 +1,130 @@ +/* + * 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 { renderHook, waitFor } from '@testing-library/react'; +import { useGitTags } from './useGitTags'; +import { Entity } from '@backstage/catalog-model'; +import { GitTag } from '@backstage/plugin-azure-devops-common'; +import { TestApiProvider } from '@backstage/test-utils'; +import { AzureDevOpsApi, azureDevOpsApiRef } from '../api'; + +describe('useGitTags', () => { + const azureDevOpsApiMock = { + getGitTags: jest.fn(), + }; + const azureDevOpsApi = + azureDevOpsApiMock as Partial as AzureDevOpsApi; + + const Wrapper = (props: { children?: React.ReactNode }) => ( + + {props.children} + + ); + + it('should provide an array of GitTag', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project-repo': 'projectName/repoName', + }, + }, + }; + const tags: GitTag[] = [ + { + objectId: 'tag-1', + peeledObjectId: 'tag-1', + name: 'tag-1', + createdBy: 'awanlin', + link: 'https://dev.azure.com/org/project/repo', + commitLink: 'https://dev.azure.com/org/project/repo/commit-sha-1', + }, + { + objectId: 'tag-2', + peeledObjectId: 'tag-2', + name: 'tag-2', + createdBy: 'awanlin', + link: 'https://dev.azure.com/org/project/repo', + commitLink: 'https://dev.azure.com/org/project/repo/commit-sha-2', + }, + { + objectId: 'tag-3', + peeledObjectId: 'tag-3', + name: 'tag-3', + createdBy: 'awanlin', + link: 'https://dev.azure.com/org/project/repo', + commitLink: 'https://dev.azure.com/org/project/repo/commit-sha-3', + }, + ]; + azureDevOpsApiMock.getGitTags.mockResolvedValue({ items: tags }); + const { result } = renderHook(() => useGitTags(entity), { + wrapper: Wrapper, + }); + + expect(result.current.loading).toEqual(true); + + await waitFor(() => { + expect(result.current).toEqual({ + error: undefined, + items: tags, + loading: false, + }); + }); + }); + + it('should return throw when annotation missing', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + }, + }; + + expect(() => + renderHook(() => useGitTags(entity), { + wrapper: Wrapper, + }), + ).toThrow('Value for annotation "dev.azure.com/project" was not found'); + }); + + it('should return throw when annotation invalid', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project-repo': 'fake', + }, + }, + }; + + expect(() => + renderHook(() => useGitTags(entity), { + wrapper: Wrapper, + }), + ).toThrow( + 'Invalid value for annotation "dev.azure.com/project-repo"; expected format is: /, found: "fake"', + ); + }); +}); diff --git a/plugins/azure-devops/src/hooks/useGitTags.ts b/plugins/azure-devops/src/hooks/useGitTags.ts index f36f9224aa..13c7118934 100644 --- a/plugins/azure-devops/src/hooks/useGitTags.ts +++ b/plugins/azure-devops/src/hooks/useGitTags.ts @@ -31,10 +31,7 @@ export function useGitTags(entity: Entity): { const { project, repo } = getAnnotationValuesFromEntity(entity); const { value, loading, error } = useAsync(async () => { - if (repo) { - return await api.getGitTags(project, repo); - } - return undefined; + return await api.getGitTags(project, repo as string); }, [api, project, repo]); return { diff --git a/plugins/azure-devops/src/hooks/usePullRequests.test.tsx b/plugins/azure-devops/src/hooks/usePullRequests.test.tsx new file mode 100644 index 0000000000..3e51c003e6 --- /dev/null +++ b/plugins/azure-devops/src/hooks/usePullRequests.test.tsx @@ -0,0 +1,129 @@ +/* + * 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 { renderHook, waitFor } from '@testing-library/react'; +import { Entity } from '@backstage/catalog-model'; +import { PullRequest } from '@backstage/plugin-azure-devops-common'; +import { TestApiProvider } from '@backstage/test-utils'; +import { AzureDevOpsApi, azureDevOpsApiRef } from '../api'; +import { usePullRequests } from './usePullRequests'; + +describe('usePullRequests', () => { + const azureDevOpsApiMock = { + getPullRequests: jest.fn(), + }; + const azureDevOpsApi = + azureDevOpsApiMock as Partial as AzureDevOpsApi; + + const Wrapper = (props: { children?: React.ReactNode }) => ( + + {props.children} + + ); + + it('should provide an array of PullRequest', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project-repo': 'projectName/repoName', + }, + }, + }; + const pullRequests: PullRequest[] = [ + { + pullRequestId: 1, + repoName: 'repo', + title: 'title-1', + createdBy: 'awanlin', + link: 'https://dev.azure.com/org/project/repo', + }, + { + pullRequestId: 2, + repoName: 'repo', + title: 'title-2', + createdBy: 'awanlin', + link: 'https://dev.azure.com/org/project/repo', + }, + { + pullRequestId: 3, + repoName: 'repo', + title: 'title-3', + createdBy: 'awanlin', + link: 'https://dev.azure.com/org/project/repo', + }, + ]; + azureDevOpsApiMock.getPullRequests.mockResolvedValue({ + items: pullRequests, + }); + const { result } = renderHook(() => usePullRequests(entity), { + wrapper: Wrapper, + }); + + expect(result.current.loading).toEqual(true); + + await waitFor(() => { + expect(result.current).toEqual({ + error: undefined, + items: pullRequests, + loading: false, + }); + }); + }); + + it('should return throw when annotation missing', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + }, + }; + + expect(() => + renderHook(() => usePullRequests(entity), { + wrapper: Wrapper, + }), + ).toThrow('Value for annotation "dev.azure.com/project" was not found'); + }); + + it('should return throw when annotation invalid', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project-repo': 'fake', + }, + }, + }; + + expect(() => + renderHook(() => usePullRequests(entity), { + wrapper: Wrapper, + }), + ).toThrow( + 'Invalid value for annotation "dev.azure.com/project-repo"; expected format is: /, found: "fake"', + ); + }); +}); diff --git a/plugins/azure-devops/src/hooks/usePullRequests.ts b/plugins/azure-devops/src/hooks/usePullRequests.ts index c412836ae5..c4223871f2 100644 --- a/plugins/azure-devops/src/hooks/usePullRequests.ts +++ b/plugins/azure-devops/src/hooks/usePullRequests.ts @@ -47,10 +47,7 @@ export function usePullRequests( const { project, repo } = getAnnotationValuesFromEntity(entity); const { value, loading, error } = useAsync(async () => { - if (repo) { - return await api.getPullRequests(project, repo, options); - } - return undefined; + return await api.getPullRequests(project, repo as string, options); }, [api, project, repo, top, status]); return { diff --git a/plugins/azure-devops/src/hooks/useRepoBuilds.test.tsx b/plugins/azure-devops/src/hooks/useRepoBuilds.test.tsx new file mode 100644 index 0000000000..99eaeb1ce3 --- /dev/null +++ b/plugins/azure-devops/src/hooks/useRepoBuilds.test.tsx @@ -0,0 +1,126 @@ +/* + * 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 { renderHook, waitFor } from '@testing-library/react'; +import { Entity } from '@backstage/catalog-model'; +import { RepoBuild } from '@backstage/plugin-azure-devops-common'; +import { TestApiProvider } from '@backstage/test-utils'; +import { AzureDevOpsApi, azureDevOpsApiRef } from '../api'; +import { useRepoBuilds } from './useRepoBuilds'; + +describe('useRepoBuilds', () => { + const azureDevOpsApiMock = { + getRepoBuilds: jest.fn(), + }; + const azureDevOpsApi = + azureDevOpsApiMock as Partial as AzureDevOpsApi; + + const Wrapper = (props: { children?: React.ReactNode }) => ( + + {props.children} + + ); + + it('should provide an array of RepoBuild', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project-repo': 'projectName/repoName', + }, + }, + }; + const repoBuilds: RepoBuild[] = [ + { + id: 1, + title: 'title-1', + source: 'awanlin', + link: 'https://dev.azure.com/org/project/repo', + }, + { + id: 2, + title: 'title-2', + source: 'awanlin', + link: 'https://dev.azure.com/org/project/repo', + }, + { + id: 3, + title: 'title-3', + source: 'awanlin', + link: 'https://dev.azure.com/org/project/repo', + }, + ]; + azureDevOpsApiMock.getRepoBuilds.mockResolvedValue({ + items: repoBuilds, + }); + const { result } = renderHook(() => useRepoBuilds(entity), { + wrapper: Wrapper, + }); + + expect(result.current.loading).toEqual(true); + + await waitFor(() => { + expect(result.current).toEqual({ + error: undefined, + items: repoBuilds, + loading: false, + }); + }); + }); + + it('should return throw when annotation missing', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + }, + }; + + expect(() => + renderHook(() => useRepoBuilds(entity), { + wrapper: Wrapper, + }), + ).toThrow('Value for annotation "dev.azure.com/project" was not found'); + }); + + it('should return throw when annotation invalid', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project-repo': 'fake', + }, + }, + }; + + expect(() => + renderHook(() => useRepoBuilds(entity), { + wrapper: Wrapper, + }), + ).toThrow( + 'Invalid value for annotation "dev.azure.com/project-repo"; expected format is: /, found: "fake"', + ); + }); +}); diff --git a/plugins/azure-devops/src/hooks/useRepoBuilds.ts b/plugins/azure-devops/src/hooks/useRepoBuilds.ts index 4ba88bd5fb..9292a6e035 100644 --- a/plugins/azure-devops/src/hooks/useRepoBuilds.ts +++ b/plugins/azure-devops/src/hooks/useRepoBuilds.ts @@ -43,10 +43,7 @@ export function useRepoBuilds( const { project, repo } = getAnnotationValuesFromEntity(entity); const { value, loading, error } = useAsync(async () => { - if (repo) { - return await api.getRepoBuilds(project, repo, options); - } - return undefined; + return await api.getRepoBuilds(project, repo as string, options); }, [api, project, repo, entity]); return { diff --git a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts index a5d3725716..67e46cee7b 100644 --- a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts +++ b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.test.ts @@ -157,7 +157,7 @@ describe('getAnnotationValuesFromEntity', () => { }; expect(test).toThrow( - 'Value for annotation dev.azure.com/build-definition was not found', + 'Value for annotation "dev.azure.com/build-definition" was not found', ); }); }); @@ -180,7 +180,7 @@ describe('getAnnotationValuesFromEntity', () => { }; expect(test).toThrow( - 'Value for annotation dev.azure.com/project was not found', + 'Value for annotation "dev.azure.com/project" was not found', ); }); }); diff --git a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts index 99b194a834..9feec8bf95 100644 --- a/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts +++ b/plugins/azure-devops/src/utils/getAnnotationValuesFromEntity.ts @@ -46,7 +46,7 @@ export function getAnnotationValuesFromEntity(entity: Entity): { entity.metadata.annotations?.[AZURE_DEVOPS_PROJECT_ANNOTATION]; if (!project) { throw new Error( - `Value for annotation ${AZURE_DEVOPS_PROJECT_ANNOTATION} was not found`, + `Value for annotation "${AZURE_DEVOPS_PROJECT_ANNOTATION}" was not found`, ); } @@ -54,7 +54,7 @@ export function getAnnotationValuesFromEntity(entity: Entity): { entity.metadata.annotations?.[AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION]; if (!definition) { throw new Error( - `Value for annotation ${AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION} was not found`, + `Value for annotation "${AZURE_DEVOPS_BUILD_DEFINITION_ANNOTATION}" was not found`, ); } return { project, definition, host, org }; From c0bb5117955f0f203d5be00e31462042c5bfb75b Mon Sep 17 00:00:00 2001 From: Andre Wanlin Date: Sat, 21 Oct 2023 15:18:30 -0500 Subject: [PATCH 7/7] Refactor Readme into hook Signed-off-by: Andre Wanlin --- .../src/components/ReadmeCard/ReadmeCard.tsx | 15 +-- plugins/azure-devops/src/hooks/index.ts | 1 + .../azure-devops/src/hooks/useReadme.test.tsx | 108 ++++++++++++++++++ plugins/azure-devops/src/hooks/useReadme.ts | 42 +++++++ 4 files changed, 154 insertions(+), 12 deletions(-) create mode 100644 plugins/azure-devops/src/hooks/useReadme.test.tsx create mode 100644 plugins/azure-devops/src/hooks/useReadme.ts diff --git a/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx b/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx index d9afe8db5a..420fe6ecf4 100644 --- a/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx +++ b/plugins/azure-devops/src/components/ReadmeCard/ReadmeCard.tsx @@ -23,11 +23,9 @@ import { ErrorPanel, } from '@backstage/core-components'; import { useEntity } from '@backstage/plugin-catalog-react'; -import { useApi } from '@backstage/core-plugin-api'; import React from 'react'; -import { azureDevOpsApiRef } from '../../api'; -import useAsync from 'react-use/lib/useAsync'; -import { getAnnotationValuesFromEntity } from '../../utils'; + +import { useReadme } from '../../hooks'; const useStyles = makeStyles(theme => ({ readMe: { @@ -85,16 +83,9 @@ const ReadmeCardError = ({ error }: ErrorProps) => { export const ReadmeCard = (props: Props) => { const classes = useStyles(); - const api = useApi(azureDevOpsApiRef); const { entity } = useEntity(); - const { project, repo } = getAnnotationValuesFromEntity(entity); - const { loading, error, value } = useAsync(async () => { - if (repo) { - return await api.getReadme({ project, repo }); - } - return undefined; - }, [api, project, repo, entity]); + const { loading, error, item: value } = useReadme(entity); if (loading) { return ; diff --git a/plugins/azure-devops/src/hooks/index.ts b/plugins/azure-devops/src/hooks/index.ts index 0b745ba586..ab1a3c9998 100644 --- a/plugins/azure-devops/src/hooks/index.ts +++ b/plugins/azure-devops/src/hooks/index.ts @@ -20,3 +20,4 @@ export * from './usePullRequests'; export * from './useRepoBuilds'; export * from './useUserEmail'; export * from './useUserTeamIds'; +export * from './useReadme'; diff --git a/plugins/azure-devops/src/hooks/useReadme.test.tsx b/plugins/azure-devops/src/hooks/useReadme.test.tsx new file mode 100644 index 0000000000..ffc33c136b --- /dev/null +++ b/plugins/azure-devops/src/hooks/useReadme.test.tsx @@ -0,0 +1,108 @@ +/* + * 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 { renderHook, waitFor } from '@testing-library/react'; +import { Entity } from '@backstage/catalog-model'; +import { Readme } from '@backstage/plugin-azure-devops-common'; +import { TestApiProvider } from '@backstage/test-utils'; +import { AzureDevOpsApi, azureDevOpsApiRef } from '../api'; +import { useReadme } from './useReadme'; + +describe('useReadme', () => { + const azureDevOpsApiMock = { + getReadme: jest.fn(), + }; + const azureDevOpsApi = + azureDevOpsApiMock as Partial as AzureDevOpsApi; + + const Wrapper = (props: { children?: React.ReactNode }) => ( + + {props.children} + + ); + + it('should provide a Readme', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project-repo': 'projectName/repoName', + }, + }, + }; + const readme: Readme = { + url: 'https://dev.azure.com/org/project/repo', + content: 'This is some fake README content', + }; + azureDevOpsApiMock.getReadme.mockResolvedValue({ + item: readme, + }); + const { result } = renderHook(() => useReadme(entity), { + wrapper: Wrapper, + }); + + expect(result.current.loading).toEqual(true); + + await waitFor(() => { + expect(result.current.item).toEqual({ + item: readme, + }); + }); + }); + + it('should return throw when annotation missing', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + }, + }; + + expect(() => + renderHook(() => useReadme(entity), { + wrapper: Wrapper, + }), + ).toThrow('Value for annotation "dev.azure.com/project" was not found'); + }); + + it('should return throw when annotation invalid', async () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + namespace: 'default', + name: 'project-repo', + annotations: { + 'dev.azure.com/project-repo': 'fake', + }, + }, + }; + + expect(() => + renderHook(() => useReadme(entity), { + wrapper: Wrapper, + }), + ).toThrow( + 'Invalid value for annotation "dev.azure.com/project-repo"; expected format is: /, found: "fake"', + ); + }); +}); diff --git a/plugins/azure-devops/src/hooks/useReadme.ts b/plugins/azure-devops/src/hooks/useReadme.ts new file mode 100644 index 0000000000..60b2e21990 --- /dev/null +++ b/plugins/azure-devops/src/hooks/useReadme.ts @@ -0,0 +1,42 @@ +/* + * 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 { Readme } from '@backstage/plugin-azure-devops-common'; + +import { Entity } from '@backstage/catalog-model'; +import { azureDevOpsApiRef } from '../api'; +import { useApi } from '@backstage/core-plugin-api'; +import useAsync from 'react-use/lib/useAsync'; +import { getAnnotationValuesFromEntity } from '../utils'; + +export function useReadme(entity: Entity): { + item?: Readme; + loading: boolean; + error?: Error; +} { + const api = useApi(azureDevOpsApiRef); + const { project, repo } = getAnnotationValuesFromEntity(entity); + + const { value, loading, error } = useAsync(async () => { + return await api.getReadme({ project, repo: repo as string }); + }, [api, project, repo]); + + return { + item: value, + loading, + error, + }; +}