From 16aeb3f10ac4c036587224cc7fc9ea426770391b Mon Sep 17 00:00:00 2001 From: Mark Dunphy Date: Thu, 20 Mar 2025 16:12:40 -0400 Subject: [PATCH] move to useOnClick and migrate entity URL copy to blueprint Signed-off-by: Mark Dunphy --- .../EntityContextMenuItemBlueprint.tsx | 80 ++++++++++--------- .../src/alpha/blueprints/index.ts | 4 +- .../components/EntityHeader/EntityHeader.tsx | 3 +- .../catalog/src/alpha/contextMenuItems.tsx | 57 +++++++++++++ plugins/catalog/src/alpha/pages.tsx | 5 +- plugins/catalog/src/alpha/plugin.tsx | 2 + .../catalog/src/alpha/searchResultItems.tsx | 2 +- .../EntityContextMenu/EntityContextMenu.tsx | 19 ++--- 8 files changed, 117 insertions(+), 55 deletions(-) create mode 100644 plugins/catalog/src/alpha/contextMenuItems.tsx diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx index 1bb9ede1a9..17dfede815 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx +++ b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx @@ -16,68 +16,77 @@ import React from 'react'; import { - DialogApiDialog, - ExtensionBoundary, - coreExtensionData, createExtensionBlueprint, - dialogApiRef, + ApiHolder, + createExtensionDataRef, } 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; + useTitle: () => string; + icon: React.JSX.Element; useHref: () => string; } | { - title: string; - icon: JSX.Element; + useTitle: () => string; + icon: React.JSX.Element; href: string; }; /** @alpha */ export type FactoryDialogParams = { - dialogLoader: () => Promise< - ({ dialog }: { dialog: DialogApiDialog }) => JSX.Element - >; - title: string; - icon: JSX.Element; + useOnClick: ({ + apis, + }: { + apis: ApiHolder; + }) => React.MouseEventHandler; + useTitle: () => string; + icon: React.JSX.Element; }; /** @alpha */ export type EntityContextMenuItemParams = - | FactoryLoaderParams | FactoryHrefParams | FactoryDialogParams; +export type ContextMenuItemProps = { + onClose: () => void; +}; + +export type ContextMenuItemComponent = ( + props: ContextMenuItemProps, +) => React.JSX.Element; + +export const contextMenuItemComponentDataRef = + createExtensionDataRef().with({ + id: 'catalog.contextMenuItemComponent', + }); + /** @alpha */ export const EntityContextMenuItemBlueprint = createExtensionBlueprint({ kind: 'entity-context-menu-item', attachTo: { id: 'page:catalog/entity', input: 'contextMenuItems' }, - output: [coreExtensionData.reactElement], - *factory(params: EntityContextMenuItemParams, { node, apis }) { - const loaderFactory = () => { - if ('loader' in params) { - return params.loader; - } + output: [contextMenuItemComponentDataRef], + *factory(params: EntityContextMenuItemParams, { apis }) { + const loaderFactory = (): ContextMenuItemComponent => { + if ('useOnClick' in params) { + return ({ onClose }) => { + const onClick = params.useOnClick({ apis }); + const title = params.useTitle(); - if ('dialogLoader' in params) { - const dialogApi = apis.get(dialogApiRef); - return async () => { - const Dialog = await params.dialogLoader(); return ( - dialogApi?.show(Dialog)}> + { + onClick(e); + onClose(); + }} + > {params.icon} - + ); }; @@ -85,20 +94,19 @@ export const EntityContextMenuItemBlueprint = createExtensionBlueprint({ const useHref = 'useHref' in params ? params.useHref : () => params.href; - return async () => { + return () => { const href = useHref(); + const title = params.useTitle(); return ( {params.icon} - + ); }; }; - yield coreExtensionData.reactElement( - ExtensionBoundary.lazy(node, loaderFactory()), - ); + yield contextMenuItemComponentDataRef(loaderFactory()); }, }); diff --git a/plugins/catalog-react/src/alpha/blueprints/index.ts b/plugins/catalog-react/src/alpha/blueprints/index.ts index 0d4e65a39c..004652f716 100644 --- a/plugins/catalog-react/src/alpha/blueprints/index.ts +++ b/plugins/catalog-react/src/alpha/blueprints/index.ts @@ -24,9 +24,11 @@ export { EntityHeaderBlueprint } from './EntityHeaderBlueprint'; export { defaultEntityContentGroups } from './extensionData'; export type { EntityCardType } from './extensionData'; export { + contextMenuItemComponentDataRef, EntityContextMenuItemBlueprint, + type ContextMenuItemProps, + type ContextMenuItemComponent, type FactoryHrefParams, - type FactoryLoaderParams, type FactoryDialogParams, type EntityContextMenuItemParams, } from './EntityContextMenuItemBlueprint'; diff --git a/plugins/catalog/src/alpha/components/EntityHeader/EntityHeader.tsx b/plugins/catalog/src/alpha/components/EntityHeader/EntityHeader.tsx index 4a1af40168..7fa6471d3e 100644 --- a/plugins/catalog/src/alpha/components/EntityHeader/EntityHeader.tsx +++ b/plugins/catalog/src/alpha/components/EntityHeader/EntityHeader.tsx @@ -55,6 +55,7 @@ import { import { EntityLabels } from '../EntityLabels'; import { EntityContextMenu } from '../../../components/EntityContextMenu'; import { rootRouteRef, unregisterRedirectRouteRef } from '../../../routes'; +import { ContextMenuItemComponent } from '@backstage/plugin-catalog-react/alpha'; function headerProps( paramKind: string | undefined, @@ -178,7 +179,7 @@ export function EntityHeader(props: { UNSTABLE_contextMenuOptions?: { disableUnregister: boolean | 'visible' | 'hidden' | 'disable'; }; - extraMenuItems?: JSX.Element[]; + extraMenuItems?: ContextMenuItemComponent[]; /** * 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 diff --git a/plugins/catalog/src/alpha/contextMenuItems.tsx b/plugins/catalog/src/alpha/contextMenuItems.tsx new file mode 100644 index 0000000000..33479b6f65 --- /dev/null +++ b/plugins/catalog/src/alpha/contextMenuItems.tsx @@ -0,0 +1,57 @@ +/* + * Copyright 2023 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 { EntityContextMenuItemBlueprint } from '@backstage/plugin-catalog-react/alpha'; +import FileCopyTwoToneIcon from '@material-ui/icons/FileCopyTwoTone'; +import useCopyToClipboard from 'react-use/esm/useCopyToClipboard'; +import { alertApiRef, useApi } from '@backstage/core-plugin-api'; +import { useTranslationRef } from '@backstage/frontend-plugin-api'; +import { catalogTranslationRef } from './translation'; + +export const copyEntityUrlContextMenuItem = EntityContextMenuItemBlueprint.make( + { + name: 'copy-entity-url', + params: { + icon: , + useTitle: () => { + const { t } = useTranslationRef(catalogTranslationRef); + return t('entityContextMenu.copyURLMenuTitle'); + }, + useOnClick: () => { + const [copyState, copyToClipboard] = useCopyToClipboard(); + const alertApi = useApi(alertApiRef); + const { t } = useTranslationRef(catalogTranslationRef); + + React.useEffect(() => { + if (!copyState.error && copyState.value) { + alertApi.post({ + message: t('entityContextMenu.copiedMessage'), + severity: 'info', + display: 'transient', + }); + } + }, [copyState, alertApi, t]); + + return async () => { + copyToClipboard(window.location.toString()); + }; + }, + }, + }, +); + +export default [copyEntityUrlContextMenuItem]; diff --git a/plugins/catalog/src/alpha/pages.tsx b/plugins/catalog/src/alpha/pages.tsx index b7832f3a16..74ea49f685 100644 --- a/plugins/catalog/src/alpha/pages.tsx +++ b/plugins/catalog/src/alpha/pages.tsx @@ -31,6 +31,7 @@ import { EntityHeaderBlueprint, EntityContentBlueprint, defaultEntityContentGroups, + contextMenuItemComponentDataRef, } from '@backstage/plugin-catalog-react/alpha'; import { rootRouteRef } from '../routes'; import { useEntityFromUrl } from '../components/CatalogEntityPage/useEntityFromUrl'; @@ -72,7 +73,7 @@ export const catalogEntityPage = PageBlueprint.makeWithOverrides({ EntityContentBlueprint.dataRefs.filterExpression.optional(), EntityContentBlueprint.dataRefs.group.optional(), ]), - contextMenuItems: createExtensionInput([coreExtensionData.reactElement]), + contextMenuItems: createExtensionInput([contextMenuItemComponentDataRef]), }, config: { schema: { @@ -90,7 +91,7 @@ export const catalogEntityPage = PageBlueprint.makeWithOverrides({ const { EntityLayout } = await import('./components/EntityLayout'); const menuItems = inputs.contextMenuItems.map(item => - item.get(coreExtensionData.reactElement), + item.get(contextMenuItemComponentDataRef), ); type Groups = Record< diff --git a/plugins/catalog/src/alpha/plugin.tsx b/plugins/catalog/src/alpha/plugin.tsx index 8a18500043..04034e2156 100644 --- a/plugins/catalog/src/alpha/plugin.tsx +++ b/plugins/catalog/src/alpha/plugin.tsx @@ -34,6 +34,7 @@ import navItems from './navItems'; import entityCards from './entityCards'; import entityContents from './entityContents'; import searchResultItems from './searchResultItems'; +import contextMenuItems from './contextMenuItems'; /** @alpha */ export default createFrontendPlugin({ @@ -55,6 +56,7 @@ export default createFrontendPlugin({ ...navItems, ...entityCards, ...entityContents, + ...contextMenuItems, ...searchResultItems, ], }); diff --git a/plugins/catalog/src/alpha/searchResultItems.tsx b/plugins/catalog/src/alpha/searchResultItems.tsx index 8e140de637..db8d0bfc09 100644 --- a/plugins/catalog/src/alpha/searchResultItems.tsx +++ b/plugins/catalog/src/alpha/searchResultItems.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2023 The Backstage Authors + * 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. diff --git a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx index ecfae2c26c..c0ba1a500c 100644 --- a/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx +++ b/plugins/catalog/src/components/EntityContextMenu/EntityContextMenu.tsx @@ -25,7 +25,6 @@ import Tooltip from '@material-ui/core/Tooltip'; import { Theme, makeStyles } from '@material-ui/core/styles'; import BugReportIcon from '@material-ui/icons/BugReport'; import MoreVert from '@material-ui/icons/MoreVert'; -import FileCopyTwoToneIcon from '@material-ui/icons/FileCopyTwoTone'; import { SyntheticEvent, useEffect, useState } from 'react'; import { IconComponent } from '@backstage/core-plugin-api'; import { useEntityPermission } from '@backstage/plugin-catalog-react/alpha'; @@ -35,6 +34,7 @@ import { useApi, alertApiRef } from '@backstage/core-plugin-api'; import useCopyToClipboard from 'react-use/esm/useCopyToClipboard'; import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import type { ContextMenuItemComponent } from '@backstage/plugin-catalog-react/alpha'; /** @public */ export type EntityContextMenuClassKey = 'button'; @@ -61,7 +61,7 @@ interface ExtraContextMenuItem { interface EntityContextMenuProps { UNSTABLE_extraContextMenuItems?: ExtraContextMenuItem[]; UNSTABLE_contextMenuOptions?: UnregisterEntityOptions; - extraMenuItems?: JSX.Element[]; + extraMenuItems?: ContextMenuItemComponent[]; onUnregisterEntity: () => void; onInspectEntity: () => void; } @@ -147,7 +147,6 @@ export function EntityContextMenu(props: EntityContextMenuProps) { > {extraItems} - {extraMenuItems} - { - onClose(); - copyToClipboard(window.location.toString()); - }} - > - - - - - + {extraMenuItems?.map(ExtraMenuItem => ( + + ))}