move to useOnClick and migrate entity URL copy to blueprint
Signed-off-by: Mark Dunphy <markd@spotify.com>
This commit is contained in:
@@ -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<JSX.Element>;
|
||||
};
|
||||
|
||||
/** @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<HTMLLIElement>;
|
||||
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<ContextMenuItemComponent>().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 (
|
||||
<MenuItem onClick={() => dialogApi?.show(Dialog)}>
|
||||
<MenuItem
|
||||
onClick={e => {
|
||||
onClick(e);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>{params.icon}</ListItemIcon>
|
||||
<ListItemText primary={params.title} />
|
||||
<ListItemText primary={title} />
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
||||
@@ -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 (
|
||||
<MenuItem component="a" href={href}>
|
||||
<ListItemIcon>{params.icon}</ListItemIcon>
|
||||
<ListItemText primary={params.title} />
|
||||
<ListItemText primary={title} />
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
yield coreExtensionData.reactElement(
|
||||
ExtensionBoundary.lazy(node, loaderFactory()),
|
||||
);
|
||||
yield contextMenuItemComponentDataRef(loaderFactory());
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: <FileCopyTwoToneIcon fontSize="small" />,
|
||||
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];
|
||||
@@ -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<
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
>
|
||||
<MenuList autoFocusItem={Boolean(anchorEl)}>
|
||||
{extraItems}
|
||||
{extraMenuItems}
|
||||
<UnregisterEntity
|
||||
unregisterEntityOptions={UNSTABLE_contextMenuOptions}
|
||||
isUnregisterAllowed={isAllowed}
|
||||
@@ -165,17 +164,9 @@ export function EntityContextMenu(props: EntityContextMenuProps) {
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={t('entityContextMenu.inspectMenuTitle')} />
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
onClose();
|
||||
copyToClipboard(window.location.toString());
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<FileCopyTwoToneIcon fontSize="small" />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={t('entityContextMenu.copyURLMenuTitle')} />
|
||||
</MenuItem>
|
||||
{extraMenuItems?.map(ExtraMenuItem => (
|
||||
<ExtraMenuItem onClose={onClose} />
|
||||
))}
|
||||
</MenuList>
|
||||
</Popover>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user