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 diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md index 8476eed695..1db7a8e5e4 100644 --- a/plugins/catalog-react/report-alpha.api.md +++ b/plugins/catalog-react/report-alpha.api.md @@ -372,18 +372,43 @@ export const EntityHeaderBlueprint: ExtensionBlueprint<{ name: undefined; params: { loader: () => Promise; + filter?: 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', + {} + >; 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 431197e753..b83136dcd9 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityHeaderBlueprint.tsx +++ b/plugins/catalog-react/src/alpha/blueprints/EntityHeaderBlueprint.tsx @@ -19,22 +19,44 @@ 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, element: coreExtensionData.reactElement, }, - output: [coreExtensionData.reactElement.optional()], + config: { + schema: { + filter: z => createEntityPredicateSchema(z).optional(), + }, + }, + output: [ + entityFilterFunctionDataRef.optional(), + entityFilterExpressionDataRef.optional(), + coreExtensionData.reactElement.optional(), + ], *factory( params: { loader: () => Promise; + filter?: 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/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/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..f0c5316a84 100644 --- a/plugins/catalog/src/alpha/pages.tsx +++ b/plugins/catalog/src/alpha/pages.tsx @@ -79,10 +79,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 +124,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 +179,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 (