Merge pull request #11870 from SuganJoe/Issue-10434

Customize the Entity context menu in Catalog
This commit is contained in:
Ben Lambert
2022-06-30 15:37:13 +02:00
committed by GitHub
8 changed files with 206 additions and 31 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/plugin-catalog': patch
---
Adding ability to customize the "unregister entity" menu item in the entity context menu on the entity page with options 'visible','hidden','disabled'.With this three new options, one can hide the "unregister entity" menu item from the list, disable or keep it enabled.
The boolean input for "unregister entity" will be deprecated later in favour of the above three options.
@@ -167,7 +167,12 @@ const EntityLayoutWrapper = (props: { children?: ReactNode }) => {
return (
<>
<EntityLayout UNSTABLE_extraContextMenuItems={extraMenuItems}>
<EntityLayout
UNSTABLE_extraContextMenuItems={extraMenuItems}
UNSTABLE_contextMenuOptions={{
disableUnregister: 'visible',
}}
>
{props.children}
</EntityLayout>
<EntityBadgesDialog
+2 -2
View File
@@ -259,10 +259,10 @@ export interface EntityLayoutProps {
children?: React_2.ReactNode;
// (undocumented)
NotFoundComponent?: React_2.ReactNode;
// Warning: (ae-forgotten-export) The symbol "contextMenuOptions" needs to be exported by the entry point index.d.ts
// Warning: (ae-forgotten-export) The symbol "EntityContextMenuOptions" needs to be exported by the entry point index.d.ts
//
// (undocumented)
UNSTABLE_contextMenuOptions?: contextMenuOptions;
UNSTABLE_contextMenuOptions?: EntityContextMenuOptions;
// Warning: (ae-forgotten-export) The symbol "ExtraContextMenuItem" needs to be exported by the entry point index.d.ts
//
// (undocumented)
@@ -42,7 +42,6 @@ function render(children: React.ReactNode) {
describe('ComponentContextMenu', () => {
it('should call onUnregisterEntity on button click', async () => {
const mockCallback = jest.fn();
await render(
<EntityContextMenu
onUnregisterEntity={mockCallback}
@@ -61,6 +60,30 @@ describe('ComponentContextMenu', () => {
expect(mockCallback).toBeCalled();
});
it('check Unregister entity button is disabled', async () => {
const mockCallback = jest.fn();
const { getByText } = await render(
<EntityContextMenu
UNSTABLE_contextMenuOptions={{ disableUnregister: 'disable' }}
onUnregisterEntity={mockCallback}
onInspectEntity={() => {}}
/>,
);
const button = await screen.findByTestId('menu-button');
expect(button).toBeInTheDocument();
fireEvent.click(button);
const unregister = await screen.getByText('Unregister entity');
expect(unregister).toBeInTheDocument();
const unregisterSpanItem = getByText(/Unregister entity/);
const unregisterMenuListItem =
unregisterSpanItem?.parentElement?.parentElement;
expect(unregisterMenuListItem).toHaveAttribute('aria-disabled');
});
it('should call onInspectEntity on button click', async () => {
const mockCallback = jest.fn();
@@ -24,7 +24,6 @@ import {
Popover,
} from '@material-ui/core';
import { makeStyles } from '@material-ui/core/styles';
import CancelIcon from '@material-ui/icons/Cancel';
import BugReportIcon from '@material-ui/icons/BugReport';
import MoreVert from '@material-ui/icons/MoreVert';
import React, { useState } from 'react';
@@ -32,6 +31,7 @@ import { IconComponent } from '@backstage/core-plugin-api';
import { useEntityPermission } from '@backstage/plugin-catalog-react';
import { catalogEntityDeletePermission } from '@backstage/plugin-catalog-common';
import { BackstageTheme } from '@backstage/theme';
import { UnregisterEntity, UnregisterEntityOptions } from './UnregisterEntity';
/** @public */
export type EntityContextMenuClassKey = 'button';
@@ -55,14 +55,9 @@ interface ExtraContextMenuItem {
onClick: () => void;
}
// unstable context menu option, eg: disable the unregister entity menu
interface contextMenuOptions {
disableUnregister: boolean;
}
interface EntityContextMenuProps {
UNSTABLE_extraContextMenuItems?: ExtraContextMenuItem[];
UNSTABLE_contextMenuOptions?: contextMenuOptions;
UNSTABLE_contextMenuOptions?: UnregisterEntityOptions;
onUnregisterEntity: () => void;
onInspectEntity: () => void;
}
@@ -79,6 +74,7 @@ export function EntityContextMenu(props: EntityContextMenuProps) {
const unregisterPermission = useEntityPermission(
catalogEntityDeletePermission,
);
const isAllowed = unregisterPermission.allowed;
const onOpen = (event: React.SyntheticEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
@@ -106,11 +102,6 @@ export function EntityContextMenu(props: EntityContextMenuProps) {
<Divider key="the divider is here!" />,
];
const disableUnregister =
(!unregisterPermission.allowed ||
UNSTABLE_contextMenuOptions?.disableUnregister) ??
false;
return (
<>
<IconButton
@@ -136,18 +127,12 @@ export function EntityContextMenu(props: EntityContextMenuProps) {
>
<MenuList>
{extraItems}
<MenuItem
onClick={() => {
onClose();
onUnregisterEntity();
}}
disabled={disableUnregister}
>
<ListItemIcon>
<CancelIcon fontSize="small" />
</ListItemIcon>
<ListItemText primary="Unregister entity" />
</MenuItem>
<UnregisterEntity
unregisterEntityOptions={UNSTABLE_contextMenuOptions}
isUnregisterAllowed={isAllowed}
onUnregisterEntity={onUnregisterEntity}
onClose={onClose}
/>
<MenuItem
onClick={() => {
onClose();
@@ -0,0 +1,80 @@
/*
* Copyright 2020 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 { EntityProvider } from '@backstage/plugin-catalog-react';
import { permissionApiRef } from '@backstage/plugin-permission-react';
import {
MockPermissionApi,
renderInTestApp,
TestApiProvider,
} from '@backstage/test-utils';
import { fireEvent, screen } from '@testing-library/react';
import * as React from 'react';
import { UnregisterEntity } from './UnregisterEntity';
const mockPermissionApi = new MockPermissionApi();
function render(children: React.ReactNode) {
return renderInTestApp(
<TestApiProvider apis={[[permissionApiRef, mockPermissionApi]]}>
<EntityProvider
entity={{ apiVersion: 'a', kind: 'b', metadata: { name: 'c' } }}
children={children}
/>
</TestApiProvider>,
);
}
describe('ComponentContextMenu', () => {
it('should call onUnregisterEntity on button click', async () => {
const mockCallback = jest.fn();
await render(
<UnregisterEntity
unregisterEntityOptions={{ disableUnregister: 'visible' }}
isUnregisterAllowed
onUnregisterEntity={mockCallback}
onClose={() => {}}
/>,
);
const unregister = await screen.findByText('Unregister entity');
expect(unregister).toBeInTheDocument();
fireEvent.click(unregister);
expect(mockCallback).toBeCalled();
});
it('check Unregister entity button is disabled', async () => {
const mockCallback = jest.fn();
const { getByText } = await render(
<UnregisterEntity
unregisterEntityOptions={{ disableUnregister: 'disable' }}
isUnregisterAllowed
onUnregisterEntity={mockCallback}
onClose={() => {}}
/>,
);
const unregister = await screen.getByText('Unregister entity');
expect(unregister).toBeInTheDocument();
const unregisterSpanItem = getByText(/Unregister entity/);
const unregisterMenuListItem =
unregisterSpanItem?.parentElement?.parentElement;
expect(unregisterMenuListItem).toHaveAttribute('aria-disabled');
});
});
@@ -0,0 +1,72 @@
/*
* Copyright 2020 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 { ListItemIcon, ListItemText, MenuItem } from '@material-ui/core';
import CancelIcon from '@material-ui/icons/Cancel';
type VisibleType = 'visible' | 'hidden' | 'disable';
export type UnregisterEntityOptions = {
disableUnregister: boolean | VisibleType;
};
interface UnregisterEntityProps {
unregisterEntityOptions?: UnregisterEntityOptions;
isUnregisterAllowed: boolean;
onUnregisterEntity: () => void;
onClose: () => void;
}
export function UnregisterEntity(props: UnregisterEntityProps) {
const {
unregisterEntityOptions,
isUnregisterAllowed,
onUnregisterEntity,
onClose,
} = props;
const isBoolean =
typeof unregisterEntityOptions?.disableUnregister === 'boolean';
const isDisabled =
(!isUnregisterAllowed ||
(isBoolean
? !!unregisterEntityOptions?.disableUnregister
: unregisterEntityOptions?.disableUnregister === 'disable')) ??
false;
let unregisterButton = <></>;
if (unregisterEntityOptions?.disableUnregister !== 'hidden') {
unregisterButton = (
<MenuItem
onClick={() => {
onClose();
onUnregisterEntity();
}}
disabled={isDisabled}
>
<ListItemIcon>
<CancelIcon fontSize="small" />
</ListItemIcon>
<ListItemText primary="Unregister entity" />
</MenuItem>
);
}
return <>{unregisterButton}</>;
}
@@ -142,15 +142,18 @@ interface ExtraContextMenuItem {
onClick: () => void;
}
type VisibleType = 'visible' | 'hidden' | 'disable';
// NOTE(blam): Intentionally not exported at this point, since it's part of
// unstable context menu option, eg: disable the unregister entity menu
interface contextMenuOptions {
disableUnregister: boolean;
interface EntityContextMenuOptions {
disableUnregister: boolean | VisibleType;
}
/** @public */
export interface EntityLayoutProps {
UNSTABLE_extraContextMenuItems?: ExtraContextMenuItem[];
UNSTABLE_contextMenuOptions?: contextMenuOptions;
UNSTABLE_contextMenuOptions?: EntityContextMenuOptions;
children?: React.ReactNode;
NotFoundComponent?: React.ReactNode;
}