Merge pull request #30589 from backstage/blam/entity-header-filter

`nfs`: Support `filter` param on `EntityHeaderBlueprint`
This commit is contained in:
Ben Lambert
2025-07-22 10:20:18 +02:00
committed by GitHub
7 changed files with 118 additions and 39 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-react': patch
---
Support `filter` parameter on the `EntityHeaderBlueprint`
+5
View File
@@ -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
+34 -9
View File
@@ -372,18 +372,43 @@ export const EntityHeaderBlueprint: ExtensionBlueprint<{
name: undefined;
params: {
loader: () => Promise<JSX.Element>;
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',
@@ -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<JSX.Element>;
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),
+17 -10
View File
@@ -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<
@@ -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 ?? (
<EntityHeader
parentEntityRelations={parentEntityRelations}
UNSTABLE_contextMenuOptions={UNSTABLE_contextMenuOptions}
UNSTABLE_extraContextMenuItems={UNSTABLE_extraContextMenuItems}
contextMenuItems={contextMenuItems}
/>
);
const routes = useElementFilter(
children,
elements =>
@@ -142,14 +150,7 @@ export const EntityLayout = (props: EntityLayoutProps) => {
return (
<Page themeId={entity?.spec?.type?.toString() ?? 'home'}>
{header ?? (
<EntityHeader
parentEntityRelations={parentEntityRelations}
UNSTABLE_contextMenuOptions={UNSTABLE_contextMenuOptions}
UNSTABLE_extraContextMenuItems={UNSTABLE_extraContextMenuItems}
contextMenuItems={contextMenuItems}
/>
)}
{!loading && header}
{loading && <Progress />}
+21 -7
View File
@@ -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<Groups>(
(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 (
<AsyncEntityProvider {...entityFromUrl}>
<EntityLayout