diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml
index 6a757f3cb2..6b8fb5ad57 100644
--- a/packages/app-next/app-config.yaml
+++ b/packages/app-next/app-config.yaml
@@ -26,11 +26,14 @@ app:
title: Custom
# Entity page cards
- - entity-card:catalog/about
+ - entity-card:catalog/about:
+ config:
+ area: info
- entity-card:catalog/labels
- entity-card:catalog/links:
config:
filter: kind:component has:links
+ area: info
# - entity-card:linguist/languages
- entity-card:catalog-graph/relations:
config:
@@ -68,6 +71,11 @@ app:
# - entity-content:azure-devops/pull-requests
# - entity-content:azure-devops/git-tags
+ - catalog-overview-entity-content-layout:app/sticky:
+ config:
+ # this layout will apply to entities of kind component
+ filter: 'kind:component'
+
# scmAuthExtension: >-
# createScmAuthExtension({
# id: 'apis.scmAuth.addons.ghe',
diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx
index 1992d5b35f..bb2998fb00 100644
--- a/packages/app-next/src/App.tsx
+++ b/packages/app-next/src/App.tsx
@@ -44,6 +44,7 @@ import kubernetesPlugin from '@backstage/plugin-kubernetes/alpha';
import { convertLegacyPlugin } from '@backstage/core-compat-api';
import { convertLegacyPageExtension } from '@backstage/core-compat-api';
import { convertLegacyEntityContentExtension } from '@backstage/plugin-catalog-react/alpha';
+import { customEntityContentOverviewLayoutModule } from './EntityPages';
/*
@@ -131,6 +132,7 @@ const app = createApp({
kubernetesPlugin,
notFoundErrorPageModule,
customHomePageModule,
+ customEntityContentOverviewLayoutModule,
...collectedLegacyPlugins,
],
/* Handled through config instead */
diff --git a/packages/app-next/src/EntityPages.tsx b/packages/app-next/src/EntityPages.tsx
new file mode 100644
index 0000000000..bee31f2c3c
--- /dev/null
+++ b/packages/app-next/src/EntityPages.tsx
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { useCallback } from 'react';
+import Grid from '@material-ui/core/Grid';
+import { createFrontendModule } from '@backstage/frontend-plugin-api';
+import {
+ OverviewEntityContentLauyoutBlueprint,
+ OverviewEntityContentLayoutProps,
+} from '@backstage/plugin-catalog-react/alpha';
+import { Entity } from '@backstage/catalog-model';
+import { useEntity } from '@backstage/plugin-catalog-react';
+import { makeStyles } from '@material-ui/core/styles';
+
+const useStyles = makeStyles(theme => ({
+ [theme.breakpoints.up('sm')]: {
+ infoArea: {
+ order: 1,
+ },
+ card: {
+ alignSelf: 'stretch',
+ '& > *': {
+ height: '100%',
+ minHeight: 400,
+ },
+ },
+ },
+}));
+
+function StickyEntityContentOverviewLayout(
+ props: OverviewEntityContentLayoutProps,
+) {
+ const { cards, buildFilterFn } = props;
+
+ const classes = useStyles();
+
+ const { entity } = useEntity();
+
+ const catalogEntityFilter = useCallback(
+ (options: {
+ filterFunction?: (entity: Entity) => boolean;
+ filterExpression?: string;
+ }) => {
+ const { filterFunction, filterExpression } = options;
+ return buildFilterFn(filterFunction, filterExpression)(entity);
+ },
+ [entity, buildFilterFn],
+ );
+
+ return (
+
+
+
+ {cards
+ .filter(catalogEntityFilter)
+ .filter(card => card.area === 'info')
+ .map((card, index) => (
+
+ {card.element}
+
+ ))}
+
+
+
+
+ {cards
+ .filter(catalogEntityFilter)
+ .filter(card => card.area === 'glance')
+ .map((card, index) => (
+
+ {card.element}
+
+ ))}
+ {cards
+ .filter(catalogEntityFilter)
+ .filter(card => !card.area || card.area === 'main')
+ .map((card, index) => (
+
+ {card.element}
+
+ ))}
+
+
+
+ );
+}
+
+export const customEntityContentOverviewLayoutModule = createFrontendModule({
+ pluginId: 'app',
+ extensions: [
+ OverviewEntityContentLauyoutBlueprint.make({
+ name: 'sticky',
+ params: {
+ areas: ['info', 'glance', 'main'],
+ defaultArea: 'main',
+ loader: async () => StickyEntityContentOverviewLayout,
+ },
+ }),
+ ],
+});
diff --git a/plugins/api-docs/report-alpha.api.md b/plugins/api-docs/report-alpha.api.md
index c1529c3028..d0af0eb55d 100644
--- a/plugins/api-docs/report-alpha.api.md
+++ b/plugins/api-docs/report-alpha.api.md
@@ -108,9 +108,11 @@ const _default: FrontendPlugin<
name: 'has-apis';
config: {
filter: string | undefined;
+ area: string | undefined;
};
configInput: {
filter?: string | undefined;
+ area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<
@@ -131,11 +133,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
+ >
+ | ConfigurableExtensionDataRef<
+ string,
+ 'catalog.entity-card-area',
+ {
+ optional: true;
+ }
>;
inputs: {};
params: {
loader: () => Promise;
filter?: string | ((entity: Entity) => boolean) | undefined;
+ defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:api-docs/definition': ExtensionDefinition<{
@@ -143,9 +153,11 @@ const _default: FrontendPlugin<
name: 'definition';
config: {
filter: string | undefined;
+ area: string | undefined;
};
configInput: {
filter?: string | undefined;
+ area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<
@@ -166,11 +178,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
+ >
+ | ConfigurableExtensionDataRef<
+ string,
+ 'catalog.entity-card-area',
+ {
+ optional: true;
+ }
>;
inputs: {};
params: {
loader: () => Promise;
filter?: string | ((entity: Entity) => boolean) | undefined;
+ defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:api-docs/consumed-apis': ExtensionDefinition<{
@@ -178,9 +198,11 @@ const _default: FrontendPlugin<
name: 'consumed-apis';
config: {
filter: string | undefined;
+ area: string | undefined;
};
configInput: {
filter?: string | undefined;
+ area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<
@@ -201,11 +223,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
+ >
+ | ConfigurableExtensionDataRef<
+ string,
+ 'catalog.entity-card-area',
+ {
+ optional: true;
+ }
>;
inputs: {};
params: {
loader: () => Promise;
filter?: string | ((entity: Entity) => boolean) | undefined;
+ defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:api-docs/provided-apis': ExtensionDefinition<{
@@ -213,9 +243,11 @@ const _default: FrontendPlugin<
name: 'provided-apis';
config: {
filter: string | undefined;
+ area: string | undefined;
};
configInput: {
filter?: string | undefined;
+ area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<
@@ -236,11 +268,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
+ >
+ | ConfigurableExtensionDataRef<
+ string,
+ 'catalog.entity-card-area',
+ {
+ optional: true;
+ }
>;
inputs: {};
params: {
loader: () => Promise;
filter?: string | ((entity: Entity) => boolean) | undefined;
+ defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:api-docs/consuming-components': ExtensionDefinition<{
@@ -248,9 +288,11 @@ const _default: FrontendPlugin<
name: 'consuming-components';
config: {
filter: string | undefined;
+ area: string | undefined;
};
configInput: {
filter?: string | undefined;
+ area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<
@@ -271,11 +313,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
+ >
+ | ConfigurableExtensionDataRef<
+ string,
+ 'catalog.entity-card-area',
+ {
+ optional: true;
+ }
>;
inputs: {};
params: {
loader: () => Promise;
filter?: string | ((entity: Entity) => boolean) | undefined;
+ defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:api-docs/providing-components': ExtensionDefinition<{
@@ -283,9 +333,11 @@ const _default: FrontendPlugin<
name: 'providing-components';
config: {
filter: string | undefined;
+ area: string | undefined;
};
configInput: {
filter?: string | undefined;
+ area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<
@@ -306,11 +358,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
+ >
+ | ConfigurableExtensionDataRef<
+ string,
+ 'catalog.entity-card-area',
+ {
+ optional: true;
+ }
>;
inputs: {};
params: {
loader: () => Promise;
filter?: string | ((entity: Entity) => boolean) | undefined;
+ defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-content:api-docs/definition': ExtensionDefinition<{
diff --git a/plugins/catalog-graph/report-alpha.api.md b/plugins/catalog-graph/report-alpha.api.md
index f4699e9ceb..f9f942ba71 100644
--- a/plugins/catalog-graph/report-alpha.api.md
+++ b/plugins/catalog-graph/report-alpha.api.md
@@ -43,6 +43,7 @@ const _default: FrontendPlugin<
height: number | undefined;
} & {
filter: string | undefined;
+ area: string | undefined;
};
configInput: {
height?: number | undefined;
@@ -58,6 +59,7 @@ const _default: FrontendPlugin<
relationPairs?: [string, string][] | undefined;
} & {
filter?: string | undefined;
+ area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<
@@ -78,6 +80,13 @@ const _default: FrontendPlugin<
{
optional: true;
}
+ >
+ | ConfigurableExtensionDataRef<
+ string,
+ 'catalog.entity-card-area',
+ {
+ optional: true;
+ }
>;
inputs: {
[x: string]: ExtensionInput<
@@ -93,6 +102,7 @@ const _default: FrontendPlugin<
params: {
loader: () => Promise;
filter?: string | ((entity: Entity) => boolean) | undefined;
+ defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'page:catalog-graph': ExtensionDefinition<{
diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md
index 259e6b4d4c..5d9304566e 100644
--- a/plugins/catalog-react/report-alpha.api.md
+++ b/plugins/catalog-react/report-alpha.api.md
@@ -12,6 +12,7 @@ import { Entity } from '@backstage/catalog-model';
import { ExtensionBlueprint } from '@backstage/frontend-plugin-api';
import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
import { JSX as JSX_2 } from 'react';
+import { default as React_2 } from 'react';
import { ResourcePermission } from '@backstage/plugin-permission-common';
import { RouteRef } from '@backstage/frontend-plugin-api';
import { TranslationRef } from '@backstage/core-plugin-api/alpha';
@@ -113,6 +114,7 @@ export const EntityCardBlueprint: ExtensionBlueprint<{
params: {
loader: () => Promise;
filter?: string | ((entity: Entity) => boolean) | undefined;
+ defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
output:
| ConfigurableExtensionDataRef
@@ -129,13 +131,22 @@ export const EntityCardBlueprint: ExtensionBlueprint<{
{
optional: true;
}
+ >
+ | ConfigurableExtensionDataRef<
+ string,
+ 'catalog.entity-card-area',
+ {
+ optional: true;
+ }
>;
inputs: {};
config: {
filter: string | undefined;
+ area: string | undefined;
};
configInput: {
filter?: string | undefined;
+ area?: string | undefined;
};
dataRefs: {
filterFunction: ConfigurableExtensionDataRef<
@@ -148,6 +159,7 @@ export const EntityCardBlueprint: ExtensionBlueprint<{
'catalog.entity-filter-expression',
{}
>;
+ area: ConfigurableExtensionDataRef;
};
}>;
@@ -240,6 +252,98 @@ export const EntityContentBlueprint: ExtensionBlueprint<{
// @alpha
export function isOwnerOf(owner: Entity, entity: Entity): boolean;
+// @alpha (undocumented)
+export const OverviewEntityContentLauyoutBlueprint: ExtensionBlueprint<{
+ kind: 'catalog-overview-entity-content-layout';
+ name: undefined;
+ params: {
+ areas: Array;
+ defaultArea: string;
+ defaultFilter?: string | ((entity: Entity) => boolean) | undefined;
+ loader: () => Promise<
+ (props: OverviewEntityContentLayoutProps) => React_2.JSX.Element
+ >;
+ };
+ output:
+ | ConfigurableExtensionDataRef
+ | ConfigurableExtensionDataRef<
+ string[],
+ 'catalog.overview-entity-content.layout.areas',
+ {}
+ >
+ | ConfigurableExtensionDataRef<
+ (props: OverviewEntityContentLayoutProps) => React_2.JSX.Element,
+ 'catalog.overview-entity-content.layout.page-component',
+ {}
+ >
+ | ConfigurableExtensionDataRef<
+ (entity: Entity) => boolean,
+ 'catalog.overview-entity-content.layout.filter-function',
+ {
+ optional: true;
+ }
+ >
+ | ConfigurableExtensionDataRef<
+ string,
+ 'catalog.overview-entity-content.layout.filter-expression',
+ {
+ optional: true;
+ }
+ >;
+ inputs: {};
+ config: {
+ area: string | undefined;
+ filter: string | undefined;
+ };
+ configInput: {
+ filter?: string | undefined;
+ area?: string | undefined;
+ };
+ dataRefs: {
+ areas: ConfigurableExtensionDataRef<
+ string[],
+ 'catalog.overview-entity-content.layout.areas',
+ {}
+ >;
+ defaultArea: ConfigurableExtensionDataRef<
+ string,
+ 'catalog.entity-card-area',
+ {}
+ >;
+ filterFunction: ConfigurableExtensionDataRef<
+ (entity: Entity) => boolean,
+ 'catalog.overview-entity-content.layout.filter-function',
+ {}
+ >;
+ filterExpression: ConfigurableExtensionDataRef<
+ string,
+ 'catalog.overview-entity-content.layout.filter-expression',
+ {}
+ >;
+ component: ConfigurableExtensionDataRef<
+ (props: OverviewEntityContentLayoutProps) => React_2.JSX.Element,
+ 'catalog.overview-entity-content.layout.page-component',
+ {}
+ >;
+ };
+}>;
+
+// @alpha (undocumented)
+export interface OverviewEntityContentLayoutProps {
+ // (undocumented)
+ buildFilterFn: (
+ filterFunction?: (entity: Entity) => boolean,
+ filterExpression?: string,
+ ) => (entity: Entity) => boolean;
+ // (undocumented)
+ cards: Array<{
+ area?: string;
+ element: React_2.JSX.Element;
+ filterFunction?: (entity: Entity) => boolean;
+ filterExpression?: string;
+ }>;
+}
+
// @alpha
export function useEntityPermission(
permission: ResourcePermission<'catalog-entity'>,
diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.test.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.test.tsx
index 9f012f6055..eba76c6e05 100644
--- a/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.test.tsx
+++ b/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.test.tsx
@@ -51,6 +51,9 @@ describe('EntityCardBlueprint', () => {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
+ "area": {
+ "type": "string",
+ },
"filter": {
"type": "string",
},
@@ -83,6 +86,15 @@ describe('EntityCardBlueprint', () => {
"optional": [Function],
"toString": [Function],
},
+ {
+ "$$type": "@backstage/ExtensionDataRef",
+ "config": {
+ "optional": true,
+ },
+ "id": "catalog.entity-card-area",
+ "optional": [Function],
+ "toString": [Function],
+ },
],
"override": [Function],
"toString": [Function],
diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.ts b/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.ts
index 82ff2df44a..0a935d494e 100644
--- a/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.ts
+++ b/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.ts
@@ -22,6 +22,8 @@ import {
import {
entityFilterFunctionDataRef,
entityFilterExpressionDataRef,
+ entityCardAreaDataRef,
+ defaultEntityCardArea,
} from './extensionData';
/**
@@ -35,25 +37,30 @@ export const EntityCardBlueprint = createExtensionBlueprint({
coreExtensionData.reactElement,
entityFilterFunctionDataRef.optional(),
entityFilterExpressionDataRef.optional(),
+ entityCardAreaDataRef.optional(),
],
dataRefs: {
filterFunction: entityFilterFunctionDataRef,
filterExpression: entityFilterExpressionDataRef,
+ area: entityCardAreaDataRef,
},
config: {
schema: {
filter: z => z.string().optional(),
+ area: z => z.string().optional(),
},
},
*factory(
{
loader,
filter,
+ defaultArea,
}: {
loader: () => Promise;
filter?:
| typeof entityFilterFunctionDataRef.T
| typeof entityFilterExpressionDataRef.T;
+ defaultArea?: (typeof defaultEntityCardArea)[number];
},
{ node, config },
) {
@@ -66,5 +73,10 @@ export const EntityCardBlueprint = createExtensionBlueprint({
} else if (typeof filter === 'function') {
yield entityFilterFunctionDataRef(filter);
}
+
+ const area = config.area ?? defaultArea;
+ if (area) {
+ yield entityCardAreaDataRef(area);
+ }
},
});
diff --git a/plugins/catalog-react/src/alpha/blueprints/OverviewEntityContentLauyoutBlueprint.tsx b/plugins/catalog-react/src/alpha/blueprints/OverviewEntityContentLauyoutBlueprint.tsx
new file mode 100644
index 0000000000..2c5a7a510a
--- /dev/null
+++ b/plugins/catalog-react/src/alpha/blueprints/OverviewEntityContentLauyoutBlueprint.tsx
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2025 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Entity } from '@backstage/catalog-model';
+import {
+ createExtensionDataRef,
+ createExtensionBlueprint,
+ ExtensionBoundary,
+} from '@backstage/frontend-plugin-api';
+import React, { lazy as reactLazy, ComponentProps } from 'react';
+import { EntityCardBlueprint } from './EntityCardBlueprint';
+
+/** @alpha */
+export interface OverviewEntityContentLayoutProps {
+ buildFilterFn: (
+ filterFunction?: (entity: Entity) => boolean,
+ filterExpression?: string,
+ ) => (entity: Entity) => boolean;
+ cards: Array<{
+ area?: string;
+ element: React.JSX.Element;
+ filterFunction?: (entity: Entity) => boolean;
+ filterExpression?: string;
+ }>;
+}
+
+const catalogOverviewEntityContentLayoutAreasDataRef = createExtensionDataRef<
+ Array
+>().with({
+ id: 'catalog.overview-entity-content.layout.areas',
+});
+
+const catalogOverviewEntityContentLayoutFilterFunctionDataRef =
+ createExtensionDataRef<(entity: Entity) => boolean>().with({
+ id: 'catalog.overview-entity-content.layout.filter-function',
+ });
+
+const catalogOverviewEntityContentLayoutFilterExpressionDataRef =
+ createExtensionDataRef().with({
+ id: 'catalog.overview-entity-content.layout.filter-expression',
+ });
+
+const catalogOverviewEntityContentLayoutComponentDataRef =
+ createExtensionDataRef<
+ (props: OverviewEntityContentLayoutProps) => React.JSX.Element
+ >().with({
+ id: 'catalog.overview-entity-content.layout.page-component',
+ });
+
+/** @alpha */
+export const OverviewEntityContentLauyoutBlueprint = createExtensionBlueprint({
+ kind: 'catalog-overview-entity-content-layout',
+ attachTo: { id: 'entity-content:catalog/overview', input: 'layouts' },
+ output: [
+ catalogOverviewEntityContentLayoutAreasDataRef,
+ EntityCardBlueprint.dataRefs.area,
+ catalogOverviewEntityContentLayoutFilterFunctionDataRef.optional(),
+ catalogOverviewEntityContentLayoutFilterExpressionDataRef.optional(),
+ catalogOverviewEntityContentLayoutComponentDataRef,
+ ],
+ dataRefs: {
+ areas: catalogOverviewEntityContentLayoutAreasDataRef,
+ defaultArea: EntityCardBlueprint.dataRefs.area,
+ filterFunction: catalogOverviewEntityContentLayoutFilterFunctionDataRef,
+ filterExpression: catalogOverviewEntityContentLayoutFilterExpressionDataRef,
+ component: catalogOverviewEntityContentLayoutComponentDataRef,
+ },
+ config: {
+ schema: {
+ area: z => z.string().optional(),
+ filter: z => z.string().optional(),
+ },
+ },
+ *factory(
+ {
+ loader,
+ areas,
+ defaultArea,
+ defaultFilter = () => false, // do not use the layout if a default filter is undefined
+ }: {
+ areas: Array;
+ defaultArea: (typeof areas)[number]; // fixed typescript area type
+ defaultFilter?:
+ | typeof catalogOverviewEntityContentLayoutFilterFunctionDataRef.T
+ | typeof catalogOverviewEntityContentLayoutFilterExpressionDataRef.T;
+ loader: () => Promise<
+ (props: OverviewEntityContentLayoutProps) => React.JSX.Element
+ >;
+ },
+ { node, config },
+ ) {
+ yield catalogOverviewEntityContentLayoutAreasDataRef(areas);
+
+ yield EntityCardBlueprint.dataRefs.area(config.area ?? defaultArea);
+
+ if (config.filter) {
+ yield catalogOverviewEntityContentLayoutFilterExpressionDataRef(
+ config.filter,
+ );
+ } else if (typeof defaultFilter === 'string') {
+ yield catalogOverviewEntityContentLayoutFilterExpressionDataRef(
+ defaultFilter,
+ );
+ } else if (typeof defaultFilter === 'function') {
+ yield catalogOverviewEntityContentLayoutFilterFunctionDataRef(
+ defaultFilter,
+ );
+ }
+
+ const ExtensionComponent = reactLazy(() =>
+ loader().then(component => ({ default: component })),
+ );
+
+ yield catalogOverviewEntityContentLayoutComponentDataRef(
+ (props: ComponentProps) => {
+ return (
+
+
+
+ );
+ },
+ );
+ },
+});
diff --git a/plugins/catalog-react/src/alpha/blueprints/extensionData.tsx b/plugins/catalog-react/src/alpha/blueprints/extensionData.tsx
index 69bd4d47c3..1d714e8164 100644
--- a/plugins/catalog-react/src/alpha/blueprints/extensionData.tsx
+++ b/plugins/catalog-react/src/alpha/blueprints/extensionData.tsx
@@ -50,3 +50,14 @@ export const entityContentGroupDataRef = createExtensionDataRef<
>().with({
id: 'catalog.entity-content-group',
});
+
+/**
+ * @alpha
+ * Default entity content groups.
+ */
+export const defaultEntityCardArea = ['info', 'glance', 'main'] as const;
+
+/** @internal */
+export const entityCardAreaDataRef = createExtensionDataRef().with({
+ id: 'catalog.entity-card-area',
+});
diff --git a/plugins/catalog-react/src/alpha/blueprints/index.ts b/plugins/catalog-react/src/alpha/blueprints/index.ts
index 1a3b9941f7..744eb729bd 100644
--- a/plugins/catalog-react/src/alpha/blueprints/index.ts
+++ b/plugins/catalog-react/src/alpha/blueprints/index.ts
@@ -15,4 +15,8 @@
*/
export { EntityCardBlueprint } from './EntityCardBlueprint';
export { EntityContentBlueprint } from './EntityContentBlueprint';
+export {
+ OverviewEntityContentLauyoutBlueprint,
+ type OverviewEntityContentLayoutProps,
+} from './OverviewEntityContentLauyoutBlueprint';
export { defaultEntityContentGroups } from './extensionData';
diff --git a/plugins/catalog/report-alpha.api.md b/plugins/catalog/report-alpha.api.md
index ec53d87a31..eaa47846d1 100644
--- a/plugins/catalog/report-alpha.api.md
+++ b/plugins/catalog/report-alpha.api.md
@@ -17,6 +17,7 @@ import { ExternalRouteRef } from '@backstage/frontend-plugin-api';
import { FrontendPlugin } from '@backstage/frontend-plugin-api';
import { IconComponent } from '@backstage/core-plugin-api';
import { JSX as JSX_2 } from 'react';
+import { OverviewEntityContentLayoutProps } from '@backstage/plugin-catalog-react/alpha';
import { RouteRef } from '@backstage/frontend-plugin-api';
import { SearchResultItemExtensionComponent } from '@backstage/plugin-search-react/alpha';
import { SearchResultItemExtensionPredicate } from '@backstage/plugin-search-react/alpha';
@@ -217,9 +218,11 @@ const _default: FrontendPlugin<
name: 'about';
config: {
filter: string | undefined;
+ area: string | undefined;
};
configInput: {
filter?: string | undefined;
+ area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef
@@ -236,11 +239,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
+ >
+ | ConfigurableExtensionDataRef<
+ string,
+ 'catalog.entity-card-area',
+ {
+ optional: true;
+ }
>;
inputs: {};
params: {
loader: () => Promise;
filter?: string | ((entity: Entity) => boolean) | undefined;
+ defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:catalog/links': ExtensionDefinition<{
@@ -248,9 +259,11 @@ const _default: FrontendPlugin<
name: 'links';
config: {
filter: string | undefined;
+ area: string | undefined;
};
configInput: {
filter?: string | undefined;
+ area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef
@@ -267,11 +280,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
+ >
+ | ConfigurableExtensionDataRef<
+ string,
+ 'catalog.entity-card-area',
+ {
+ optional: true;
+ }
>;
inputs: {};
params: {
loader: () => Promise;
filter?: string | ((entity: Entity) => boolean) | undefined;
+ defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:catalog/labels': ExtensionDefinition<{
@@ -279,9 +300,11 @@ const _default: FrontendPlugin<
name: 'labels';
config: {
filter: string | undefined;
+ area: string | undefined;
};
configInput: {
filter?: string | undefined;
+ area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef
@@ -298,11 +321,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
+ >
+ | ConfigurableExtensionDataRef<
+ string,
+ 'catalog.entity-card-area',
+ {
+ optional: true;
+ }
>;
inputs: {};
params: {
loader: () => Promise;
filter?: string | ((entity: Entity) => boolean) | undefined;
+ defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:catalog/depends-on-components': ExtensionDefinition<{
@@ -310,9 +341,11 @@ const _default: FrontendPlugin<
name: 'depends-on-components';
config: {
filter: string | undefined;
+ area: string | undefined;
};
configInput: {
filter?: string | undefined;
+ area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef
@@ -329,11 +362,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
+ >
+ | ConfigurableExtensionDataRef<
+ string,
+ 'catalog.entity-card-area',
+ {
+ optional: true;
+ }
>;
inputs: {};
params: {
loader: () => Promise;
filter?: string | ((entity: Entity) => boolean) | undefined;
+ defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:catalog/depends-on-resources': ExtensionDefinition<{
@@ -341,9 +382,11 @@ const _default: FrontendPlugin<
name: 'depends-on-resources';
config: {
filter: string | undefined;
+ area: string | undefined;
};
configInput: {
filter?: string | undefined;
+ area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef
@@ -360,11 +403,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
+ >
+ | ConfigurableExtensionDataRef<
+ string,
+ 'catalog.entity-card-area',
+ {
+ optional: true;
+ }
>;
inputs: {};
params: {
loader: () => Promise;
filter?: string | ((entity: Entity) => boolean) | undefined;
+ defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:catalog/has-components': ExtensionDefinition<{
@@ -372,9 +423,11 @@ const _default: FrontendPlugin<
name: 'has-components';
config: {
filter: string | undefined;
+ area: string | undefined;
};
configInput: {
filter?: string | undefined;
+ area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef
@@ -391,11 +444,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
+ >
+ | ConfigurableExtensionDataRef<
+ string,
+ 'catalog.entity-card-area',
+ {
+ optional: true;
+ }
>;
inputs: {};
params: {
loader: () => Promise;
filter?: string | ((entity: Entity) => boolean) | undefined;
+ defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:catalog/has-resources': ExtensionDefinition<{
@@ -403,9 +464,11 @@ const _default: FrontendPlugin<
name: 'has-resources';
config: {
filter: string | undefined;
+ area: string | undefined;
};
configInput: {
filter?: string | undefined;
+ area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef
@@ -422,11 +485,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
+ >
+ | ConfigurableExtensionDataRef<
+ string,
+ 'catalog.entity-card-area',
+ {
+ optional: true;
+ }
>;
inputs: {};
params: {
loader: () => Promise;
filter?: string | ((entity: Entity) => boolean) | undefined;
+ defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:catalog/has-subcomponents': ExtensionDefinition<{
@@ -434,9 +505,11 @@ const _default: FrontendPlugin<
name: 'has-subcomponents';
config: {
filter: string | undefined;
+ area: string | undefined;
};
configInput: {
filter?: string | undefined;
+ area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef
@@ -453,11 +526,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
+ >
+ | ConfigurableExtensionDataRef<
+ string,
+ 'catalog.entity-card-area',
+ {
+ optional: true;
+ }
>;
inputs: {};
params: {
loader: () => Promise;
filter?: string | ((entity: Entity) => boolean) | undefined;
+ defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:catalog/has-subdomains': ExtensionDefinition<{
@@ -465,9 +546,11 @@ const _default: FrontendPlugin<
name: 'has-subdomains';
config: {
filter: string | undefined;
+ area: string | undefined;
};
configInput: {
filter?: string | undefined;
+ area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef
@@ -484,11 +567,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
+ >
+ | ConfigurableExtensionDataRef<
+ string,
+ 'catalog.entity-card-area',
+ {
+ optional: true;
+ }
>;
inputs: {};
params: {
loader: () => Promise;
filter?: string | ((entity: Entity) => boolean) | undefined;
+ defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:catalog/has-systems': ExtensionDefinition<{
@@ -496,9 +587,11 @@ const _default: FrontendPlugin<
name: 'has-systems';
config: {
filter: string | undefined;
+ area: string | undefined;
};
configInput: {
filter?: string | undefined;
+ area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef
@@ -515,11 +608,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
+ >
+ | ConfigurableExtensionDataRef<
+ string,
+ 'catalog.entity-card-area',
+ {
+ optional: true;
+ }
>;
inputs: {};
params: {
loader: () => Promise;
filter?: string | ((entity: Entity) => boolean) | undefined;
+ defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-content:catalog/overview': ExtensionDefinition<{
@@ -572,6 +673,37 @@ const _default: FrontendPlugin<
}
>;
inputs: {
+ layouts: ExtensionInput<
+ | ConfigurableExtensionDataRef
+ | ConfigurableExtensionDataRef<
+ string[],
+ 'catalog.overview-entity-content.layout.areas',
+ {}
+ >
+ | ConfigurableExtensionDataRef<
+ (props: OverviewEntityContentLayoutProps) => JSX_2.Element,
+ 'catalog.overview-entity-content.layout.page-component',
+ {}
+ >
+ | ConfigurableExtensionDataRef<
+ (entity: Entity) => boolean,
+ 'catalog.overview-entity-content.layout.filter-function',
+ {
+ optional: true;
+ }
+ >
+ | ConfigurableExtensionDataRef<
+ string,
+ 'catalog.overview-entity-content.layout.filter-expression',
+ {
+ optional: true;
+ }
+ >,
+ {
+ singleton: false;
+ optional: false;
+ }
+ >;
cards: ExtensionInput<
| ConfigurableExtensionDataRef
| ConfigurableExtensionDataRef<
@@ -587,6 +719,13 @@ const _default: FrontendPlugin<
{
optional: true;
}
+ >
+ | ConfigurableExtensionDataRef<
+ string,
+ 'catalog.entity-card-area',
+ {
+ optional: true;
+ }
>,
{
singleton: false;
diff --git a/plugins/catalog/src/alpha/entityContents.tsx b/plugins/catalog/src/alpha/entityContents.tsx
index 259b92492c..c3277ab3a8 100644
--- a/plugins/catalog/src/alpha/entityContents.tsx
+++ b/plugins/catalog/src/alpha/entityContents.tsx
@@ -19,38 +19,85 @@ import {
coreExtensionData,
createExtensionInput,
} from '@backstage/frontend-plugin-api';
-import { EntityContentBlueprint } from '@backstage/plugin-catalog-react/alpha';
+import {
+ EntityCardBlueprint,
+ EntityContentBlueprint,
+ OverviewEntityContentLauyoutBlueprint,
+} from '@backstage/plugin-catalog-react/alpha';
+import { buildFilterFn } from './filter/FilterWrapper';
+import { useEntity } from '@backstage/plugin-catalog-react';
-export const catalogOverviewEntityContent =
- EntityContentBlueprint.makeWithOverrides({
- name: 'overview',
- inputs: {
- cards: createExtensionInput([
- coreExtensionData.reactElement,
- EntityContentBlueprint.dataRefs.filterFunction.optional(),
- EntityContentBlueprint.dataRefs.filterExpression.optional(),
- ]),
- },
- factory: (originalFactory, { inputs }) => {
- return originalFactory({
- defaultPath: '/',
- defaultTitle: 'Overview',
- loader: async () =>
- import('./EntityOverviewPage').then(m => (
- ({
- element: c.get(coreExtensionData.reactElement),
- filterFunction: c.get(
+const catalogOverviewEntityContent = EntityContentBlueprint.makeWithOverrides({
+ name: 'overview',
+ inputs: {
+ layouts: createExtensionInput([
+ OverviewEntityContentLauyoutBlueprint.dataRefs.areas,
+ OverviewEntityContentLauyoutBlueprint.dataRefs.defaultArea,
+ OverviewEntityContentLauyoutBlueprint.dataRefs.filterFunction.optional(),
+ OverviewEntityContentLauyoutBlueprint.dataRefs.filterExpression.optional(),
+ OverviewEntityContentLauyoutBlueprint.dataRefs.component,
+ ]),
+ cards: createExtensionInput([
+ coreExtensionData.reactElement,
+ EntityContentBlueprint.dataRefs.filterFunction.optional(),
+ EntityContentBlueprint.dataRefs.filterExpression.optional(),
+ EntityCardBlueprint.dataRefs.area.optional(),
+ ]),
+ },
+ factory: (originalFactory, { inputs }) => {
+ return originalFactory({
+ defaultPath: '/',
+ defaultTitle: 'Overview',
+ loader: async () => {
+ const defaultLayout = await import('./EntityOverviewPage').then(
+ m => m.EntityOverviewPage,
+ );
+
+ const Component = () => {
+ const { entity } = useEntity();
+
+ // Use the first layout that matches the entity filter
+ const layout = inputs.layouts.find(l => {
+ const filterFunction = l.get(
+ OverviewEntityContentLauyoutBlueprint.dataRefs.filterFunction,
+ );
+ const filterExpression = l.get(
+ OverviewEntityContentLauyoutBlueprint.dataRefs.filterExpression,
+ );
+
+ return buildFilterFn(filterFunction, filterExpression)(entity);
+ });
+
+ const Layout =
+ layout?.get(
+ OverviewEntityContentLauyoutBlueprint.dataRefs.component,
+ ) ?? defaultLayout;
+
+ return (
+ ({
+ element: card.get(coreExtensionData.reactElement),
+ filterFunction: card.get(
EntityContentBlueprint.dataRefs.filterFunction,
),
- filterExpression: c.get(
+ filterExpression: card.get(
EntityContentBlueprint.dataRefs.filterExpression,
),
+ area:
+ card.get(EntityCardBlueprint.dataRefs.area) ??
+ layout?.get(
+ OverviewEntityContentLauyoutBlueprint.dataRefs.defaultArea,
+ ),
}))}
/>
- )),
- });
- },
- });
+ );
+ };
+
+ return ;
+ },
+ });
+ },
+});
export default [catalogOverviewEntityContent];
diff --git a/plugins/org/report-alpha.api.md b/plugins/org/report-alpha.api.md
index 1040b61733..1c1bb4b4a4 100644
--- a/plugins/org/report-alpha.api.md
+++ b/plugins/org/report-alpha.api.md
@@ -22,9 +22,11 @@ const _default: FrontendPlugin<
name: 'group-profile';
config: {
filter: string | undefined;
+ area: string | undefined;
};
configInput: {
filter?: string | undefined;
+ area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<
@@ -45,11 +47,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
+ >
+ | ConfigurableExtensionDataRef<
+ string,
+ 'catalog.entity-card-area',
+ {
+ optional: true;
+ }
>;
inputs: {};
params: {
loader: () => Promise;
filter?: string | ((entity: Entity) => boolean) | undefined;
+ defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:org/members-list': ExtensionDefinition<{
@@ -57,9 +67,11 @@ const _default: FrontendPlugin<
name: 'members-list';
config: {
filter: string | undefined;
+ area: string | undefined;
};
configInput: {
filter?: string | undefined;
+ area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<
@@ -80,11 +92,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
+ >
+ | ConfigurableExtensionDataRef<
+ string,
+ 'catalog.entity-card-area',
+ {
+ optional: true;
+ }
>;
inputs: {};
params: {
loader: () => Promise;
filter?: string | ((entity: Entity) => boolean) | undefined;
+ defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:org/ownership': ExtensionDefinition<{
@@ -92,9 +112,11 @@ const _default: FrontendPlugin<
name: 'ownership';
config: {
filter: string | undefined;
+ area: string | undefined;
};
configInput: {
filter?: string | undefined;
+ area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<
@@ -115,11 +137,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
+ >
+ | ConfigurableExtensionDataRef<
+ string,
+ 'catalog.entity-card-area',
+ {
+ optional: true;
+ }
>;
inputs: {};
params: {
loader: () => Promise;
filter?: string | ((entity: Entity) => boolean) | undefined;
+ defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
'entity-card:org/user-profile': ExtensionDefinition<{
@@ -127,9 +157,11 @@ const _default: FrontendPlugin<
name: 'user-profile';
config: {
filter: string | undefined;
+ area: string | undefined;
};
configInput: {
filter?: string | undefined;
+ area?: string | undefined;
};
output:
| ConfigurableExtensionDataRef<
@@ -150,11 +182,19 @@ const _default: FrontendPlugin<
{
optional: true;
}
+ >
+ | ConfigurableExtensionDataRef<
+ string,
+ 'catalog.entity-card-area',
+ {
+ optional: true;
+ }
>;
inputs: {};
params: {
loader: () => Promise;
filter?: string | ((entity: Entity) => boolean) | undefined;
+ defaultArea?: 'main' | 'info' | 'glance' | undefined;
};
}>;
}