From 87baa188ddf8d6559056ead966e650ea54113fa3 Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Fri, 28 Feb 2025 12:13:43 +0100 Subject: [PATCH] refactor: keep only the ability of overriding a defautl header for now Signed-off-by: Camila Belo --- .changeset/brave-ears-bow.md | 2 +- .changeset/nervous-cups-happen.md | 82 +--------------- packages/app-next/app-config.yaml | 2 +- plugins/catalog-react/report-alpha.api.md | 95 +++---------------- .../blueprints/EntityHeaderBlueprint.tsx | 75 ++------------- plugins/catalog/report-alpha.api.md | 52 ++-------- .../components/EntityHeader/EntityHeader.tsx | 25 ++--- plugins/catalog/src/alpha/pages.test.tsx | 1 - plugins/catalog/src/alpha/pages.tsx | 40 ++------ 9 files changed, 50 insertions(+), 324 deletions(-) diff --git a/.changeset/brave-ears-bow.md b/.changeset/brave-ears-bow.md index 49fd16dedc..f89f4ebacf 100644 --- a/.changeset/brave-ears-bow.md +++ b/.changeset/brave-ears-bow.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog': minor --- -Now multiple entity page headers can be configured based on an entity filter. +Now a custom entity page header can be passed as input to the default entity page. diff --git a/.changeset/nervous-cups-happen.md b/.changeset/nervous-cups-happen.md index 40031a2170..f42933dd6c 100644 --- a/.changeset/nervous-cups-happen.md +++ b/.changeset/nervous-cups-happen.md @@ -2,52 +2,7 @@ '@backstage/plugin-catalog-react': minor --- -Introduce a new `EntityHeaderBlueprint` that allows you to customize the default header and also have different headers depending on an entity filter. - -### Usage examples - -Customizing the default header to render more title actions: - -```jsx -import { EntityHeaderBlueprint } from '@backstage/plugin-catalog-react/alpha'; -// ... - -function CopyEntityNameToClipboard() { - const { entity } = useEntity(); - const alertApi = useApi(alertApiRef); - - const handleClick = useCallback(() => { - if (!entity) return; - window.navigator.clipboard - .writeText(entity.metadata.name) - .then(() => - alertApi.post({ message: 'Entity name copied to clipboard!' }), - ); - }, [entity, alertApi]); - - return ( - - - - - - ); -} - -EntityHeaderBlueprint.make({ - name: 'my-default-header', - params: { - // The `FavoriteEntity` icon button is added by default - // You can also completely override the default title - // title: - title: { actions: [] }, - // A subtitle element is also supported. - // subtitle: - }, -}); -``` - -Setting up a completely different default header component: +Introduces a new `EntityHeaderBlueprint` that allows you to override the default entity page header. ```jsx import { EntityHeaderBlueprint } from '@backstage/plugin-catalog-react/alpha'; @@ -59,38 +14,3 @@ EntityHeaderBlueprint.make({ }, }); ``` - -Use a different header for entities of type template: - -```jsx -import { EntityHeaderBlueprint } from '@backstage/plugin-catalog-react/alpha'; -import { MyTemplateHeader } from './MyTemplateHeader'; - -EntityHeaderBlueprint.make({ - name: 'my-template-header', - params: { - defaultFilter: 'kind:template', - loader: () => import('./MyTemplateHeader').then(m => ), -}); -``` - -Disabling a header via configuration: - -```yaml -# app-config.yaml -app: - extensions: - - entity-header:app/my-template-header: false -``` - -Changing a header default filter via configuration: - -```yaml -# app-config.yaml -app: - extensions: - - entity-header:app/my-template-header: - config: - # Using this custom header with components instead - filter: 'kind:component' -``` diff --git a/packages/app-next/app-config.yaml b/packages/app-next/app-config.yaml index 8129ab87f7..9d2bcf198a 100644 --- a/packages/app-next/app-config.yaml +++ b/packages/app-next/app-config.yaml @@ -23,7 +23,7 @@ app: # - development: false # example overriding a default group title - documentation: - title: Docs 2 + title: Docs - deployment: title: Deployments # example adding a new group diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md index baf186897a..6c7a31c54f 100644 --- a/plugins/catalog-react/report-alpha.api.md +++ b/plugins/catalog-react/report-alpha.api.md @@ -14,7 +14,6 @@ import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { JsonValue } from '@backstage/types'; import { JSX as JSX_2 } from 'react'; import { default as React_2 } from 'react'; -import { ReactNode } from 'react'; import { ResourcePermission } from '@backstage/plugin-permission-common'; import { RouteRef } from '@backstage/frontend-plugin-api'; import { TranslationRef } from '@backstage/core-plugin-api/alpha'; @@ -326,95 +325,25 @@ export interface EntityContentLayoutProps { export const EntityHeaderBlueprint: ExtensionBlueprint<{ kind: 'entity-header'; name: undefined; - params: - | { - defaultFilter?: string | ((entity: Entity) => boolean) | undefined; - loader: () => Promise; - } - | { - defaultFilter?: string | ((entity: Entity) => boolean) | undefined; - title?: - | ReactNode - | { - actions: ReactNode[]; - }; - subtitle?: ReactNode; - }; - 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; - } - > - | ConfigurableExtensionDataRef< - | ReactNode - | { - actions: ReactNode[]; - }, - 'entity-header.titleActions', - { - optional: true; - } - > - | ConfigurableExtensionDataRef< - ReactNode, - 'entity-header.subtitle', - { - optional: true; - } - >; + params: { + loader: () => Promise; + }; + output: ConfigurableExtensionDataRef< + JSX_2.Element, + 'core.reactElement', + { + optional: true; + } + >; inputs: {}; - config: { - filter: string | undefined; - }; - configInput: { - filter?: string | undefined; - }; + config: {}; + configInput: {}; dataRefs: { - filterFunction: ConfigurableExtensionDataRef< - (entity: Entity) => boolean, - 'catalog.entity-filter-function', - {} - >; - filterExpression: ConfigurableExtensionDataRef< - string, - 'catalog.entity-filter-expression', - {} - >; element: ConfigurableExtensionDataRef< JSX_2.Element, 'core.reactElement', {} >; - title: ConfigurableExtensionDataRef< - | ReactNode - | { - actions: ReactNode[]; - }, - 'entity-header.titleActions', - {} - >; - subtitle: ConfigurableExtensionDataRef< - ReactNode, - 'entity-header.subtitle', - {} - >; }; }>; diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityHeaderBlueprint.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityHeaderBlueprint.tsx index 5f62dea47c..431197e753 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityHeaderBlueprint.tsx +++ b/plugins/catalog-react/src/alpha/blueprints/EntityHeaderBlueprint.tsx @@ -14,88 +14,31 @@ * limitations under the License. */ -import { ReactNode } from 'react'; import { createExtensionBlueprint, coreExtensionData, - createExtensionDataRef, ExtensionBoundary, } from '@backstage/frontend-plugin-api'; -import { - entityFilterExpressionDataRef, - entityFilterFunctionDataRef, -} from './extensionData'; - -const entityHeaderTitleActionsDataRef = createExtensionDataRef< - ReactNode | { actions: ReactNode[] } ->().with({ id: 'entity-header.titleActions' }); - -const entityHeaderSubtitleDataRef = createExtensionDataRef().with({ - id: 'entity-header.subtitle', -}); /** @alpha */ export const EntityHeaderBlueprint = createExtensionBlueprint({ kind: 'entity-header', - attachTo: { id: 'page:catalog/entity', input: 'headers' }, - output: [ - entityFilterFunctionDataRef.optional(), - entityFilterExpressionDataRef.optional(), - coreExtensionData.reactElement.optional(), - entityHeaderTitleActionsDataRef.optional(), - entityHeaderSubtitleDataRef.optional(), - ], + attachTo: { id: 'page:catalog/entity', input: 'header' }, dataRefs: { - filterFunction: entityFilterFunctionDataRef, - filterExpression: entityFilterExpressionDataRef, element: coreExtensionData.reactElement, - title: entityHeaderTitleActionsDataRef, - subtitle: entityHeaderSubtitleDataRef, - }, - config: { - schema: { - filter: z => z.string().optional(), - }, }, + output: [coreExtensionData.reactElement.optional()], *factory( - params: - | { - defaultFilter?: - | typeof entityFilterFunctionDataRef.T - | typeof entityFilterExpressionDataRef.T; - loader: () => Promise; - } - | { - defaultFilter?: - | typeof entityFilterFunctionDataRef.T - | typeof entityFilterExpressionDataRef.T; - title?: ReactNode | { actions: ReactNode[] }; - subtitle?: ReactNode; - }, - { config, node }, + params: { + loader: () => Promise; + }, + { node }, ) { - const { defaultFilter } = params; - - if (config.filter) { - yield entityFilterExpressionDataRef(config.filter); - } else if (typeof defaultFilter === 'string') { - yield entityFilterExpressionDataRef(defaultFilter); - } else if (typeof defaultFilter === 'function') { - yield entityFilterFunctionDataRef(defaultFilter); - } - - if ('loader' in params) { + const { loader } = params; + if (loader) { yield coreExtensionData.reactElement( - ExtensionBoundary.lazy(node, params.loader), + ExtensionBoundary.lazy(node, loader), ); } - - if ('title' in params && params.title) { - yield entityHeaderTitleActionsDataRef(params.title); - } - - if ('subtitle' in params && params.subtitle) { - yield entityHeaderSubtitleDataRef(params.subtitle); - } }, }); diff --git a/plugins/catalog/report-alpha.api.md b/plugins/catalog/report-alpha.api.md index 25b3aa2e8a..113fa5b6a1 100644 --- a/plugins/catalog/report-alpha.api.md +++ b/plugins/catalog/report-alpha.api.md @@ -20,7 +20,6 @@ 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 { ReactNode } from 'react'; import { RouteRef } from '@backstage/frontend-plugin-api'; import { SearchResultItemExtensionComponent } from '@backstage/plugin-search-react/alpha'; import { SearchResultItemExtensionPredicate } from '@backstage/plugin-search-react/alpha'; @@ -973,48 +972,17 @@ const _default: FrontendPlugin< } >; inputs: { - headers: ExtensionInput< - | 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; - } - > - | ConfigurableExtensionDataRef< - | ReactNode - | { - actions: ReactNode[]; - }, - 'entity-header.titleActions', - { - optional: true; - } - > - | ConfigurableExtensionDataRef< - ReactNode, - 'entity-header.subtitle', - { - optional: true; - } - >, + header: ExtensionInput< + ConfigurableExtensionDataRef< + JSX_2.Element, + 'core.reactElement', + { + optional: true; + } + >, { - singleton: false; - optional: false; + singleton: true; + optional: true; } >; contents: ExtensionInput< diff --git a/plugins/catalog/src/alpha/components/EntityHeader/EntityHeader.tsx b/plugins/catalog/src/alpha/components/EntityHeader/EntityHeader.tsx index fe3aeef6d3..a8d9773e28 100644 --- a/plugins/catalog/src/alpha/components/EntityHeader/EntityHeader.tsx +++ b/plugins/catalog/src/alpha/components/EntityHeader/EntityHeader.tsx @@ -20,7 +20,6 @@ import React, { useEffect, ComponentProps, ReactNode, - Fragment, } from 'react'; import { useNavigate, useLocation, useSearchParams } from 'react-router-dom'; import useAsync from 'react-use/esm/useAsync'; @@ -98,12 +97,6 @@ function findParentRelation( return null; } -function isDefaultHeaderProps( - title: ReactNode | { actions: ReactNode[] }, -): title is { actions: ReactNode[] } { - return title !== null && typeof title === 'object' && 'actions' in title; -} - const useStyles = makeStyles(theme => ({ breadcrumbs: { color: theme.page.fontColor, @@ -119,8 +112,7 @@ const useStyles = makeStyles(theme => ({ }, })); -function EntityHeaderTitle(props: { actions?: ReactNode[] }) { - const { actions } = props; +function EntityHeaderTitle() { const { entity } = useAsyncEntity(); const { kind, namespace, name } = useRouteRefParams(entityRouteRef); const { headerTitle: title } = headerProps(kind, namespace, name, entity); @@ -135,9 +127,6 @@ function EntityHeaderTitle(props: { actions?: ReactNode[] }) { {entity ? : title} {entity && } - {actions?.map((action, index) => ( - {action} - ))} ); } @@ -200,14 +189,14 @@ export function EntityHeader(props: { * It adds breadcrumbs in the Entity page to enhance user navigation and context awareness. */ parentEntityRelations?: string[]; - title?: ReactNode | { actions: ReactNode[] }; + title?: ReactNode; subtitle?: ReactNode; }) { const { UNSTABLE_extraContextMenuItems, UNSTABLE_contextMenuOptions, parentEntityRelations, - title = { actions: [] }, + title, subtitle, } = props; const { entity } = useAsyncEntity(); @@ -236,7 +225,7 @@ export function EntityHeader(props: { [setConfirmationDialogOpen], ); - const cleanUpAfterUnregiterConfirmation = useCallback(async () => { + const cleanUpAfterUnregisterConfirmation = useCallback(async () => { setConfirmationDialogOpen(false); navigate( unregisterRedirectRoute ? unregisterRedirectRoute() : catalogRoute(), @@ -279,9 +268,7 @@ export function EntityHeader(props: {
: title - } + title={title ?? } subtitle={ subtitle ?? ( @@ -312,7 +299,7 @@ export function EntityHeader(props: { entity={entity!} open={confirmationDialogOpen} onClose={closeUnregisterEntityDialog} - onConfirm={cleanUpAfterUnregiterConfirmation} + onConfirm={cleanUpAfterUnregisterConfirmation} /> )} diff --git a/plugins/catalog/src/alpha/pages.test.tsx b/plugins/catalog/src/alpha/pages.test.tsx index 567a586e3d..7c548dc1f8 100644 --- a/plugins/catalog/src/alpha/pages.test.tsx +++ b/plugins/catalog/src/alpha/pages.test.tsx @@ -117,7 +117,6 @@ describe('Entity page', () => { params: { defaultPath: '/overview', defaultTitle: 'Overview', - defaultGroup: 'documentation', loader: async () =>
Mock Overview content
, }, }); diff --git a/plugins/catalog/src/alpha/pages.tsx b/plugins/catalog/src/alpha/pages.tsx index 84eccae3b3..977ca41118 100644 --- a/plugins/catalog/src/alpha/pages.tsx +++ b/plugins/catalog/src/alpha/pages.tsx @@ -60,13 +60,10 @@ export const catalogPage = PageBlueprint.makeWithOverrides({ export const catalogEntityPage = PageBlueprint.makeWithOverrides({ name: 'entity', inputs: { - headers: createExtensionInput([ - EntityHeaderBlueprint.dataRefs.filterFunction.optional(), - EntityHeaderBlueprint.dataRefs.filterExpression.optional(), - EntityHeaderBlueprint.dataRefs.element.optional(), - EntityHeaderBlueprint.dataRefs.title.optional(), - EntityHeaderBlueprint.dataRefs.subtitle.optional(), - ]), + header: createExtensionInput( + [EntityHeaderBlueprint.dataRefs.element.optional()], + { singleton: true, optional: true }, + ), contents: createExtensionInput([ coreExtensionData.reactElement, coreExtensionData.routePath, @@ -90,24 +87,6 @@ export const catalogEntityPage = PageBlueprint.makeWithOverrides({ defaultPath: '/catalog/:namespace/:kind/:name', routeRef: convertLegacyRouteRef(entityRouteRef), loader: async () => { - const headers = inputs.headers.map(header => { - const element = header.get( - EntityHeaderBlueprint.dataRefs.element, - ) ?? ( - - ); - return { - filter: buildFilterFn( - header.get(EntityHeaderBlueprint.dataRefs.filterFunction), - header.get(EntityHeaderBlueprint.dataRefs.filterExpression), - ), - element, - }; - }); - const { EntityLayout } = await import('./components/EntityLayout'); type Groups = Record< @@ -115,6 +94,10 @@ export const catalogEntityPage = PageBlueprint.makeWithOverrides({ { title: string; items: Array<(typeof inputs.contents)[0]> } >; + const header = inputs.header?.get( + EntityHeaderBlueprint.dataRefs.element, + ) ?? ; + let groups = Object.entries(defaultEntityContentGroups).reduce( (rest, group) => { const [groupId, groupValue] = group; @@ -150,12 +133,9 @@ export const catalogEntityPage = PageBlueprint.makeWithOverrides({ } const Component = () => { - const { entity, ...rest } = useEntityFromUrl(); - const header = headers.find(({ filter }) => entity && filter(entity)); - return ( - - + + {Object.values(groups).flatMap(({ title, items }) => items.map(output => (