From cc8505f4e00a21a19c9f2a9194798d4a5f9693ab Mon Sep 17 00:00:00 2001 From: Marcus Andersson Date: Tue, 13 Jan 2026 21:39:03 +0100 Subject: [PATCH] Added support for custom icon/prefix in panel menu extensions. --- packages/grafana-data/src/types/panel.ts | 1 + .../src/types/pluginExtensions.ts | 16 +++++-- .../src/components/Menu/MenuItem.tsx | 9 ++-- .../src/components/Menu/MenuItemPrefix.tsx | 48 +++++++++++++++++++ .../dashgrid/PanelHeader/PanelHeaderMenu.tsx | 1 + .../extensions/registry/AddedLinksRegistry.ts | 2 + .../plugins/extensions/usePluginLinks.tsx | 2 + .../app/features/plugins/extensions/utils.tsx | 6 +++ 8 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 packages/grafana-ui/src/components/Menu/MenuItemPrefix.tsx diff --git a/packages/grafana-data/src/types/panel.ts b/packages/grafana-data/src/types/panel.ts index b9d6491cf9b..df7ab01a9c3 100644 --- a/packages/grafana-data/src/types/panel.ts +++ b/packages/grafana-data/src/types/panel.ts @@ -188,6 +188,7 @@ export interface PanelOptionsEditorConfig void; shortcut?: string; diff --git a/packages/grafana-data/src/types/pluginExtensions.ts b/packages/grafana-data/src/types/pluginExtensions.ts index 76241735a52..385da2dfb09 100644 --- a/packages/grafana-data/src/types/pluginExtensions.ts +++ b/packages/grafana-data/src/types/pluginExtensions.ts @@ -28,7 +28,9 @@ export type PluginExtensionLink = PluginExtensionBase & { type: PluginExtensionTypes.link; path?: string; onClick?: (event?: React.MouseEvent) => void; - icon?: IconName | React.ReactNode; + icon?: IconName; + suffix?: React.ReactElement; + prefix?: React.ReactElement; category?: string; openInNewTab?: boolean; }; @@ -106,7 +108,9 @@ export type PluginAddedLinksConfigureFunc = (context: Re description: string; path: string; onClick: (event: React.MouseEvent | undefined, helpers: PluginExtensionEventHelpers) => void; - icon: IconName | React.ReactNode; + icon: IconName; + suffix: React.ReactElement; + prefix: React.ReactElement; category: string; openInNewTab: boolean; }> @@ -136,7 +140,7 @@ export type PluginExtensionAddedLinkConfig = Pl configure?: PluginAddedLinksConfigureFunc; // (Optional) A icon that can be displayed in the ui for the extension option. - icon?: IconName | React.ReactNode; + icon?: IconName; // (Optional) A category to be used when grouping the options in the ui category?: string; @@ -144,6 +148,12 @@ export type PluginExtensionAddedLinkConfig = Pl // (Optional) If true, opens the link in a new tab (renders with target="_blank") // (Important: this is not guaranteed, depends on the extension point if it implements it.) openInNewTab?: boolean; + + // (Optional) A React element that will be displayed after the title + suffix?: React.ReactElement; + + // (Optional) A React element that will be displayed before the title + prefix?: React.ReactElement; }; export type PluginExtensionExposedComponentConfig = PluginExtensionConfigBase & { diff --git a/packages/grafana-ui/src/components/Menu/MenuItem.tsx b/packages/grafana-ui/src/components/Menu/MenuItem.tsx index 078b9b7e12e..68db053ac44 100644 --- a/packages/grafana-ui/src/components/Menu/MenuItem.tsx +++ b/packages/grafana-ui/src/components/Menu/MenuItem.tsx @@ -11,6 +11,7 @@ import { IconName } from '../../types/icon'; import { Icon } from '../Icon/Icon'; import { Stack } from '../Layout/Stack/Stack'; +import { MenuItemPrefix } from './MenuItemPrefix'; import { SubMenu } from './SubMenu'; /** @internal */ @@ -30,6 +31,8 @@ export interface MenuItemProps { target?: LinkTarget; /** Icon of the menu item */ icon?: IconName; + /** Prefix of the menu item if icon is specified prefix will be ignored */ + prefix?: React.ReactElement; /** Role of the menu item */ role?: AriaRole; /** Url of the menu item */ @@ -79,6 +82,7 @@ export const MenuItem = React.memo( customSubMenuContainerStyles, shortcut, testId, + prefix, } = props; const styles = useStyles2(getStyles); const [isActive, setIsActive] = useState(active); @@ -176,7 +180,7 @@ export const MenuItem = React.memo( {...disabledProps} > - {icon && } + {label}
{hasShortcut && ( @@ -272,9 +276,6 @@ const getStyles = (theme: GrafanaTheme2) => { color: theme.colors.action.disabledText, }, }), - icon: css({ - opacity: 0.7, - }), rightWrapper: css({ display: 'flex', alignItems: 'center', diff --git a/packages/grafana-ui/src/components/Menu/MenuItemPrefix.tsx b/packages/grafana-ui/src/components/Menu/MenuItemPrefix.tsx new file mode 100644 index 00000000000..f6f76541b02 --- /dev/null +++ b/packages/grafana-ui/src/components/Menu/MenuItemPrefix.tsx @@ -0,0 +1,48 @@ +import { css } from '@emotion/css'; + +import { IconName } from '@grafana/data'; + +import { useStyles2 } from '../../themes/ThemeContext'; +import { Icon } from '../Icon/Icon'; +import { getSvgSize } from '../Icon/utils'; + +type MenuItemPrefixProps = { + prefix?: React.ReactElement; + icon?: IconName; +}; + +/** @internal */ +export function MenuItemPrefix({ prefix, icon }: MenuItemPrefixProps): React.ReactNode { + const styles = useStyles2(getStyles); + + if (!icon && !prefix) { + return null; + } + + if (icon) { + return ; + } + + return ( +
+ {prefix} +
+ ); +} + +function getStyles() { + const prefixSize = getSvgSize('md'); + + return { + icon: css({ + opacity: 0.7, + }), + prefix: css({ + display: 'inline-block', + verticalAlign: 'middle', + width: prefixSize, + height: prefixSize, + overflow: 'hidden', + }), + }; +} diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx index 90555fa7535..4044fabe31f 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderMenu.tsx @@ -29,6 +29,7 @@ export function PanelHeaderMenu({ items }: Props) { key={item.text} label={item.text} icon={item.iconClassName} + prefix={item.prefix} childItems={item.subMenu ? renderItems(item.subMenu) : undefined} url={item.href} onClick={item.onClick} diff --git a/public/app/features/plugins/extensions/registry/AddedLinksRegistry.ts b/public/app/features/plugins/extensions/registry/AddedLinksRegistry.ts index c3dd9faf6f1..c3892074d55 100644 --- a/public/app/features/plugins/extensions/registry/AddedLinksRegistry.ts +++ b/public/app/features/plugins/extensions/registry/AddedLinksRegistry.ts @@ -20,6 +20,8 @@ export type AddedLinkRegistryItem = { onClick?: (event: React.MouseEvent | undefined, helpers: PluginExtensionEventHelpers) => void; configure?: PluginAddedLinksConfigureFunc; icon?: IconName; + suffix?: React.ReactElement; + prefix?: React.ReactElement; category?: string; openInNewTab?: boolean; }; diff --git a/public/app/features/plugins/extensions/usePluginLinks.tsx b/public/app/features/plugins/extensions/usePluginLinks.tsx index 9ec5d215f94..2e32c85f081 100644 --- a/public/app/features/plugins/extensions/usePluginLinks.tsx +++ b/public/app/features/plugins/extensions/usePluginLinks.tsx @@ -86,6 +86,8 @@ export function usePluginLinks({ path: isString(path) ? getLinkExtensionPathWithTracking(pluginId, path, extensionPointId) : undefined, category: overrides?.category || addedLink.category, openInNewTab: overrides?.openInNewTab ?? addedLink.openInNewTab, + suffix: overrides?.suffix || addedLink.suffix, + prefix: overrides?.prefix || addedLink.prefix, }; extensions.push(extension); diff --git a/public/app/features/plugins/extensions/utils.tsx b/public/app/features/plugins/extensions/utils.tsx index 0046aab0fe6..e21fb4a5e67 100644 --- a/public/app/features/plugins/extensions/utils.tsx +++ b/public/app/features/plugins/extensions/utils.tsx @@ -421,6 +421,7 @@ export function createExtensionSubMenu(extensions: PluginExtensionLink[]): Panel onClick: extension.onClick, iconClassName: extension.icon, target: extension.openInNewTab ? '_blank' : undefined, + prefix: extension.prefix, }); continue; } @@ -435,6 +436,7 @@ export function createExtensionSubMenu(extensions: PluginExtensionLink[]): Panel onClick: extension.onClick, iconClassName: extension.icon, target: extension.openInNewTab ? '_blank' : undefined, + prefix: extension.prefix, }); } @@ -484,6 +486,8 @@ export function getLinkExtensionOverrides( icon = config.icon, category = config.category, openInNewTab = config.openInNewTab, + suffix = config.suffix, + prefix = config.prefix, ...rest } = overrides; @@ -509,6 +513,8 @@ export function getLinkExtensionOverrides( icon, category, openInNewTab, + suffix, + prefix, }; } catch (error) { if (error instanceof Error) {