From 761b56605d4a08a9142a448d91219afd5f3ee61c Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Thu, 6 Feb 2025 20:01:46 +0100 Subject: [PATCH] feat: creat entity content layout extension Signed-off-by: Camila Belo --- packages/app-next/app-config.yaml | 10 +- packages/app-next/src/App.tsx | 2 + packages/app-next/src/EntityPages.tsx | 123 ++++++++++++++++ plugins/api-docs/report-alpha.api.md | 60 ++++++++ plugins/catalog-graph/report-alpha.api.md | 10 ++ plugins/catalog-react/report-alpha.api.md | 104 +++++++++++++ .../blueprints/EntityCardBlueprint.test.tsx | 12 ++ .../alpha/blueprints/EntityCardBlueprint.ts | 12 ++ .../OverviewEntityContentLauyoutBlueprint.tsx | 137 +++++++++++++++++ .../src/alpha/blueprints/extensionData.tsx | 11 ++ .../src/alpha/blueprints/index.ts | 4 + plugins/catalog/report-alpha.api.md | 139 ++++++++++++++++++ plugins/catalog/src/alpha/entityContents.tsx | 99 +++++++++---- plugins/org/report-alpha.api.md | 40 +++++ 14 files changed, 736 insertions(+), 27 deletions(-) create mode 100644 packages/app-next/src/EntityPages.tsx create mode 100644 plugins/catalog-react/src/alpha/blueprints/OverviewEntityContentLauyoutBlueprint.tsx 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; }; }>; }