From 8cb5d89e3748355cfd0901ec30b8c081d08a84da Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 30 Sep 2021 11:33:42 +0200 Subject: [PATCH 01/21] chore: starting to add EntityAncestorsProcessingErrorsPanel Signed-off-by: blam --- packages/catalog-client/src/CatalogClient.ts | 20 +++++ packages/catalog-client/src/types/api.ts | 15 ++++ .../EntityProcessingErrorsPanel.tsx | 79 +++++++++++++++---- .../components/EntitySwitch/EntitySwitch.tsx | 2 +- 4 files changed, 100 insertions(+), 16 deletions(-) diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index 1a6bc5c70d..7e1b4d8010 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -33,6 +33,8 @@ import { CatalogEntitiesRequest, CatalogListResponse, CatalogRequestOptions, + CatalogEntityAncestorsRequest, + CatalogEntityAncestorsResponse, } from './types/api'; import { DiscoveryApi } from './types/discovery'; @@ -44,6 +46,24 @@ export class CatalogClient implements CatalogApi { this.discoveryApi = options.discoveryApi; } + async getEntityAncestors( + request: CatalogEntityAncestorsRequest, + options?: CatalogRequestOptions, + ): Promise { + const { kind, namespace, name } = Object.fromEntries( + Object.entries(request.entityName).map(([k, v]) => [ + k, + encodeURIComponent(v), + ]), + ); + + return await this.requestRequired( + 'GET', + `/entity/by-name/${kind}/${namespace}/${name}/ancestry`, + options, + ); + } + async getLocationById( id: string, options?: CatalogRequestOptions, diff --git a/packages/catalog-client/src/types/api.ts b/packages/catalog-client/src/types/api.ts index 679588b10a..609d67eac1 100644 --- a/packages/catalog-client/src/types/api.ts +++ b/packages/catalog-client/src/types/api.ts @@ -28,6 +28,17 @@ export type CatalogEntitiesRequest = { fields?: string[] | undefined; }; +/** @public */ +export type CatalogEntityAncestorsRequest = { + entityName: EntityName; +}; + +/** @public */ +export type CatalogEntityAncestorsResponse = { + root: EntityName; + items: { entity: Entity; parents: EntityName[] }[]; +}; + /** @public */ export type CatalogListResponse = { items: T[]; @@ -45,6 +56,10 @@ export interface CatalogApi { request?: CatalogEntitiesRequest, options?: CatalogRequestOptions, ): Promise>; + getEntityAncestors( + request: CatalogEntityAncestorsRequest, + options?: CatalogRequestOptions, + ): Promise; getEntityByName( name: EntityName, options?: CatalogRequestOptions, diff --git a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx index 833ca74daa..9649edd6de 100644 --- a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx +++ b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx @@ -14,20 +14,30 @@ * limitations under the License. */ -import { Entity, UNSTABLE_EntityStatusItem } from '@backstage/catalog-model'; -import { useEntity } from '@backstage/plugin-catalog-react'; +import { + Entity, + getEntityName, + UNSTABLE_EntityStatusItem, +} from '@backstage/catalog-model'; +import { catalogApiRef, useEntity } from '@backstage/plugin-catalog-react'; import { Box } from '@material-ui/core'; import React from 'react'; import { ResponseErrorPanel } from '@backstage/core-components'; import { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from '@backstage/catalog-client'; +import { useApi } from '@backstage/core-plugin-api'; +import { useAsync } from 'react-use'; -const errorfilter = (i: UNSTABLE_EntityStatusItem) => +const errorFilter = (i: UNSTABLE_EntityStatusItem) => i.error && i.level === 'error' && i.type === ENTITY_STATUS_CATALOG_PROCESSING_TYPE; -export const hasCatalogProcessingErrors = (entity: Entity) => - entity?.status?.items?.filter(errorfilter).length! > 0; +export const hasCatalogProcessingErrors = async (entity: Entity) => { + return entity?.status?.items?.filter(errorFilter).length! > 0; + const catalogApi = useApi(catalogApiRef); + const { status } = await catalogApi.getEntityStatus(entity.id); + return status.some(errorFilter); +}; /** * Displays a list of errors if the entity is invalid. @@ -36,16 +46,55 @@ export const EntityProcessingErrorsPanel = () => { const { entity } = useEntity(); const catalogProcessingErrors = (entity?.status?.items?.filter( - errorfilter, + errorFilter, ) as Required[]) || []; - return ( - <> - {catalogProcessingErrors.map(({ error }, index) => ( - - - - ))} - - ); + return catalogProcessingErrors.map(({ error }, index) => ( + + + + )); +}; + +/** + * Displays a list of errors from the ancestors of the current entity. + */ +export const EntityAncestorsProcessingErrorsPanel = () => { + const { entity } = useEntity(); + const catalogApi = useApi(catalogApiRef); + + const { loading, value, error } = useAsync(() => + catalogApi.getEntityAncestors({ entityName: getEntityName(entity) }), + ); + + if (loading) { + return null; + } + + if (error) { + return ( + + + + ); + } + + const ancestorErrors = + value?.items.flatMap(({ entity: ancestor }) => + ancestor?.status?.items?.filter?.(errorFilter), + ) ?? []; + + if (ancestorErrors.length === 0) { + return null; + } + + const filteredAncestors = ancestorErrors.filter( + (e): e is UNSTABLE_EntityStatusItem => Boolean(e), + ); + + return filteredAncestors.map(({ error: ancestorError }, index) => ( + + + + )); }; diff --git a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx index 42898da018..fd6f7227cc 100644 --- a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx +++ b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx @@ -32,7 +32,7 @@ const EntitySwitchCase = (_: { attachComponentData(EntitySwitchCase, ENTITY_SWITCH_KEY, true); type SwitchCase = { - if?: (entity: Entity) => boolean; + if?: (entity: Entity) => boolean | Promise; children: JSX.Element; }; From fa6bc2aa97a1659f52ad391ebe5ca96b000c96c8 Mon Sep 17 00:00:00 2001 From: blam Date: Thu, 30 Sep 2021 11:38:25 +0200 Subject: [PATCH 02/21] chore: add some comments on what's left to do Signed-off-by: blam --- .../EntityProcessingErrorsPanel.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx index 9649edd6de..a5adde3c9a 100644 --- a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx +++ b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx @@ -33,10 +33,8 @@ const errorFilter = (i: UNSTABLE_EntityStatusItem) => i.type === ENTITY_STATUS_CATALOG_PROCESSING_TYPE; export const hasCatalogProcessingErrors = async (entity: Entity) => { + // go grab ancestors and check all items, not just the current entity return entity?.status?.items?.filter(errorFilter).length! > 0; - const catalogApi = useApi(catalogApiRef); - const { status } = await catalogApi.getEntityStatus(entity.id); - return status.some(errorFilter); }; /** @@ -60,6 +58,7 @@ export const EntityProcessingErrorsPanel = () => { * Displays a list of errors from the ancestors of the current entity. */ export const EntityAncestorsProcessingErrorsPanel = () => { + // move this logic into the existing component const { entity } = useEntity(); const catalogApi = useApi(catalogApiRef); From e6225ac290ff612961092421938525f7ca19b0e4 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 1 Oct 2021 13:03:03 +0200 Subject: [PATCH 03/21] Export CatalogAncestor types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: Ben Lambert Co-authored-by: Patrik Oldsberg Signed-off-by: Johan Haals --- packages/catalog-client/src/types/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/catalog-client/src/types/index.ts b/packages/catalog-client/src/types/index.ts index 7963891a4c..3552a72e3c 100644 --- a/packages/catalog-client/src/types/index.ts +++ b/packages/catalog-client/src/types/index.ts @@ -21,6 +21,8 @@ export type { CatalogEntitiesRequest, CatalogListResponse, CatalogRequestOptions, + CatalogEntityAncestorsRequest, + CatalogEntityAncestorsResponse, } from './api'; export type { DiscoveryApi } from './discovery'; export { CATALOG_FILTER_EXISTS } from './api'; From 3cf068a4b1c1f7167369ae41b9041056c18a1e39 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 1 Oct 2021 13:04:54 +0200 Subject: [PATCH 04/21] catalog-client: Fix getEntityAncestors request path Signed-off-by: Johan Haals --- packages/catalog-client/src/CatalogClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index 7e1b4d8010..e0314b9eb8 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -59,7 +59,7 @@ export class CatalogClient implements CatalogApi { return await this.requestRequired( 'GET', - `/entity/by-name/${kind}/${namespace}/${name}/ancestry`, + `/entities/by-name/${kind}/${namespace}/${name}/ancestry`, options, ); } From a0c6ae89b19e20f9ca7617764e3b5d3c927a669e Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 1 Oct 2021 13:05:32 +0200 Subject: [PATCH 05/21] Add getEntityAncestors to CatalogClientWrapper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: Ben Lambert Co-authored-by: Patrik Oldsberg Signed-off-by: Johan Haals --- plugins/catalog/src/CatalogClientWrapper.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/plugins/catalog/src/CatalogClientWrapper.ts b/plugins/catalog/src/CatalogClientWrapper.ts index 46ca8ec0f9..0463cbf30e 100644 --- a/plugins/catalog/src/CatalogClientWrapper.ts +++ b/plugins/catalog/src/CatalogClientWrapper.ts @@ -23,6 +23,8 @@ import { CatalogEntitiesRequest, CatalogListResponse, CatalogRequestOptions, + CatalogEntityAncestorsRequest, + CatalogEntityAncestorsResponse, } from '@backstage/catalog-client'; import { IdentityApi } from '@backstage/core-plugin-api'; @@ -118,4 +120,13 @@ export class CatalogClientWrapper implements CatalogApi { token: options?.token ?? (await this.identityApi.getIdToken()), }); } + + async getEntityAncestors( + request: CatalogEntityAncestorsRequest, + options?: CatalogRequestOptions, + ): Promise { + return await this.client.getEntityAncestors(request, { + token: options?.token ?? (await this.identityApi.getIdToken()), + }); + } } From 672d6ed01a95fca0da1e1477288c1324bd060da1 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 1 Oct 2021 13:06:48 +0200 Subject: [PATCH 06/21] Add async support to entitySwitch if statements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Co-authored-by: Ben Lambert Co-authored-by: Patrik Oldsberg Signed-off-by: Johan Haals --- .../components/EntitySwitch/EntitySwitch.tsx | 41 ++++++++++++++++--- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx index fd6f7227cc..201e884f43 100644 --- a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx +++ b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx @@ -19,25 +19,35 @@ import { useEntity } from '@backstage/plugin-catalog-react'; import { PropsWithChildren, ReactNode } from 'react'; import { attachComponentData, + useApiHolder, useElementFilter, + ApiHolder, } from '@backstage/core-plugin-api'; +import { useAsync } from 'react-use'; const ENTITY_SWITCH_KEY = 'core.backstage.entitySwitch'; const EntitySwitchCase = (_: { - if?: (entity: Entity) => boolean; + if?: ( + entity: Entity, + context: { apiRegistry: ApiHolder }, + ) => boolean | Promise; children: ReactNode; }) => null; attachComponentData(EntitySwitchCase, ENTITY_SWITCH_KEY, true); type SwitchCase = { - if?: (entity: Entity) => boolean | Promise; + if?: ( + entity: Entity, + context: { apiRegistry: ApiHolder }, + ) => boolean | Promise; children: JSX.Element; }; export const EntitySwitch = ({ children }: PropsWithChildren<{}>) => { const { entity } = useEntity(); + const apiRegistry = useApiHolder(); const switchCases = useElementFilter(children, collection => collection .selectByComponentData({ @@ -51,10 +61,29 @@ export const EntitySwitch = ({ children }: PropsWithChildren<{}>) => { }), ); - const matchingCase = switchCases.find(switchCase => - switchCase.if ? switchCase.if(entity) : true, - ); - return matchingCase?.children ?? null; + const { loading, value } = useAsync(async () => { + const promises = switchCases.map( + async ({ if: condition, children: output }) => { + if (!condition) { + return output; + } + + try { + const matches = await condition(entity, { apiRegistry }); + return matches ? output : null; + } catch { + return null; + } + }, + ); + return (await Promise.all(promises)).find(Boolean) ?? null; + }, [switchCases]); + + if (loading || !value) { + return null; + } + + return value; }; EntitySwitch.Case = EntitySwitchCase; From 2ea14f0bcfff9a23f6d19fa57524311ab9de9033 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 1 Oct 2021 13:32:22 +0200 Subject: [PATCH 07/21] Display errors from ancestors Signed-off-by: Johan Haals --- .../src/next/NextEntitiesCatalog.ts | 1 - .../EntityProcessingErrorsPanel.tsx | 115 +++++++++++++----- 2 files changed, 85 insertions(+), 31 deletions(-) diff --git a/plugins/catalog-backend/src/next/NextEntitiesCatalog.ts b/plugins/catalog-backend/src/next/NextEntitiesCatalog.ts index 14fda4ea24..f8c244cbfd 100644 --- a/plugins/catalog-backend/src/next/NextEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/next/NextEntitiesCatalog.ts @@ -189,7 +189,6 @@ export class NextEntitiesCatalog implements EntitiesCatalog { current = todo.pop() ) { const currentRef = stringifyEntityRef(current); - seenEntityRefs.add(currentRef); const parentRows = await this.database( 'refresh_state_references', diff --git a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx index a5adde3c9a..f4636f26db 100644 --- a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx +++ b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx @@ -16,7 +16,9 @@ import { Entity, + EntityName, getEntityName, + stringifyEntityRef, UNSTABLE_EntityStatusItem, } from '@backstage/catalog-model'; import { catalogApiRef, useEntity } from '@backstage/plugin-catalog-react'; @@ -24,34 +26,96 @@ import { Box } from '@material-ui/core'; import React from 'react'; import { ResponseErrorPanel } from '@backstage/core-components'; import { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from '@backstage/catalog-client'; -import { useApi } from '@backstage/core-plugin-api'; +import { useApi, ApiHolder } from '@backstage/core-plugin-api'; import { useAsync } from 'react-use'; +import { CatalogApi } from '@backstage/catalog-client'; +import { SerializedError } from '@backstage/errors'; const errorFilter = (i: UNSTABLE_EntityStatusItem) => i.error && i.level === 'error' && i.type === ENTITY_STATUS_CATALOG_PROCESSING_TYPE; -export const hasCatalogProcessingErrors = async (entity: Entity) => { - // go grab ancestors and check all items, not just the current entity - return entity?.status?.items?.filter(errorFilter).length! > 0; +async function getOwnAndAncestorsErrors( + entityName: EntityName, + catalogApi: CatalogApi, +): Promise { + const ancestors = await catalogApi.getEntityAncestors({ entityName }); + return ancestors.items.flatMap(item => { + const statuses = item.entity.status?.items ?? []; + return statuses + .filter(errorFilter) + .map(e => e.error) + .filter((e): e is SerializedError => Boolean(e)); + }); +} + +export const hasCatalogProcessingErrors = async ( + entity: Entity, + context: { apiRegistry: ApiHolder }, +) => { + const catalogApi = context.apiRegistry.get(catalogApiRef); + if (!catalogApi) { + throw new Error(`No implementation available for ${catalogApiRef}`); + } + + const errors = await getOwnAndAncestorsErrors( + getEntityName(entity), + catalogApi, + ); + return errors.length > 0; }; /** * Displays a list of errors if the entity is invalid. */ export const EntityProcessingErrorsPanel = () => { + /* const { entity } = useEntity(); const catalogProcessingErrors = (entity?.status?.items?.filter( errorFilter, ) as Required[]) || []; - return catalogProcessingErrors.map(({ error }, index) => ( - - - - )); + return ( + <> + {catalogProcessingErrors.map(({ error }, index) => ( + + + + ))} + + ); + */ + // move this logic into the existing component + const { entity } = useEntity(); + const catalogApi = useApi(catalogApiRef); + + const { loading, error, value } = useAsync(() => { + return getOwnAndAncestorsErrors(getEntityName(entity), catalogApi); + }, [stringifyEntityRef(entity), catalogApi]); + + if (error) { + return ( + + + + ); + } + + if (loading || !value?.length) { + return null; + } + + return ( + <> + {value.map((ancestorError, index) => ( + + + + ))} + + ); }; /** @@ -62,13 +126,9 @@ export const EntityAncestorsProcessingErrorsPanel = () => { const { entity } = useEntity(); const catalogApi = useApi(catalogApiRef); - const { loading, value, error } = useAsync(() => - catalogApi.getEntityAncestors({ entityName: getEntityName(entity) }), - ); - - if (loading) { - return null; - } + const { loading, error, value } = useAsync(() => { + return getOwnAndAncestorsErrors(getEntityName(entity), catalogApi); + }, [stringifyEntityRef(entity), catalogApi]); if (error) { return ( @@ -78,22 +138,17 @@ export const EntityAncestorsProcessingErrorsPanel = () => { ); } - const ancestorErrors = - value?.items.flatMap(({ entity: ancestor }) => - ancestor?.status?.items?.filter?.(errorFilter), - ) ?? []; - - if (ancestorErrors.length === 0) { + if (loading || !value?.length) { return null; } - const filteredAncestors = ancestorErrors.filter( - (e): e is UNSTABLE_EntityStatusItem => Boolean(e), + return ( + <> + {value.map((ancestorError, index) => ( + + + + ))} + ); - - return filteredAncestors.map(({ error: ancestorError }, index) => ( - - - - )); }; From 90b5f43c5a10525cef3a73c9a1b861cfab8a176e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 1 Oct 2021 15:18:42 +0200 Subject: [PATCH 08/21] catalog: cleanup + rename entityName -> entityRef, apiRegistry -> apis Signed-off-by: Patrik Oldsberg --- packages/catalog-client/src/CatalogClient.ts | 2 +- packages/catalog-client/src/types/api.ts | 2 +- plugins/catalog/package.json | 3 +- .../EntityProcessingErrorsPanel.tsx | 71 +++---------------- .../components/EntitySwitch/EntitySwitch.tsx | 8 +-- 5 files changed, 18 insertions(+), 68 deletions(-) diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index e0314b9eb8..3a4c0455ad 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -51,7 +51,7 @@ export class CatalogClient implements CatalogApi { options?: CatalogRequestOptions, ): Promise { const { kind, namespace, name } = Object.fromEntries( - Object.entries(request.entityName).map(([k, v]) => [ + Object.entries(request.entityRef).map(([k, v]) => [ k, encodeURIComponent(v), ]), diff --git a/packages/catalog-client/src/types/api.ts b/packages/catalog-client/src/types/api.ts index 609d67eac1..234c49c779 100644 --- a/packages/catalog-client/src/types/api.ts +++ b/packages/catalog-client/src/types/api.ts @@ -30,7 +30,7 @@ export type CatalogEntitiesRequest = { /** @public */ export type CatalogEntityAncestorsRequest = { - entityName: EntityName; + entityRef: EntityName; }; /** @public */ diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 8c55c606b1..c2b8aaf4d6 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -37,6 +37,7 @@ "@backstage/core-plugin-api": "^0.1.9", "@backstage/integration-react": "^0.1.11", "@backstage/plugin-catalog-react": "^0.5.1", + "@backstage/errors": "^0.1.2", "@backstage/theme": "^0.2.10", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -63,4 +64,4 @@ "files": [ "dist" ] -} +} \ No newline at end of file diff --git a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx index f4636f26db..9e7ea20f85 100644 --- a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx +++ b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx @@ -25,10 +25,12 @@ import { catalogApiRef, useEntity } from '@backstage/plugin-catalog-react'; import { Box } from '@material-ui/core'; import React from 'react'; import { ResponseErrorPanel } from '@backstage/core-components'; -import { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from '@backstage/catalog-client'; +import { + CatalogApi, + ENTITY_STATUS_CATALOG_PROCESSING_TYPE, +} from '@backstage/catalog-client'; import { useApi, ApiHolder } from '@backstage/core-plugin-api'; import { useAsync } from 'react-use'; -import { CatalogApi } from '@backstage/catalog-client'; import { SerializedError } from '@backstage/errors'; const errorFilter = (i: UNSTABLE_EntityStatusItem) => @@ -37,10 +39,10 @@ const errorFilter = (i: UNSTABLE_EntityStatusItem) => i.type === ENTITY_STATUS_CATALOG_PROCESSING_TYPE; async function getOwnAndAncestorsErrors( - entityName: EntityName, + entityRef: EntityName, catalogApi: CatalogApi, ): Promise { - const ancestors = await catalogApi.getEntityAncestors({ entityName }); + const ancestors = await catalogApi.getEntityAncestors({ entityRef }); return ancestors.items.flatMap(item => { const statuses = item.entity.status?.items ?? []; return statuses @@ -52,9 +54,9 @@ async function getOwnAndAncestorsErrors( export const hasCatalogProcessingErrors = async ( entity: Entity, - context: { apiRegistry: ApiHolder }, + context: { apis: ApiHolder }, ) => { - const catalogApi = context.apiRegistry.get(catalogApiRef); + const catalogApi = context.apis.get(catalogApiRef); if (!catalogApi) { throw new Error(`No implementation available for ${catalogApiRef}`); } @@ -66,67 +68,14 @@ export const hasCatalogProcessingErrors = async ( return errors.length > 0; }; -/** - * Displays a list of errors if the entity is invalid. - */ -export const EntityProcessingErrorsPanel = () => { - /* - const { entity } = useEntity(); - const catalogProcessingErrors = - (entity?.status?.items?.filter( - errorFilter, - ) as Required[]) || []; - - return ( - <> - {catalogProcessingErrors.map(({ error }, index) => ( - - - - ))} - - ); - */ - // move this logic into the existing component - const { entity } = useEntity(); - const catalogApi = useApi(catalogApiRef); - - const { loading, error, value } = useAsync(() => { - return getOwnAndAncestorsErrors(getEntityName(entity), catalogApi); - }, [stringifyEntityRef(entity), catalogApi]); - - if (error) { - return ( - - - - ); - } - - if (loading || !value?.length) { - return null; - } - - return ( - <> - {value.map((ancestorError, index) => ( - - - - ))} - - ); -}; - /** * Displays a list of errors from the ancestors of the current entity. */ -export const EntityAncestorsProcessingErrorsPanel = () => { - // move this logic into the existing component +export const EntityProcessingErrorsPanel = () => { const { entity } = useEntity(); const catalogApi = useApi(catalogApiRef); - const { loading, error, value } = useAsync(() => { + const { loading, error, value } = useAsync(async () => { return getOwnAndAncestorsErrors(getEntityName(entity), catalogApi); }, [stringifyEntityRef(entity), catalogApi]); diff --git a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx index 201e884f43..bf2caac525 100644 --- a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx +++ b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx @@ -30,7 +30,7 @@ const ENTITY_SWITCH_KEY = 'core.backstage.entitySwitch'; const EntitySwitchCase = (_: { if?: ( entity: Entity, - context: { apiRegistry: ApiHolder }, + context: { apis: ApiHolder }, ) => boolean | Promise; children: ReactNode; }) => null; @@ -40,14 +40,14 @@ attachComponentData(EntitySwitchCase, ENTITY_SWITCH_KEY, true); type SwitchCase = { if?: ( entity: Entity, - context: { apiRegistry: ApiHolder }, + context: { apis: ApiHolder }, ) => boolean | Promise; children: JSX.Element; }; export const EntitySwitch = ({ children }: PropsWithChildren<{}>) => { const { entity } = useEntity(); - const apiRegistry = useApiHolder(); + const apis = useApiHolder(); const switchCases = useElementFilter(children, collection => collection .selectByComponentData({ @@ -69,7 +69,7 @@ export const EntitySwitch = ({ children }: PropsWithChildren<{}>) => { } try { - const matches = await condition(entity, { apiRegistry }); + const matches = await condition(entity, { apis }); return matches ? output : null; } catch { return null; From 26c08d8dd78e4e5903fa21c322b11c778605c568 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 4 Oct 2021 17:08:51 +0200 Subject: [PATCH 09/21] Display entity which error originated from Signed-off-by: Johan Haals --- .../EntityProcessingErrorsPanel.tsx | 53 ++++++++++++++----- 1 file changed, 40 insertions(+), 13 deletions(-) diff --git a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx index 9e7ea20f85..65e594fb3b 100644 --- a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx +++ b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx @@ -20,8 +20,13 @@ import { getEntityName, stringifyEntityRef, UNSTABLE_EntityStatusItem, + compareEntityToRef, } from '@backstage/catalog-model'; -import { catalogApiRef, useEntity } from '@backstage/plugin-catalog-react'; +import { + catalogApiRef, + EntityRefLink, + useEntity, +} from '@backstage/plugin-catalog-react'; import { Box } from '@material-ui/core'; import React from 'react'; import { ResponseErrorPanel } from '@backstage/core-components'; @@ -38,18 +43,29 @@ const errorFilter = (i: UNSTABLE_EntityStatusItem) => i.level === 'error' && i.type === ENTITY_STATUS_CATALOG_PROCESSING_TYPE; +type GetOwnAndAncestorsErrorsResponse = { + items: { + errors: SerializedError[]; + entity: Entity; + }[]; +}; + async function getOwnAndAncestorsErrors( entityRef: EntityName, catalogApi: CatalogApi, -): Promise { +): Promise { const ancestors = await catalogApi.getEntityAncestors({ entityRef }); - return ancestors.items.flatMap(item => { - const statuses = item.entity.status?.items ?? []; - return statuses - .filter(errorFilter) - .map(e => e.error) - .filter((e): e is SerializedError => Boolean(e)); - }); + const items = ancestors.items + .map(item => { + const statuses = item.entity.status?.items ?? []; + const errors = statuses + .filter(errorFilter) + .map(e => e.error) + .filter((e): e is SerializedError => Boolean(e)); + return { errors: errors, entity: item.entity }; + }) + .filter(item => item.errors.length > 0); + return { items }; } export const hasCatalogProcessingErrors = async ( @@ -65,7 +81,7 @@ export const hasCatalogProcessingErrors = async ( getEntityName(entity), catalogApi, ); - return errors.length > 0; + return errors.items.length > 0; }; /** @@ -87,15 +103,26 @@ export const EntityProcessingErrorsPanel = () => { ); } - if (loading || !value?.length) { + if (loading || !value) { return null; } return ( <> - {value.map((ancestorError, index) => ( + {value.items.map((ancestorError, index) => ( - + {!compareEntityToRef( + entity, + stringifyEntityRef(ancestorError.entity), + ) && ( + + The error below originates from{' '} + + + )} + {ancestorError.errors.map((e, i) => ( + + ))} ))} From 5ec945f1417e9019930d519492eb08ac158dfa9e Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 4 Oct 2021 17:09:55 +0200 Subject: [PATCH 10/21] chore: format package.json Signed-off-by: Johan Haals --- plugins/catalog/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index c2b8aaf4d6..eab929d13f 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -64,4 +64,4 @@ "files": [ "dist" ] -} \ No newline at end of file +} From 7413797ecf6b3e319c1db962d254cd7f9ed9f11f Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 4 Oct 2021 17:35:19 +0200 Subject: [PATCH 11/21] Add EntityProcessingErrorsPanel tests Signed-off-by: Johan Haals --- .../EntityProcessingErrorsPanel.test.tsx | 131 +++++++++++++++++- 1 file changed, 126 insertions(+), 5 deletions(-) diff --git a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx index a4e284a0eb..9990277da3 100644 --- a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx +++ b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx @@ -14,14 +14,24 @@ * limitations under the License. */ -import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { + CatalogApi, + catalogApiRef, + EntityProvider, +} from '@backstage/plugin-catalog-react'; import { renderInTestApp } from '@backstage/test-utils'; import React from 'react'; import { EntityProcessingErrorsPanel } from './EntityProcessingErrorsPanel'; -import { Entity } from '@backstage/catalog-model'; +import { Entity, getEntityName } from '@backstage/catalog-model'; +import { ApiProvider, ApiRegistry } from '@backstage/core-app-api'; describe('', () => { + const catalogClient: jest.Mocked = { + getEntityAncestors: jest.fn(), + } as any; + const apis = ApiRegistry.with(catalogApiRef, catalogClient); + it('renders EntityProcessErrors if the entity has errors', async () => { const entity: Entity = { apiVersion: 'v1', @@ -86,10 +96,16 @@ describe('', () => { }, }; + catalogClient.getEntityAncestors.mockResolvedValue({ + root: getEntityName(entity), + items: [{ entity, parents: [] }], + }); const { getByText, queryByText } = await renderInTestApp( - - - , + + + + + , ); expect( @@ -99,5 +115,110 @@ describe('', () => { ).toBeInTheDocument(); expect(getByText('Error: Foo')).toBeInTheDocument(); expect(queryByText('Error: This should not be rendered')).toBeNull(); + expect( + queryByText('The error below originates from'), + ).not.toBeInTheDocument(); + }); + + it('renders EntityProcessErrors if the parent entity has errors', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'software', + description: 'This is the description', + }, + spec: { + owner: 'guest', + type: 'service', + lifecycle: 'production', + }, + }; + + const parent: Entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'parent', + description: 'This is the description', + }, + + spec: { + owner: 'guest', + type: 'service', + lifecycle: 'production', + }, + status: { + items: [ + { + type: 'backstage.io/catalog-processing', + level: 'error', + message: + 'InputError: Policy check failed; caused by Error: Malformed envelope, /metadata/labels should be object', + error: { + name: 'InputError', + message: + 'Policy check failed; caused by Error: Malformed envelope, /metadata/labels should be object', + cause: { + name: 'Error', + message: + 'Malformed envelope, /metadata/labels should be object', + }, + }, + }, + { + type: 'foo', + level: 'error', + message: 'InputError: This should not be rendered', + error: { + name: 'InputError', + message: 'Foo', + cause: { + name: 'Error', + message: + 'Malformed envelope, /metadata/labels should be object', + }, + }, + }, + { + type: 'backstage.io/catalog-processing', + level: 'error', + message: 'InputError: Foo', + error: { + name: 'InputError', + message: 'Foo', + cause: { + name: 'Error', + message: + 'Malformed envelope, /metadata/labels should be object', + }, + }, + }, + ], + }, + }; + catalogClient.getEntityAncestors.mockResolvedValue({ + root: getEntityName(entity), + items: [ + { entity, parents: [getEntityName(parent)] }, + { entity: parent, parents: [] }, + ], + }); + const { getByText, queryByText } = await renderInTestApp( + + + + + , + ); + + expect( + getByText( + 'Error: Policy check failed; caused by Error: Malformed envelope, /metadata/labels should be object', + ), + ).toBeInTheDocument(); + expect(getByText('Error: Foo')).toBeInTheDocument(); + expect(queryByText('Error: This should not be rendered')).toBeNull(); + expect(queryByText('The error below originates from')).toBeInTheDocument(); }); }); From 604987599e08acc862e5331c99b347651d1f0612 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 5 Oct 2021 09:30:43 +0200 Subject: [PATCH 12/21] chore: Adds missing method to mocked catalog APIs Signed-off-by: Johan Haals --- .../auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts | 1 + plugins/auth-backend/src/providers/aws-alb/provider.test.ts | 1 + plugins/badges-backend/src/service/router.test.ts | 1 + .../src/components/CatalogGraphCard/CatalogGraphCard.test.tsx | 1 + .../src/components/CatalogGraphPage/CatalogGraphPage.test.tsx | 1 + .../EntityRelationsGraph/EntityRelationsGraph.test.tsx | 1 + .../src/components/EntityRelationsGraph/useEntityStore.test.ts | 1 + plugins/catalog-import/src/api/CatalogImportClient.test.ts | 1 + .../StepPrepareCreatePullRequest.test.tsx | 1 + .../components/DefaultExplorePage/DefaultExplorePage.test.tsx | 1 + .../DomainExplorerContent/DomainExplorerContent.test.tsx | 1 + .../GroupsExplorerContent/GroupsExplorerContent.test.tsx | 1 + plugins/fossa/src/components/FossaPage/FossaPage.test.tsx | 1 + plugins/todo-backend/src/service/TodoReaderService.test.ts | 1 + 14 files changed, 14 insertions(+) diff --git a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts index 5dd3c768bb..0ddcaa6d2c 100644 --- a/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts +++ b/plugins/auth-backend/src/lib/catalog/CatalogIdentityClient.test.ts @@ -34,6 +34,7 @@ describe('CatalogIdentityClient', () => { getLocationByEntity: jest.fn(), removeEntityByUid: jest.fn(), refreshEntity: jest.fn(), + getEntityAncestors: jest.fn(), }; const tokenIssuer: jest.Mocked = { issueToken: jest.fn(), diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts index c9647176cd..ca4e521316 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.test.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.test.ts @@ -77,6 +77,7 @@ describe('AwsALBAuthProvider', () => { removeEntityByUid: jest.fn(), getEntityByName: jest.fn(), refreshEntity: jest.fn(), + getEntityAncestors: jest.fn(), }; const mockRequest = { diff --git a/plugins/badges-backend/src/service/router.test.ts b/plugins/badges-backend/src/service/router.test.ts index 5dc877ec32..51ad1e7e11 100644 --- a/plugins/badges-backend/src/service/router.test.ts +++ b/plugins/badges-backend/src/service/router.test.ts @@ -67,6 +67,7 @@ describe('createRouter', () => { removeLocationById: jest.fn(), removeEntityByUid: jest.fn(), refreshEntity: jest.fn(), + getEntityAncestors: jest.fn(), }; config = new ConfigReader({ diff --git a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx index 8207517e03..7e3149fef7 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx @@ -57,6 +57,7 @@ describe('', () => { addLocation: jest.fn(), removeLocationById: jest.fn(), refreshEntity: jest.fn(), + getEntityAncestors: jest.fn(), }; apis = ApiRegistry.with(catalogApiRef, catalog); diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx index c291faadbd..8b467a017e 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.test.tsx @@ -87,6 +87,7 @@ describe('', () => { addLocation: jest.fn(), removeLocationById: jest.fn(), refreshEntity: jest.fn(), + getEntityAncestors: jest.fn(), }; const apis = ApiRegistry.with(catalogApiRef, catalog); diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx index fe3bef3e84..2d6c5cc93d 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.test.tsx @@ -156,6 +156,7 @@ describe('', () => { addLocation: jest.fn(), removeLocationById: jest.fn(), refreshEntity: jest.fn(), + getEntityAncestors: jest.fn(), }; const apis = ApiRegistry.with(catalogApiRef, catalog); diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.test.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.test.ts index 2675469c15..a60750bab5 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.test.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityStore.test.ts @@ -37,6 +37,7 @@ describe('useEntityStore', () => { addLocation: jest.fn(), removeLocationById: jest.fn(), refreshEntity: jest.fn(), + getEntityAncestors: jest.fn(), }; useApi.mockReturnValue(catalogApi); diff --git a/plugins/catalog-import/src/api/CatalogImportClient.test.ts b/plugins/catalog-import/src/api/CatalogImportClient.test.ts index 373acd4b5f..0ddd92e121 100644 --- a/plugins/catalog-import/src/api/CatalogImportClient.test.ts +++ b/plugins/catalog-import/src/api/CatalogImportClient.test.ts @@ -105,6 +105,7 @@ describe('CatalogImportClient', () => { getLocationById: jest.fn(), removeEntityByUid: jest.fn(), refreshEntity: jest.fn(), + getEntityAncestors: jest.fn(), }; let catalogImportClient: CatalogImportClient; diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx index 2c4dd54f9e..0a55d6337b 100644 --- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx +++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx @@ -45,6 +45,7 @@ describe('', () => { removeLocationById: jest.fn(), removeEntityByUid: jest.fn(), refreshEntity: jest.fn(), + getEntityAncestors: jest.fn(), }; const errorApi: jest.Mocked = { diff --git a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx index 84b922bef5..8927cf088b 100644 --- a/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx +++ b/plugins/explore/src/components/DefaultExplorePage/DefaultExplorePage.test.tsx @@ -32,6 +32,7 @@ describe('', () => { removeEntityByUid: jest.fn(), getEntityByName: jest.fn(), refreshEntity: jest.fn(), + getEntityAncestors: jest.fn(), }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( diff --git a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx index 5fd2b02ee9..d78fc0428d 100644 --- a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx +++ b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx @@ -34,6 +34,7 @@ describe('', () => { removeEntityByUid: jest.fn(), getEntityByName: jest.fn(), refreshEntity: jest.fn(), + getEntityAncestors: jest.fn(), }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( diff --git a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx index 56dfd59aa8..f0815e0116 100644 --- a/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx +++ b/plugins/explore/src/components/GroupsExplorerContent/GroupsExplorerContent.test.tsx @@ -33,6 +33,7 @@ describe('', () => { removeEntityByUid: jest.fn(), getEntityByName: jest.fn(), refreshEntity: jest.fn(), + getEntityAncestors: jest.fn(), }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( diff --git a/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx b/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx index c7fca26a3b..78c8b7bb40 100644 --- a/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx +++ b/plugins/fossa/src/components/FossaPage/FossaPage.test.tsx @@ -37,6 +37,7 @@ describe('', () => { removeEntityByUid: jest.fn(), removeLocationById: jest.fn(), refreshEntity: jest.fn(), + getEntityAncestors: jest.fn(), }; const fossaApi: jest.Mocked = { getFindingSummary: jest.fn(), diff --git a/plugins/todo-backend/src/service/TodoReaderService.test.ts b/plugins/todo-backend/src/service/TodoReaderService.test.ts index 777fb0acf3..3d86a1726b 100644 --- a/plugins/todo-backend/src/service/TodoReaderService.test.ts +++ b/plugins/todo-backend/src/service/TodoReaderService.test.ts @@ -51,6 +51,7 @@ function mockCatalogClient(entity?: Entity): jest.Mocked { removeLocationById: jest.fn(), removeEntityByUid: jest.fn(), refreshEntity: jest.fn(), + getEntityAncestors: jest.fn(), }; if (entity) { mock.getEntityByName.mockReturnValue(entity); From a1594fb8cabf1d76cb2940b29a5582babc40d162 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 5 Oct 2021 09:52:43 +0200 Subject: [PATCH 13/21] api reports Signed-off-by: Johan Haals --- packages/catalog-client/api-report.md | 24 ++++++++++++++++++++++++ plugins/catalog/api-report.md | 26 +++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/packages/catalog-client/api-report.md b/packages/catalog-client/api-report.md index 8a81c0eb10..3571cbf57e 100644 --- a/packages/catalog-client/api-report.md +++ b/packages/catalog-client/api-report.md @@ -38,6 +38,11 @@ export interface CatalogApi { options?: CatalogRequestOptions, ): Promise>; // (undocumented) + getEntityAncestors( + request: CatalogEntityAncestorsRequest, + options?: CatalogRequestOptions, + ): Promise; + // (undocumented) getEntityByName( name: EntityName, options?: CatalogRequestOptions, @@ -88,6 +93,11 @@ export class CatalogClient implements CatalogApi { options?: CatalogRequestOptions, ): Promise>; // (undocumented) + getEntityAncestors( + request: CatalogEntityAncestorsRequest, + options?: CatalogRequestOptions, + ): Promise; + // (undocumented) getEntityByName( compoundName: EntityName, options?: CatalogRequestOptions, @@ -133,6 +143,20 @@ export type CatalogEntitiesRequest = { fields?: string[] | undefined; }; +// @public (undocumented) +export type CatalogEntityAncestorsRequest = { + entityRef: EntityName; +}; + +// @public (undocumented) +export type CatalogEntityAncestorsResponse = { + root: EntityName; + items: { + entity: Entity; + parents: EntityName[]; + }[]; +}; + // @public (undocumented) export type CatalogListResponse = { items: T[]; diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index 1ee0d841c9..4937a3eb86 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -7,10 +7,13 @@ import { AddLocationRequest } from '@backstage/catalog-client'; import { AddLocationResponse } from '@backstage/catalog-client'; +import { ApiHolder } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; import { CatalogClient } from '@backstage/catalog-client'; import { CatalogEntitiesRequest } from '@backstage/catalog-client'; +import { CatalogEntityAncestorsRequest } from '@backstage/catalog-client'; +import { CatalogEntityAncestorsResponse } from '@backstage/catalog-client'; import { CatalogListResponse } from '@backstage/catalog-client'; import { CatalogRequestOptions } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; @@ -68,6 +71,11 @@ export class CatalogClientWrapper implements CatalogApi { options?: CatalogRequestOptions, ): Promise>; // (undocumented) + getEntityAncestors( + request: CatalogEntityAncestorsRequest, + options?: CatalogRequestOptions, + ): Promise; + // (undocumented) getEntityByName( compoundName: EntityName, options?: CatalogRequestOptions, @@ -346,7 +354,7 @@ export const EntityPageLayout: { // Warning: (ae-missing-release-tag) "EntityProcessingErrorsPanel" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export const EntityProcessingErrorsPanel: () => JSX.Element; +export const EntityProcessingErrorsPanel: () => JSX.Element | null; // Warning: (ae-missing-release-tag) "EntitySwitch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -354,7 +362,14 @@ export const EntityProcessingErrorsPanel: () => JSX.Element; export const EntitySwitch: { ({ children }: PropsWithChildren<{}>): JSX.Element | null; Case: (_: { - if?: ((entity: Entity) => boolean) | undefined; + if?: + | (( + entity: Entity, + context: { + apis: ApiHolder; + }, + ) => boolean | Promise) + | undefined; children: ReactNode; }) => null; }; @@ -382,7 +397,12 @@ export const FilteredEntityLayout: ({ // Warning: (ae-missing-release-tag) "hasCatalogProcessingErrors" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const hasCatalogProcessingErrors: (entity: Entity) => boolean; +export const hasCatalogProcessingErrors: ( + entity: Entity, + context: { + apis: ApiHolder; + }, +) => Promise; // Warning: (ae-missing-release-tag) "isComponentType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // From bb0f6b8a0f3fb45f75cef1e188c54cc9b7b5f690 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 5 Oct 2021 10:35:49 +0200 Subject: [PATCH 14/21] add changeset Signed-off-by: Johan Haals --- .changeset/orange-jokes-drum.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .changeset/orange-jokes-drum.md diff --git a/.changeset/orange-jokes-drum.md b/.changeset/orange-jokes-drum.md new file mode 100644 index 0000000000..cae689d734 --- /dev/null +++ b/.changeset/orange-jokes-drum.md @@ -0,0 +1,10 @@ +--- +'@backstage/catalog-client': minor +'@backstage/plugin-catalog': minor +--- + +Updates the `` to accept asynchronous `if` functions. + +Adds the new `getEntityAncestors` method to `CatalogClient`. + +Updates the `` to make use of the ancestry endpoint to display errors for entities further up the ancestry tree. This makes it easier to discover issues where for example the origin location has been removed or malformed. From 4e3755151c358ae04f59408c7bcaec3bdfdf6ffd Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 5 Oct 2021 14:41:24 +0200 Subject: [PATCH 15/21] Keep existing EntitySwitch behaviour, add tests Keep the existing behavior for non async statements Co-authored-by: Patrik Oldsberg Signed-off-by: Johan Haals --- .../EntitySwitch/EntitySwitch.test.tsx | 59 ++++++++++++++++ .../components/EntitySwitch/EntitySwitch.tsx | 70 +++++++++++++------ 2 files changed, 106 insertions(+), 23 deletions(-) diff --git a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx index f80a66b9bb..7f8bf35b74 100644 --- a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx +++ b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.test.tsx @@ -112,4 +112,63 @@ describe('EntitySwitch', () => { expect(rendered.queryByText('A')).not.toBeInTheDocument(); expect(rendered.queryByText('B')).toBeInTheDocument(); }); + + it('should switch with async condition that is true', async () => { + const entity = { kind: 'component' } as Entity; + + const shouldRender = () => Promise.resolve(true); + const rendered = render( + + + + + + + + , + ); + + await expect(rendered.findByText('A')).resolves.toBeInTheDocument(); + expect(rendered.queryByText('B')).not.toBeInTheDocument(); + }); + + it('should switch with sync condition that is false', async () => { + const entity = { kind: 'component' } as Entity; + + const shouldRender = () => Promise.resolve(false); + const rendered = render( + + + + + true} children="B" /> + + + , + ); + + await expect(rendered.findByText('B')).resolves.toBeInTheDocument(); + expect(rendered.queryByText('A')).not.toBeInTheDocument(); + }); + + it('should switch with sync condition that throws', async () => { + const entity = { kind: 'component' } as Entity; + + const shouldRender = () => Promise.reject(); + const rendered = render( + + + + + false} children="B" /> + + + + , + ); + + await expect(rendered.findByText('C')).resolves.toBeInTheDocument(); + expect(rendered.queryByText('A')).not.toBeInTheDocument(); + expect(rendered.queryByText('B')).not.toBeInTheDocument(); + }); }); diff --git a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx index bf2caac525..5c50c2507c 100644 --- a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx +++ b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx @@ -16,7 +16,7 @@ import { Entity } from '@backstage/catalog-model'; import { useEntity } from '@backstage/plugin-catalog-react'; -import { PropsWithChildren, ReactNode } from 'react'; +import React, { PropsWithChildren, ReactNode } from 'react'; import { attachComponentData, useApiHolder, @@ -45,45 +45,69 @@ type SwitchCase = { children: JSX.Element; }; +type SwitchCaseResult = { + if: boolean | Promise; + children: JSX.Element; +}; + export const EntitySwitch = ({ children }: PropsWithChildren<{}>) => { const { entity } = useEntity(); const apis = useApiHolder(); - const switchCases = useElementFilter(children, collection => - collection - .selectByComponentData({ - key: ENTITY_SWITCH_KEY, - withStrictError: 'Child of EntitySwitch is not an EntitySwitch.Case', - }) - .getElements() - .flatMap((element: React.ReactElement) => { - const { if: condition, children: elementsChildren } = element.props; - return [{ if: condition, children: elementsChildren }]; - }), + const results = useElementFilter( + children, + collection => + collection + .selectByComponentData({ + key: ENTITY_SWITCH_KEY, + withStrictError: 'Child of EntitySwitch is not an EntitySwitch.Case', + }) + .getElements() + .flatMap((element: React.ReactElement) => { + const { if: condition, children: elementsChildren } = + element.props as SwitchCase; + return [ + { + if: condition?.(entity, { apis }) ?? true, + children: elementsChildren, + }, + ]; + }), + [apis, entity], + ); + const hasAsyncCases = results.some( + r => typeof r.if === 'object' && 'then' in r.if, ); + if (hasAsyncCases) { + return ; + } + + return results.find(r => r.if)?.children ?? null; +}; + +function AsyncEntitySwitch({ results }: { results: SwitchCaseResult[] }) { const { loading, value } = useAsync(async () => { - const promises = switchCases.map( + const promises = results.map( async ({ if: condition, children: output }) => { - if (!condition) { - return output; + try { + if (await condition) { + return output; + } + } catch { + /* ignored */ } - try { - const matches = await condition(entity, { apis }); - return matches ? output : null; - } catch { - return null; - } + return null; }, ); return (await Promise.all(promises)).find(Boolean) ?? null; - }, [switchCases]); + }, [results]); if (loading || !value) { return null; } return value; -}; +} EntitySwitch.Case = EntitySwitchCase; From 2338a116e8b0eff4030c5d32f8477710f633c8cc Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 5 Oct 2021 16:17:49 +0200 Subject: [PATCH 16/21] chore: Add entityRouteRef to mountedRoutes in test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Patrik Oldsberg Co-authored-by: Fredrik Adelöw Signed-off-by: Johan Haals --- .../EntityProcessingErrorsPanel.test.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx index 9990277da3..c406d0ed3c 100644 --- a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx +++ b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.test.tsx @@ -18,6 +18,7 @@ import { CatalogApi, catalogApiRef, EntityProvider, + entityRouteRef, } from '@backstage/plugin-catalog-react'; import { renderInTestApp } from '@backstage/test-utils'; @@ -210,6 +211,11 @@ describe('', () => { , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, ); expect( From 348e6c2d2580aa0a1e5b64381bdbfa54e7b661f4 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 5 Oct 2021 16:23:30 +0200 Subject: [PATCH 17/21] Remove unrelated change Signed-off-by: Johan Haals --- plugins/catalog-backend/src/next/NextEntitiesCatalog.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/catalog-backend/src/next/NextEntitiesCatalog.ts b/plugins/catalog-backend/src/next/NextEntitiesCatalog.ts index f8c244cbfd..14fda4ea24 100644 --- a/plugins/catalog-backend/src/next/NextEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/next/NextEntitiesCatalog.ts @@ -189,6 +189,7 @@ export class NextEntitiesCatalog implements EntitiesCatalog { current = todo.pop() ) { const currentRef = stringifyEntityRef(current); + seenEntityRefs.add(currentRef); const parentRows = await this.database( 'refresh_state_references', From dccaa059aea135510f2a4e5dd9d33ad7870c95d9 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 5 Oct 2021 16:38:20 +0200 Subject: [PATCH 18/21] Refactor catalog client to accept entityRef Signed-off-by: Johan Haals --- packages/catalog-client/src/CatalogClient.ts | 9 ++------- packages/catalog-client/src/types/api.ts | 2 +- .../EntityProcessingErrorsPanel.tsx | 12 +++++------- 3 files changed, 8 insertions(+), 15 deletions(-) diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index 3a4c0455ad..b2420bc71b 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -20,6 +20,7 @@ import { Location, LOCATION_ANNOTATION, ORIGIN_LOCATION_ANNOTATION, + parseEntityRef, stringifyEntityRef, stringifyLocationReference, } from '@backstage/catalog-model'; @@ -50,13 +51,7 @@ export class CatalogClient implements CatalogApi { request: CatalogEntityAncestorsRequest, options?: CatalogRequestOptions, ): Promise { - const { kind, namespace, name } = Object.fromEntries( - Object.entries(request.entityRef).map(([k, v]) => [ - k, - encodeURIComponent(v), - ]), - ); - + const { kind, namespace, name } = parseEntityRef(request.entityRef); return await this.requestRequired( 'GET', `/entities/by-name/${kind}/${namespace}/${name}/ancestry`, diff --git a/packages/catalog-client/src/types/api.ts b/packages/catalog-client/src/types/api.ts index 234c49c779..c7025e9452 100644 --- a/packages/catalog-client/src/types/api.ts +++ b/packages/catalog-client/src/types/api.ts @@ -30,7 +30,7 @@ export type CatalogEntitiesRequest = { /** @public */ export type CatalogEntityAncestorsRequest = { - entityRef: EntityName; + entityRef: string; }; /** @public */ diff --git a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx index 65e594fb3b..0efc9d5b03 100644 --- a/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx +++ b/plugins/catalog/src/components/EntityProcessingErrorsPanel/EntityProcessingErrorsPanel.tsx @@ -16,8 +16,6 @@ import { Entity, - EntityName, - getEntityName, stringifyEntityRef, UNSTABLE_EntityStatusItem, compareEntityToRef, @@ -51,7 +49,7 @@ type GetOwnAndAncestorsErrorsResponse = { }; async function getOwnAndAncestorsErrors( - entityRef: EntityName, + entityRef: string, catalogApi: CatalogApi, ): Promise { const ancestors = await catalogApi.getEntityAncestors({ entityRef }); @@ -78,7 +76,7 @@ export const hasCatalogProcessingErrors = async ( } const errors = await getOwnAndAncestorsErrors( - getEntityName(entity), + stringifyEntityRef(entity), catalogApi, ); return errors.items.length > 0; @@ -89,11 +87,11 @@ export const hasCatalogProcessingErrors = async ( */ export const EntityProcessingErrorsPanel = () => { const { entity } = useEntity(); + const entityRef = stringifyEntityRef(entity); const catalogApi = useApi(catalogApiRef); - const { loading, error, value } = useAsync(async () => { - return getOwnAndAncestorsErrors(getEntityName(entity), catalogApi); - }, [stringifyEntityRef(entity), catalogApi]); + return getOwnAndAncestorsErrors(entityRef, catalogApi); + }, [entityRef, catalogApi]); if (error) { return ( From 0e2893dcf6f7177ca59f1845c46a87aed65c67c6 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 6 Oct 2021 09:29:45 +0200 Subject: [PATCH 19/21] update api report Signed-off-by: Johan Haals --- packages/catalog-client/api-report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/catalog-client/api-report.md b/packages/catalog-client/api-report.md index 3571cbf57e..b7bb713957 100644 --- a/packages/catalog-client/api-report.md +++ b/packages/catalog-client/api-report.md @@ -145,7 +145,7 @@ export type CatalogEntitiesRequest = { // @public (undocumented) export type CatalogEntityAncestorsRequest = { - entityRef: EntityName; + entityRef: string; }; // @public (undocumented) From 6b4fe1fafe8b2157353cea3ef99321cc33425f76 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 6 Oct 2021 10:42:17 +0200 Subject: [PATCH 20/21] encode kind, namespace and name Signed-off-by: Johan Haals --- packages/catalog-client/src/CatalogClient.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index b2420bc71b..75944467d8 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -54,7 +54,9 @@ export class CatalogClient implements CatalogApi { const { kind, namespace, name } = parseEntityRef(request.entityRef); return await this.requestRequired( 'GET', - `/entities/by-name/${kind}/${namespace}/${name}/ancestry`, + `/entities/by-name/${encodeURIComponent(kind)}/${encodeURIComponent( + namespace, + )}/${encodeURIComponent(name)}/ancestry`, options, ); } From d8fe76911ca27c7e8e6186efc73ac9d8e476cd28 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 6 Oct 2021 10:42:53 +0200 Subject: [PATCH 21/21] Add hasCatalogProcessingErrors to changeset Signed-off-by: Johan Haals --- .changeset/orange-jokes-drum.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.changeset/orange-jokes-drum.md b/.changeset/orange-jokes-drum.md index cae689d734..ebc22ccd93 100644 --- a/.changeset/orange-jokes-drum.md +++ b/.changeset/orange-jokes-drum.md @@ -8,3 +8,5 @@ Updates the `` to accept asynchronous `if` funct Adds the new `getEntityAncestors` method to `CatalogClient`. Updates the `` to make use of the ancestry endpoint to display errors for entities further up the ancestry tree. This makes it easier to discover issues where for example the origin location has been removed or malformed. + +`hasCatalogProcessingErrors()` is now changed to be asynchronous so any calls outside the already established entitySwitch need to be awaited.