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 => (