Added support for custom icon/prefix in panel menu extensions.
This commit is contained in:
@@ -188,6 +188,7 @@ export interface PanelOptionsEditorConfig<TOptions, TSettings = any, TValue = an
|
|||||||
export interface PanelMenuItem {
|
export interface PanelMenuItem {
|
||||||
type?: 'submenu' | 'divider' | 'group';
|
type?: 'submenu' | 'divider' | 'group';
|
||||||
text: string;
|
text: string;
|
||||||
|
prefix?: React.ReactElement;
|
||||||
iconClassName?: IconName;
|
iconClassName?: IconName;
|
||||||
onClick?: (event: React.MouseEvent) => void;
|
onClick?: (event: React.MouseEvent) => void;
|
||||||
shortcut?: string;
|
shortcut?: string;
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ export type PluginExtensionLink = PluginExtensionBase & {
|
|||||||
type: PluginExtensionTypes.link;
|
type: PluginExtensionTypes.link;
|
||||||
path?: string;
|
path?: string;
|
||||||
onClick?: (event?: React.MouseEvent) => void;
|
onClick?: (event?: React.MouseEvent) => void;
|
||||||
icon?: IconName | React.ReactNode;
|
icon?: IconName;
|
||||||
|
suffix?: React.ReactElement;
|
||||||
|
prefix?: React.ReactElement;
|
||||||
category?: string;
|
category?: string;
|
||||||
openInNewTab?: boolean;
|
openInNewTab?: boolean;
|
||||||
};
|
};
|
||||||
@@ -106,7 +108,9 @@ export type PluginAddedLinksConfigureFunc<Context extends object> = (context: Re
|
|||||||
description: string;
|
description: string;
|
||||||
path: string;
|
path: string;
|
||||||
onClick: (event: React.MouseEvent | undefined, helpers: PluginExtensionEventHelpers<Context>) => void;
|
onClick: (event: React.MouseEvent | undefined, helpers: PluginExtensionEventHelpers<Context>) => void;
|
||||||
icon: IconName | React.ReactNode;
|
icon: IconName;
|
||||||
|
suffix: React.ReactElement;
|
||||||
|
prefix: React.ReactElement;
|
||||||
category: string;
|
category: string;
|
||||||
openInNewTab: boolean;
|
openInNewTab: boolean;
|
||||||
}>
|
}>
|
||||||
@@ -136,7 +140,7 @@ export type PluginExtensionAddedLinkConfig<Context extends object = object> = Pl
|
|||||||
configure?: PluginAddedLinksConfigureFunc<Context>;
|
configure?: PluginAddedLinksConfigureFunc<Context>;
|
||||||
|
|
||||||
// (Optional) A icon that can be displayed in the ui for the extension option.
|
// (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
|
// (Optional) A category to be used when grouping the options in the ui
|
||||||
category?: string;
|
category?: string;
|
||||||
@@ -144,6 +148,12 @@ export type PluginExtensionAddedLinkConfig<Context extends object = object> = Pl
|
|||||||
// (Optional) If true, opens the link in a new tab (renders with target="_blank")
|
// (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.)
|
// (Important: this is not guaranteed, depends on the extension point if it implements it.)
|
||||||
openInNewTab?: boolean;
|
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<Props = {}> = PluginExtensionConfigBase & {
|
export type PluginExtensionExposedComponentConfig<Props = {}> = PluginExtensionConfigBase & {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { IconName } from '../../types/icon';
|
|||||||
import { Icon } from '../Icon/Icon';
|
import { Icon } from '../Icon/Icon';
|
||||||
import { Stack } from '../Layout/Stack/Stack';
|
import { Stack } from '../Layout/Stack/Stack';
|
||||||
|
|
||||||
|
import { MenuItemPrefix } from './MenuItemPrefix';
|
||||||
import { SubMenu } from './SubMenu';
|
import { SubMenu } from './SubMenu';
|
||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
@@ -30,6 +31,8 @@ export interface MenuItemProps<T = unknown> {
|
|||||||
target?: LinkTarget;
|
target?: LinkTarget;
|
||||||
/** Icon of the menu item */
|
/** Icon of the menu item */
|
||||||
icon?: IconName;
|
icon?: IconName;
|
||||||
|
/** Prefix of the menu item if icon is specified prefix will be ignored */
|
||||||
|
prefix?: React.ReactElement;
|
||||||
/** Role of the menu item */
|
/** Role of the menu item */
|
||||||
role?: AriaRole;
|
role?: AriaRole;
|
||||||
/** Url of the menu item */
|
/** Url of the menu item */
|
||||||
@@ -79,6 +82,7 @@ export const MenuItem = React.memo(
|
|||||||
customSubMenuContainerStyles,
|
customSubMenuContainerStyles,
|
||||||
shortcut,
|
shortcut,
|
||||||
testId,
|
testId,
|
||||||
|
prefix,
|
||||||
} = props;
|
} = props;
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
const [isActive, setIsActive] = useState(active);
|
const [isActive, setIsActive] = useState(active);
|
||||||
@@ -176,7 +180,7 @@ export const MenuItem = React.memo(
|
|||||||
{...disabledProps}
|
{...disabledProps}
|
||||||
>
|
>
|
||||||
<Stack direction="row" justifyContent="flex-start" alignItems="center">
|
<Stack direction="row" justifyContent="flex-start" alignItems="center">
|
||||||
{icon && <Icon name={icon} className={styles.icon} aria-hidden />}
|
<MenuItemPrefix icon={icon} prefix={prefix} />
|
||||||
<span className={cx(styles.ellipsis, styles.label)}>{label}</span>
|
<span className={cx(styles.ellipsis, styles.label)}>{label}</span>
|
||||||
<div className={cx(styles.rightWrapper, { [styles.withShortcut]: hasShortcut })}>
|
<div className={cx(styles.rightWrapper, { [styles.withShortcut]: hasShortcut })}>
|
||||||
{hasShortcut && (
|
{hasShortcut && (
|
||||||
@@ -272,9 +276,6 @@ const getStyles = (theme: GrafanaTheme2) => {
|
|||||||
color: theme.colors.action.disabledText,
|
color: theme.colors.action.disabledText,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
icon: css({
|
|
||||||
opacity: 0.7,
|
|
||||||
}),
|
|
||||||
rightWrapper: css({
|
rightWrapper: css({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
|
|||||||
@@ -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 <Icon name={icon} className={styles.icon} aria-hidden />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.prefix} aria-hidden>
|
||||||
|
{prefix}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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',
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ export function PanelHeaderMenu({ items }: Props) {
|
|||||||
key={item.text}
|
key={item.text}
|
||||||
label={item.text}
|
label={item.text}
|
||||||
icon={item.iconClassName}
|
icon={item.iconClassName}
|
||||||
|
prefix={item.prefix}
|
||||||
childItems={item.subMenu ? renderItems(item.subMenu) : undefined}
|
childItems={item.subMenu ? renderItems(item.subMenu) : undefined}
|
||||||
url={item.href}
|
url={item.href}
|
||||||
onClick={item.onClick}
|
onClick={item.onClick}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ export type AddedLinkRegistryItem<Context extends object = object> = {
|
|||||||
onClick?: (event: React.MouseEvent | undefined, helpers: PluginExtensionEventHelpers<Context>) => void;
|
onClick?: (event: React.MouseEvent | undefined, helpers: PluginExtensionEventHelpers<Context>) => void;
|
||||||
configure?: PluginAddedLinksConfigureFunc<Context>;
|
configure?: PluginAddedLinksConfigureFunc<Context>;
|
||||||
icon?: IconName;
|
icon?: IconName;
|
||||||
|
suffix?: React.ReactElement;
|
||||||
|
prefix?: React.ReactElement;
|
||||||
category?: string;
|
category?: string;
|
||||||
openInNewTab?: boolean;
|
openInNewTab?: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -86,6 +86,8 @@ export function usePluginLinks({
|
|||||||
path: isString(path) ? getLinkExtensionPathWithTracking(pluginId, path, extensionPointId) : undefined,
|
path: isString(path) ? getLinkExtensionPathWithTracking(pluginId, path, extensionPointId) : undefined,
|
||||||
category: overrides?.category || addedLink.category,
|
category: overrides?.category || addedLink.category,
|
||||||
openInNewTab: overrides?.openInNewTab ?? addedLink.openInNewTab,
|
openInNewTab: overrides?.openInNewTab ?? addedLink.openInNewTab,
|
||||||
|
suffix: overrides?.suffix || addedLink.suffix,
|
||||||
|
prefix: overrides?.prefix || addedLink.prefix,
|
||||||
};
|
};
|
||||||
|
|
||||||
extensions.push(extension);
|
extensions.push(extension);
|
||||||
|
|||||||
@@ -421,6 +421,7 @@ export function createExtensionSubMenu(extensions: PluginExtensionLink[]): Panel
|
|||||||
onClick: extension.onClick,
|
onClick: extension.onClick,
|
||||||
iconClassName: extension.icon,
|
iconClassName: extension.icon,
|
||||||
target: extension.openInNewTab ? '_blank' : undefined,
|
target: extension.openInNewTab ? '_blank' : undefined,
|
||||||
|
prefix: extension.prefix,
|
||||||
});
|
});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -435,6 +436,7 @@ export function createExtensionSubMenu(extensions: PluginExtensionLink[]): Panel
|
|||||||
onClick: extension.onClick,
|
onClick: extension.onClick,
|
||||||
iconClassName: extension.icon,
|
iconClassName: extension.icon,
|
||||||
target: extension.openInNewTab ? '_blank' : undefined,
|
target: extension.openInNewTab ? '_blank' : undefined,
|
||||||
|
prefix: extension.prefix,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -484,6 +486,8 @@ export function getLinkExtensionOverrides(
|
|||||||
icon = config.icon,
|
icon = config.icon,
|
||||||
category = config.category,
|
category = config.category,
|
||||||
openInNewTab = config.openInNewTab,
|
openInNewTab = config.openInNewTab,
|
||||||
|
suffix = config.suffix,
|
||||||
|
prefix = config.prefix,
|
||||||
...rest
|
...rest
|
||||||
} = overrides;
|
} = overrides;
|
||||||
|
|
||||||
@@ -509,6 +513,8 @@ export function getLinkExtensionOverrides(
|
|||||||
icon,
|
icon,
|
||||||
category,
|
category,
|
||||||
openInNewTab,
|
openInNewTab,
|
||||||
|
suffix,
|
||||||
|
prefix,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user