add EntityContextMenuItemBlueprint

Signed-off-by: Mark Dunphy <markd@spotify.com>
This commit is contained in:
Mark Dunphy
2025-03-11 10:56:29 -04:00
parent 32cbef5b4c
commit 3e40a77aeb
7 changed files with 100 additions and 2 deletions
@@ -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<JSX.Element>;
};
/** @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 (
<MenuItem component="a" href={href}>
<ListItemIcon>{params.icon}</ListItemIcon>
<ListItemText primary={params.title} />
</MenuItem>
);
};
};
yield coreExtensionData.reactElement(
ExtensionBoundary.lazy(node, loaderFactory()),
);
},
});
@@ -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';
@@ -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: {
<EntityContextMenu
UNSTABLE_extraContextMenuItems={UNSTABLE_extraContextMenuItems}
UNSTABLE_contextMenuOptions={UNSTABLE_contextMenuOptions}
extraMenuItems={extraMenuItems}
onInspectEntity={openInspectEntityDialog}
onUnregisterEntity={openUnregisterEntityDialog}
/>
@@ -62,6 +62,7 @@ export interface EntityLayoutProps {
UNSTABLE_extraContextMenuItems?: ComponentProps<
typeof EntityHeader
>['UNSTABLE_extraContextMenuItems'];
extraMenuItems?: ComponentProps<typeof EntityHeader>['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}
/>
)}
+9 -2
View File
@@ -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,
) ?? <EntityHeader />;
) ?? <EntityHeader extraMenuItems={extraMenuItems} />;
let groups = Object.entries(defaultEntityContentGroups).reduce<Groups>(
(rest, group) => {
@@ -134,7 +141,7 @@ export const catalogEntityPage = PageBlueprint.makeWithOverrides({
const Component = () => {
return (
<AsyncEntityProvider {...useEntityFromUrl()}>
<EntityLayout header={header}>
<EntityLayout header={header} extraMenuItems={extraMenuItems}>
{Object.values(groups).flatMap(({ title, items }) =>
items.map(output => (
<EntityLayout.Route
@@ -61,6 +61,7 @@ interface ExtraContextMenuItem {
interface EntityContextMenuProps {
UNSTABLE_extraContextMenuItems?: ExtraContextMenuItem[];
UNSTABLE_contextMenuOptions?: UnregisterEntityOptions;
extraMenuItems?: JSX.Element[];
onUnregisterEntity: () => 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) {
>
<MenuList autoFocusItem={Boolean(anchorEl)}>
{extraItems}
{extraMenuItems}
<UnregisterEntity
unregisterEntityOptions={UNSTABLE_contextMenuOptions}
isUnregisterAllowed={isAllowed}
@@ -181,6 +181,7 @@ export interface EntityLayoutProps {
UNSTABLE_contextMenuOptions?: EntityContextMenuOptions;
children?: ReactNode;
NotFoundComponent?: ReactNode;
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
@@ -245,6 +246,7 @@ export const EntityLayout = (props: EntityLayoutProps) => {
const {
UNSTABLE_extraContextMenuItems,
UNSTABLE_contextMenuOptions,
extraMenuItems,
children,
NotFoundComponent,
parentEntityRelations,
@@ -360,6 +362,7 @@ export const EntityLayout = (props: EntityLayoutProps) => {
<EntityContextMenu
UNSTABLE_extraContextMenuItems={UNSTABLE_extraContextMenuItems}
UNSTABLE_contextMenuOptions={UNSTABLE_contextMenuOptions}
extraMenuItems={extraMenuItems}
onUnregisterEntity={() => setConfirmationDialogOpen(true)}
onInspectEntity={() => setSearchParams('inspect')}
/>