@@ -11,6 +11,8 @@ import { ExtensionBlueprint } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
import { JSX as JSX_2 } from 'react';
|
||||
import { MenuItemProps } from '@material-ui/core/MenuItem';
|
||||
import { default as React_2 } from 'react';
|
||||
import { ResourcePermission } from '@backstage/plugin-permission-common';
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { TranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
@@ -359,9 +361,12 @@ export const EntityContextMenuItemBlueprint: ExtensionBlueprint<{
|
||||
}>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export type EntityContextMenuItemParams =
|
||||
| FactoryHrefParams
|
||||
| FactoryDialogParams;
|
||||
export type EntityContextMenuItemParams = {
|
||||
useProps: () => Omit<MenuItemProps, 'onClick'> & {
|
||||
onClick?: () => Promise<void>;
|
||||
};
|
||||
icon: React_2.JSX.Element;
|
||||
};
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const EntityHeaderBlueprint: ExtensionBlueprint<{
|
||||
|
||||
+10
-14
@@ -16,28 +16,24 @@
|
||||
|
||||
import React from 'react';
|
||||
import { EntityContextMenuItemBlueprint } from './EntityContextMenuItemBlueprint';
|
||||
import { createRouteRef, useRouteRef } from '@backstage/frontend-plugin-api';
|
||||
|
||||
describe('EntityContextMenuItemBlueprint', () => {
|
||||
const routeRef = createRouteRef();
|
||||
const data = [
|
||||
{
|
||||
useTitle: () => 'Test',
|
||||
href: '/somewhere',
|
||||
icon: <span>Test</span>,
|
||||
useProps: () => ({
|
||||
title: 'Test',
|
||||
href: '/somewhere',
|
||||
component: 'a',
|
||||
disabled: true,
|
||||
}),
|
||||
},
|
||||
{
|
||||
useTitle: () => 'Test',
|
||||
useHref: () => {
|
||||
const r = useRouteRef(routeRef) ?? (() => '/somewhere');
|
||||
return r();
|
||||
},
|
||||
icon: <span>Test</span>,
|
||||
},
|
||||
{
|
||||
useTitle: () => 'TestDialog',
|
||||
useOnClick: () => () => true,
|
||||
icon: <span>Test</span>,
|
||||
useProps: () => ({
|
||||
title: 'Test',
|
||||
onClick: async () => {},
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
createExtensionDataRef,
|
||||
ExtensionBoundary,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import MenuItem, { MenuItemProps } from '@material-ui/core/MenuItem';
|
||||
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
|
||||
@@ -48,9 +48,12 @@ export type FactoryDialogParams = {
|
||||
};
|
||||
|
||||
/** @alpha */
|
||||
export type EntityContextMenuItemParams =
|
||||
| FactoryHrefParams
|
||||
| FactoryDialogParams;
|
||||
export type EntityContextMenuItemParams = {
|
||||
useProps: () => Omit<MenuItemProps, 'onClick'> & {
|
||||
onClick?: () => Promise<void>;
|
||||
};
|
||||
icon: React.JSX.Element;
|
||||
};
|
||||
|
||||
/** @alpha */
|
||||
export type ContextMenuItemProps = {
|
||||
@@ -75,36 +78,25 @@ export const EntityContextMenuItemBlueprint = createExtensionBlueprint({
|
||||
output: [contextMenuItemComponentDataRef],
|
||||
*factory(params: EntityContextMenuItemParams, { node }) {
|
||||
const loader = async (): Promise<ContextMenuItemComponent> => {
|
||||
if ('useOnClick' in params) {
|
||||
return ({ onClose }) => {
|
||||
const onClick = params.useOnClick();
|
||||
const title = params.useTitle();
|
||||
const disabled = params.useIsDisabled?.() ?? false;
|
||||
return ({ onClose }) => {
|
||||
const {
|
||||
children,
|
||||
button,
|
||||
title,
|
||||
onClick: onClickProp,
|
||||
...menuItemProps
|
||||
} = params.useProps();
|
||||
let onClick;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
disabled={disabled}
|
||||
onClick={e => {
|
||||
onClose();
|
||||
onClick(e);
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>{params.icon}</ListItemIcon>
|
||||
<ListItemText primary={title} />
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const useHref = 'useHref' in params ? params.useHref : () => params.href;
|
||||
|
||||
return () => {
|
||||
const href = useHref();
|
||||
const title = params.useTitle();
|
||||
const disabled = params.useIsDisabled?.() ?? false;
|
||||
if (onClickProp !== undefined) {
|
||||
onClick = async () => {
|
||||
await onClickProp();
|
||||
onClose();
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuItem disabled={disabled} component="a" href={href}>
|
||||
<MenuItem {...menuItemProps} onClick={onClick}>
|
||||
<ListItemIcon>{params.icon}</ListItemIcon>
|
||||
<ListItemText primary={title} />
|
||||
</MenuItem>
|
||||
|
||||
@@ -43,11 +43,7 @@ export const copyEntityUrlContextMenuItem = EntityContextMenuItemBlueprint.make(
|
||||
name: 'copy-entity-url',
|
||||
params: {
|
||||
icon: <FileCopyTwoToneIcon fontSize="small" />,
|
||||
useTitle: () => {
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
return t('entityContextMenu.copyURLMenuTitle');
|
||||
},
|
||||
useOnClick: () => {
|
||||
useProps: () => {
|
||||
const [copyState, copyToClipboard] = useCopyToClipboard();
|
||||
const alertApi = useApi(alertApiRef);
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
@@ -62,8 +58,11 @@ export const copyEntityUrlContextMenuItem = EntityContextMenuItemBlueprint.make(
|
||||
}
|
||||
}, [copyState, alertApi, t]);
|
||||
|
||||
return async () => {
|
||||
copyToClipboard(window.location.toString());
|
||||
return {
|
||||
title: t('entityContextMenu.copyURLMenuTitle'),
|
||||
onClick: async () => {
|
||||
copyToClipboard(window.location.toString());
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
@@ -75,15 +74,15 @@ export const inspectEntityContextMenuItem = EntityContextMenuItemBlueprint.make(
|
||||
name: 'inspect-entity',
|
||||
params: {
|
||||
icon: <BugReportIcon fontSize="small" />,
|
||||
useTitle: () => {
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
return t('entityContextMenu.inspectMenuTitle');
|
||||
},
|
||||
useOnClick: () => {
|
||||
useProps: () => {
|
||||
const [_, setSearchParams] = useSearchParams();
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
|
||||
return () => {
|
||||
setSearchParams('inspect');
|
||||
return {
|
||||
title: t('entityContextMenu.inspectMenuTitle'),
|
||||
onClick: async () => {
|
||||
setSearchParams('inspect');
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
@@ -95,40 +94,37 @@ export const unregisterEntityContextMenuItem =
|
||||
name: 'unregister-entity',
|
||||
params: {
|
||||
icon: <CancelIcon fontSize="small" />,
|
||||
useTitle: () => {
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
return t('entityContextMenu.unregisterMenuTitle');
|
||||
},
|
||||
useIsDisabled: () => {
|
||||
const unregisterPermission = useEntityPermission(
|
||||
catalogEntityDeletePermission,
|
||||
);
|
||||
|
||||
return !unregisterPermission.allowed;
|
||||
},
|
||||
useOnClick: () => {
|
||||
useProps: () => {
|
||||
const { entity } = useEntity();
|
||||
const dialogApi = useApi(dialogApiRef);
|
||||
const navigate = useNavigate();
|
||||
const catalogRoute = useRouteRef(rootRouteRef);
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
const unregisterRedirectRoute = useRouteRef(unregisterRedirectRouteRef);
|
||||
const unregisterPermission = useEntityPermission(
|
||||
catalogEntityDeletePermission,
|
||||
);
|
||||
|
||||
return async () => {
|
||||
dialogApi.showModal(({ dialog }: { dialog: DialogApiDialog }) => (
|
||||
<UnregisterEntityDialog
|
||||
open
|
||||
entity={entity}
|
||||
onClose={() => dialog.close()}
|
||||
onConfirm={() => {
|
||||
dialog.close();
|
||||
navigate(
|
||||
unregisterRedirectRoute
|
||||
? unregisterRedirectRoute()
|
||||
: catalogRoute(),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
));
|
||||
return {
|
||||
title: t('entityContextMenu.unregisterMenuTitle'),
|
||||
disabled: !unregisterPermission.allowed,
|
||||
onClick: async () => {
|
||||
dialogApi.showModal(({ dialog }: { dialog: DialogApiDialog }) => (
|
||||
<UnregisterEntityDialog
|
||||
open
|
||||
entity={entity}
|
||||
onClose={() => dialog.close()}
|
||||
onConfirm={() => {
|
||||
dialog.close();
|
||||
navigate(
|
||||
unregisterRedirectRoute
|
||||
? unregisterRedirectRoute()
|
||||
: catalogRoute(),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
));
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
@@ -605,15 +605,26 @@ describe('Entity page', () => {
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ useIsDisabled: () => true, href: '/somewhere' },
|
||||
{ useIsDisabled: () => false, href: '/somewhere' },
|
||||
{ useIsDisabled: () => true, useHref: () => '/somewhere' },
|
||||
{ useIsDisabled: () => false, useHref: () => '/somewhere' },
|
||||
{
|
||||
useProps: () => ({
|
||||
title: 'Test Title',
|
||||
href: '/somewhere',
|
||||
disabled: true,
|
||||
component: 'a',
|
||||
}),
|
||||
},
|
||||
{
|
||||
useProps: () => ({
|
||||
title: 'Test Title',
|
||||
href: '/somewhere',
|
||||
disabled: false,
|
||||
component: 'a',
|
||||
}),
|
||||
},
|
||||
])('should render an href based context menu item', async params => {
|
||||
const menuItem = EntityContextMenuItemBlueprint.make({
|
||||
name: 'test-href',
|
||||
params: {
|
||||
useTitle: () => 'Test Title',
|
||||
icon: <span>Test Icon</span>,
|
||||
...params,
|
||||
},
|
||||
@@ -645,6 +656,7 @@ describe('Entity page', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
const { disabled } = params.useProps();
|
||||
|
||||
await waitFor(async () => {
|
||||
await userEvent.click(screen.getByTestId('menu-button'));
|
||||
@@ -652,21 +664,29 @@ describe('Entity page', () => {
|
||||
expect(screen.getByText('Test Icon')).toBeInTheDocument();
|
||||
const anchor = screen.getByText('Test Title').closest('a');
|
||||
expect(anchor).toHaveAttribute('href', '/somewhere');
|
||||
expect(anchor).toHaveAttribute(
|
||||
'aria-disabled',
|
||||
params.useIsDisabled().toString(),
|
||||
);
|
||||
expect(anchor).toHaveAttribute('aria-disabled', disabled.toString());
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ useIsDisabled: () => true, useOnClick: () => onClickMock },
|
||||
{ useIsDisabled: () => false, useOnClick: () => onClickMock },
|
||||
])('should render a useOnClick based context menu item', async params => {
|
||||
{
|
||||
useProps: () => ({
|
||||
title: 'Test Title',
|
||||
onClick: onClickMock,
|
||||
disabled: true,
|
||||
}),
|
||||
},
|
||||
{
|
||||
useProps: () => ({
|
||||
title: 'Test Title',
|
||||
onClick: onClickMock,
|
||||
disabled: false,
|
||||
}),
|
||||
},
|
||||
])('should render an onClick based context menu item', async params => {
|
||||
const menuItem = EntityContextMenuItemBlueprint.make({
|
||||
name: 'test-href',
|
||||
name: 'test-click',
|
||||
params: {
|
||||
useTitle: () => 'Test Title',
|
||||
icon: <span>Test Icon</span>,
|
||||
...params,
|
||||
},
|
||||
@@ -674,7 +694,6 @@ describe('Entity page', () => {
|
||||
const tester = createExtensionTester(
|
||||
Object.assign({ namespace: 'catalog' }, catalogEntityPage),
|
||||
).add(menuItem);
|
||||
const disabled = params.useIsDisabled();
|
||||
|
||||
renderInTestApp(
|
||||
<TestApiProvider
|
||||
@@ -700,6 +719,7 @@ describe('Entity page', () => {
|
||||
},
|
||||
);
|
||||
|
||||
const { disabled } = params.useProps();
|
||||
await waitFor(async () => {
|
||||
await userEvent.click(screen.getByTestId('menu-button'));
|
||||
expect(screen.getByText('Test Title')).toBeInTheDocument();
|
||||
|
||||
Reference in New Issue
Block a user