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,
+ };
+}