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