From 2156d99515db9a6d102e74adf6102233121986f4 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Fri, 18 Jul 2025 12:11:24 +0200 Subject: [PATCH 1/4] feat: support filters in EntityHeader blueprint Signed-off-by: benjdlambert Signed-off-by: benjdlambert --- .../blueprints/EntityHeaderBlueprint.tsx | 32 ++++++++++++++++--- .../components/EntityLayout/EntityLayout.tsx | 19 +++++------ plugins/catalog/src/alpha/pages.tsx | 29 +++++++++++++---- 3 files changed, 60 insertions(+), 20 deletions(-) diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityHeaderBlueprint.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityHeaderBlueprint.tsx index 431197e753..73f09e4b22 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityHeaderBlueprint.tsx +++ b/plugins/catalog-react/src/alpha/blueprints/EntityHeaderBlueprint.tsx @@ -19,22 +19,46 @@ import { coreExtensionData, ExtensionBoundary, } from '@backstage/frontend-plugin-api'; +import { EntityPredicate } from '../predicates/types'; +import { Entity } from '@backstage/catalog-model'; +import { resolveEntityFilterData } from './resolveEntityFilterData'; +import { createEntityPredicateSchema } from '../predicates/createEntityPredicateSchema'; +import { + entityFilterExpressionDataRef, + entityFilterFunctionDataRef, +} from './extensionData'; /** @alpha */ export const EntityHeaderBlueprint = createExtensionBlueprint({ kind: 'entity-header', - attachTo: { id: 'page:catalog/entity', input: 'header' }, + attachTo: { id: 'page:catalog/entity', input: 'headers' }, dataRefs: { + filterFunction: entityFilterFunctionDataRef, + filterExpression: entityFilterExpressionDataRef, element: coreExtensionData.reactElement, }, - output: [coreExtensionData.reactElement.optional()], + config: { + schema: { + filter: z => + z.union([z.string(), createEntityPredicateSchema(z)]).optional(), + }, + }, + output: [ + entityFilterFunctionDataRef.optional(), + entityFilterExpressionDataRef.optional(), + coreExtensionData.reactElement.optional(), + ], *factory( params: { loader: () => Promise; + filter?: string | EntityPredicate | ((entity: Entity) => boolean); }, - { node }, + { node, config }, ) { - const { loader } = params; + const { loader, filter } = params; + + yield* resolveEntityFilterData(filter, config, node); + if (loader) { yield coreExtensionData.reactElement( ExtensionBoundary.lazy(node, loader), diff --git a/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx index 750c8567ee..8f5838d545 100644 --- a/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx +++ b/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx @@ -102,13 +102,21 @@ export const EntityLayout = (props: EntityLayoutProps) => { UNSTABLE_contextMenuOptions, contextMenuItems, children, - header, NotFoundComponent, parentEntityRelations, } = props; const { kind } = useRouteRefParams(entityRouteRef); const { entity, loading, error } = useAsyncEntity(); + const header = props.header ?? ( + + ); + const routes = useElementFilter( children, elements => @@ -142,14 +150,7 @@ export const EntityLayout = (props: EntityLayoutProps) => { return ( - {header ?? ( - - )} + {!loading && header} {loading && } diff --git a/plugins/catalog/src/alpha/pages.tsx b/plugins/catalog/src/alpha/pages.tsx index 81ec0c5cb9..52b60e423d 100644 --- a/plugins/catalog/src/alpha/pages.tsx +++ b/plugins/catalog/src/alpha/pages.tsx @@ -36,6 +36,7 @@ import { import { rootRouteRef } from '../routes'; import { useEntityFromUrl } from '../components/CatalogEntityPage/useEntityFromUrl'; import { buildFilterFn } from './filter/FilterWrapper'; +import { Progress } from '@backstage/core-components'; export const catalogPage = PageBlueprint.makeWithOverrides({ inputs: { @@ -79,10 +80,10 @@ export const catalogPage = PageBlueprint.makeWithOverrides({ export const catalogEntityPage = PageBlueprint.makeWithOverrides({ name: 'entity', inputs: { - header: createExtensionInput( - [EntityHeaderBlueprint.dataRefs.element.optional()], - { singleton: true, optional: true }, - ), + headers: createExtensionInput([ + EntityHeaderBlueprint.dataRefs.element.optional(), + EntityHeaderBlueprint.dataRefs.filterFunction.optional(), + ]), contents: createExtensionInput([ coreExtensionData.reactElement, coreExtensionData.routePath, @@ -124,9 +125,19 @@ export const catalogEntityPage = PageBlueprint.makeWithOverrides({ { title: string; items: Array<(typeof inputs.contents)[0]> } >; - const header = inputs.header?.get( - EntityHeaderBlueprint.dataRefs.element, - ); + // Get available headers, sorted by if they have a filter function or not. + // TODO(blam): we should really have priority or some specificity here which can be used to sort the headers. + // That can be done with embedding the priority in the dataRef alongside the filter function. + const headers = inputs.headers + .map(header => ({ + element: header.get(EntityHeaderBlueprint.dataRefs.element), + filter: header.get(EntityHeaderBlueprint.dataRefs.filterFunction), + })) + .sort((a, b) => { + if (a.filter && !b.filter) return -1; + if (!a.filter && b.filter) return 1; + return 0; + }); let groups = Object.entries(defaultEntityContentGroups).reduce( (rest, group) => { @@ -169,6 +180,10 @@ export const catalogEntityPage = PageBlueprint.makeWithOverrides({ ? menuItems.filter(i => i.filter(entity)).map(i => i.element) : []; + const header = headers.find( + h => !h.filter || h.filter(entity!), + )?.element; + return ( Date: Fri, 18 Jul 2025 12:12:46 +0200 Subject: [PATCH 2/4] chore: add changesets Signed-off-by: benjdlambert --- .changeset/evil-phones-unite.md | 5 +++++ .changeset/nice-crabs-clean.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/evil-phones-unite.md create mode 100644 .changeset/nice-crabs-clean.md diff --git a/.changeset/evil-phones-unite.md b/.changeset/evil-phones-unite.md new file mode 100644 index 0000000000..9a3e4ff151 --- /dev/null +++ b/.changeset/evil-phones-unite.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Support `filter` parameter on the `EntityHeaderBlueprint` diff --git a/.changeset/nice-crabs-clean.md b/.changeset/nice-crabs-clean.md new file mode 100644 index 0000000000..45690e98aa --- /dev/null +++ b/.changeset/nice-crabs-clean.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Support multiple headers in new frontend system, and don't render a header until the entity has finished loading From 7058789375b0d9a3c1fa56f9d822872211cc0290 Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Fri, 18 Jul 2025 12:22:13 +0200 Subject: [PATCH 3/4] chore: fix build and updating api-reports Signed-off-by: benjdlambert --- plugins/catalog-react/report-alpha.api.md | 48 ++++++++++++++++++----- plugins/catalog/report-alpha.api.md | 27 ++++++++----- plugins/catalog/src/alpha/pages.tsx | 1 - 3 files changed, 56 insertions(+), 20 deletions(-) diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md index 8476eed695..4726b7d146 100644 --- a/plugins/catalog-react/report-alpha.api.md +++ b/plugins/catalog-react/report-alpha.api.md @@ -372,18 +372,48 @@ export const EntityHeaderBlueprint: ExtensionBlueprint<{ name: undefined; params: { loader: () => Promise; + filter?: string | EntityPredicate | ((entity: Entity) => boolean); }; - output: ConfigurableExtensionDataRef< - JSX_2.Element, - 'core.reactElement', - { - optional: true; - } - >; + output: + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + JSX_2.Element, + 'core.reactElement', + { + optional: true; + } + >; inputs: {}; - config: {}; - configInput: {}; + config: { + filter: EntityPredicate | undefined; + }; + configInput: { + filter?: EntityPredicate | undefined; + }; dataRefs: { + filterFunction: ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + {} + >; + filterExpression: ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + {} + >; element: ConfigurableExtensionDataRef< JSX_2.Element, 'core.reactElement', diff --git a/plugins/catalog/report-alpha.api.md b/plugins/catalog/report-alpha.api.md index 4e00a5185e..81ebd10c3b 100644 --- a/plugins/catalog/report-alpha.api.md +++ b/plugins/catalog/report-alpha.api.md @@ -1097,17 +1097,24 @@ const _default: FrontendPlugin< } >; inputs: { - header: ExtensionInput< - ConfigurableExtensionDataRef< - JSX_2.Element, - 'core.reactElement', - { - optional: true; - } - >, + headers: ExtensionInput< + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + JSX_2.Element, + 'core.reactElement', + { + optional: true; + } + >, { - singleton: true; - optional: true; + singleton: false; + optional: false; } >; contents: ExtensionInput< diff --git a/plugins/catalog/src/alpha/pages.tsx b/plugins/catalog/src/alpha/pages.tsx index 52b60e423d..f0c5316a84 100644 --- a/plugins/catalog/src/alpha/pages.tsx +++ b/plugins/catalog/src/alpha/pages.tsx @@ -36,7 +36,6 @@ import { import { rootRouteRef } from '../routes'; import { useEntityFromUrl } from '../components/CatalogEntityPage/useEntityFromUrl'; import { buildFilterFn } from './filter/FilterWrapper'; -import { Progress } from '@backstage/core-components'; export const catalogPage = PageBlueprint.makeWithOverrides({ inputs: { From 1bb3f48c177142bf71b9000b0a5f6a21aee779ce Mon Sep 17 00:00:00 2001 From: benjdlambert Date: Mon, 21 Jul 2025 10:41:17 +0200 Subject: [PATCH 4/4] feat: removing filter expression possibilities Signed-off-by: benjdlambert --- plugins/catalog-react/report-alpha.api.md | 7 +------ .../src/alpha/blueprints/EntityHeaderBlueprint.tsx | 6 ++---- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md index 4726b7d146..1db7a8e5e4 100644 --- a/plugins/catalog-react/report-alpha.api.md +++ b/plugins/catalog-react/report-alpha.api.md @@ -372,7 +372,7 @@ export const EntityHeaderBlueprint: ExtensionBlueprint<{ name: undefined; params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: EntityPredicate | ((entity: Entity) => boolean); }; output: | ConfigurableExtensionDataRef< @@ -409,11 +409,6 @@ export const EntityHeaderBlueprint: ExtensionBlueprint<{ 'catalog.entity-filter-function', {} >; - filterExpression: ConfigurableExtensionDataRef< - string, - 'catalog.entity-filter-expression', - {} - >; element: ConfigurableExtensionDataRef< JSX_2.Element, 'core.reactElement', diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityHeaderBlueprint.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityHeaderBlueprint.tsx index 73f09e4b22..b83136dcd9 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityHeaderBlueprint.tsx +++ b/plugins/catalog-react/src/alpha/blueprints/EntityHeaderBlueprint.tsx @@ -34,13 +34,11 @@ export const EntityHeaderBlueprint = createExtensionBlueprint({ attachTo: { id: 'page:catalog/entity', input: 'headers' }, dataRefs: { filterFunction: entityFilterFunctionDataRef, - filterExpression: entityFilterExpressionDataRef, element: coreExtensionData.reactElement, }, config: { schema: { - filter: z => - z.union([z.string(), createEntityPredicateSchema(z)]).optional(), + filter: z => createEntityPredicateSchema(z).optional(), }, }, output: [ @@ -51,7 +49,7 @@ export const EntityHeaderBlueprint = createExtensionBlueprint({ *factory( params: { loader: () => Promise; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: EntityPredicate | ((entity: Entity) => boolean); }, { node, config }, ) {