feat: support filters in EntityHeader blueprint

Signed-off-by: benjdlambert <ben@blam.sh>

Signed-off-by: benjdlambert <ben@blam.sh>
This commit is contained in:
benjdlambert
2025-07-18 12:11:24 +02:00
parent d4ca0576cb
commit 2156d99515
3 changed files with 60 additions and 20 deletions
@@ -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<JSX.Element>;
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),
@@ -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 />}
+22 -7
View File
@@ -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<Groups>(
(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 (
<AsyncEntityProvider {...entityFromUrl}>
<EntityLayout