From 3e40a77aeb82add1541c3d7a63b12da6b12e87ab Mon Sep 17 00:00:00 2001 From: Mark Dunphy Date: Tue, 11 Mar 2025 10:56:29 -0400 Subject: [PATCH] add EntityContextMenuItemBlueprint Signed-off-by: Mark Dunphy --- .../EntityContextMenuItemBlueprint.tsx | 74 +++++++++++++++++++ .../src/alpha/blueprints/index.ts | 5 ++ .../components/EntityHeader/EntityHeader.tsx | 3 + .../components/EntityLayout/EntityLayout.tsx | 3 + plugins/catalog/src/alpha/pages.tsx | 11 ++- .../EntityContextMenu/EntityContextMenu.tsx | 3 + .../components/EntityLayout/EntityLayout.tsx | 3 + 7 files changed, 100 insertions(+), 2 deletions(-) create mode 100644 plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx new file mode 100644 index 0000000000..f2c7657927 --- /dev/null +++ b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx @@ -0,0 +1,74 @@ +/* + * 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 from 'react'; +import { + ExtensionBoundary, + coreExtensionData, + createExtensionBlueprint, +} from '@backstage/frontend-plugin-api'; +import MenuItem from '@material-ui/core/MenuItem'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import ListItemText from '@material-ui/core/ListItemText'; + +/** @alpha */ +export type FactoryLoaderParams = { + loader: () => Promise; +}; + +/** @alpha */ +export type FactoryHrefParams = + | { + title: string; + icon: JSX.Element; + useHref: () => string; + } + | { + title: string; + icon: JSX.Element; + href: string; + }; + +/** @alpha */ +export const EntityContextMenuItemBlueprint = createExtensionBlueprint({ + kind: 'entity-context-menu-item', + attachTo: { id: 'page:catalog/entity', input: 'extraContextMenuItems' }, + output: [coreExtensionData.reactElement], + *factory(params: FactoryLoaderParams | FactoryHrefParams, { node }) { + const loaderFactory = () => { + if ('loader' in params) { + return params.loader; + } + + const useHref = 'useHref' in params ? params.useHref : () => params.href; + + return async () => { + const href = useHref(); + + return ( + + {params.icon} + + + ); + }; + }; + + yield coreExtensionData.reactElement( + ExtensionBoundary.lazy(node, loaderFactory()), + ); + }, +}); diff --git a/plugins/catalog-react/src/alpha/blueprints/index.ts b/plugins/catalog-react/src/alpha/blueprints/index.ts index dc3fa7de1b..857cdef5ea 100644 --- a/plugins/catalog-react/src/alpha/blueprints/index.ts +++ b/plugins/catalog-react/src/alpha/blueprints/index.ts @@ -23,3 +23,8 @@ export { export { EntityHeaderBlueprint } from './EntityHeaderBlueprint'; export { defaultEntityContentGroups } from './extensionData'; export type { EntityCardType } from './extensionData'; +export { + EntityContextMenuItemBlueprint, + type FactoryHrefParams, + type FactoryLoaderParams, +} from './EntityContextMenuItemBlueprint'; diff --git a/plugins/catalog/src/alpha/components/EntityHeader/EntityHeader.tsx b/plugins/catalog/src/alpha/components/EntityHeader/EntityHeader.tsx index ca81ffe7cc..4a1af40168 100644 --- a/plugins/catalog/src/alpha/components/EntityHeader/EntityHeader.tsx +++ b/plugins/catalog/src/alpha/components/EntityHeader/EntityHeader.tsx @@ -178,6 +178,7 @@ export function EntityHeader(props: { UNSTABLE_contextMenuOptions?: { disableUnregister: boolean | 'visible' | 'hidden' | 'disable'; }; + extraMenuItems?: JSX.Element[]; /** * An array of relation types used to determine the parent entities in the hierarchy. * These relations are prioritized in the order provided, allowing for flexible @@ -195,6 +196,7 @@ export function EntityHeader(props: { const { UNSTABLE_extraContextMenuItems, UNSTABLE_contextMenuOptions, + extraMenuItems, parentEntityRelations, title, subtitle, @@ -281,6 +283,7 @@ export function EntityHeader(props: { diff --git a/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx index abf1e69bfb..5c92981fa5 100644 --- a/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx +++ b/plugins/catalog/src/alpha/components/EntityLayout/EntityLayout.tsx @@ -62,6 +62,7 @@ export interface EntityLayoutProps { UNSTABLE_extraContextMenuItems?: ComponentProps< typeof EntityHeader >['UNSTABLE_extraContextMenuItems']; + extraMenuItems?: ComponentProps['extraMenuItems']; children?: ReactNode; header?: JSX.Element; NotFoundComponent?: ReactNode; @@ -99,6 +100,7 @@ export const EntityLayout = (props: EntityLayoutProps) => { const { UNSTABLE_extraContextMenuItems, UNSTABLE_contextMenuOptions, + extraMenuItems, children, header, NotFoundComponent, @@ -145,6 +147,7 @@ export const EntityLayout = (props: EntityLayoutProps) => { parentEntityRelations={parentEntityRelations} UNSTABLE_contextMenuOptions={UNSTABLE_contextMenuOptions} UNSTABLE_extraContextMenuItems={UNSTABLE_extraContextMenuItems} + extraMenuItems={extraMenuItems} /> )} diff --git a/plugins/catalog/src/alpha/pages.tsx b/plugins/catalog/src/alpha/pages.tsx index be2fb341c3..c8dc7b2913 100644 --- a/plugins/catalog/src/alpha/pages.tsx +++ b/plugins/catalog/src/alpha/pages.tsx @@ -72,6 +72,9 @@ export const catalogEntityPage = PageBlueprint.makeWithOverrides({ EntityContentBlueprint.dataRefs.filterExpression.optional(), EntityContentBlueprint.dataRefs.group.optional(), ]), + extraContextMenuItems: createExtensionInput([ + coreExtensionData.reactElement, + ]), }, config: { schema: { @@ -88,6 +91,10 @@ export const catalogEntityPage = PageBlueprint.makeWithOverrides({ loader: async () => { const { EntityLayout } = await import('./components/EntityLayout'); + const extraMenuItems = inputs.extraContextMenuItems.map(item => + item.get(coreExtensionData.reactElement), + ); + type Groups = Record< string, { title: string; items: Array<(typeof inputs.contents)[0]> } @@ -95,7 +102,7 @@ export const catalogEntityPage = PageBlueprint.makeWithOverrides({ const header = inputs.header?.get( EntityHeaderBlueprint.dataRefs.element, - ) ?? ; + ) ?? ; let groups = Object.entries(defaultEntityContentGroups).reduce( (rest, group) => { @@ -134,7 +141,7 @@ export const catalogEntityPage = PageBlueprint.makeWithOverrides({ const Component = () => { return ( - + {Object.values(groups).flatMap(({ title, items }) => items.map(output => ( void; onInspectEntity: () => void; } @@ -69,6 +70,7 @@ export function EntityContextMenu(props: EntityContextMenuProps) { const { UNSTABLE_extraContextMenuItems, UNSTABLE_contextMenuOptions, + extraMenuItems, onUnregisterEntity, onInspectEntity, } = props; @@ -145,6 +147,7 @@ export function EntityContextMenu(props: EntityContextMenuProps) { > {extraItems} + {extraMenuItems} { const { UNSTABLE_extraContextMenuItems, UNSTABLE_contextMenuOptions, + extraMenuItems, children, NotFoundComponent, parentEntityRelations, @@ -360,6 +362,7 @@ export const EntityLayout = (props: EntityLayoutProps) => { setConfirmationDialogOpen(true)} onInspectEntity={() => setSearchParams('inspect')} />