Menu: refactor MenuItem and MenuGroup to be standalone component (#31639)

* Menu: refactor MenuItem and MenuGroup to be standalone component

* fixes small nits

* Chore: Refactored other components to correspond with the new Menu system (#31676)

* fixes affected components using Menu

* fixes affected components using Menu components

* fixes frontend test- I hope

* fixes frontend docs test- I hope

* fixes frontend docs test- I hope

* fixes frontend docs test- I hope

* fixes frontend docs test- I hope

* fixes frontend docs test- I hope

* added support for accessibility

* fixes frontend test- I hope

* Improve storybook story and simplify ButtonSelect

* Fixed broken graph context menu

* fixes frontend test- I hope

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
Uchechukwu Obasi 2021-03-11 15:35:17 +01:00 committed by GitHub
parent 77a024abb3
commit f51653647d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 328 additions and 253 deletions

View File

@ -13,6 +13,11 @@ export const Components = {
},
},
},
Menu: {
MenuComponent: (title: string) => `${title} menu`,
MenuGroup: (title: string) => `${title} menu group`,
MenuItem: (title: string) => `${title} menu item`,
},
Panels: {
Panel: {
title: (title: string) => `Panel header title item ${title}`,

View File

@ -16,10 +16,18 @@ export default {
},
};
const menuItems = [{ label: 'Test', items: [{ label: 'First' }, { label: 'Second' }] }];
const menuItems = [
{
label: 'Test',
items: [
{ label: 'First', ariaLabel: 'First' },
{ label: 'Second', ariaLabel: 'Second' },
],
},
];
export const Basic = () => {
return <ContextMenu x={10} y={11} onClose={() => {}} items={menuItems} />;
return <ContextMenu x={10} y={11} onClose={() => {}} itemsGroup={menuItems} />;
};
export const WithState = () => {

View File

@ -1,7 +1,10 @@
import React, { useRef, useState, useLayoutEffect } from 'react';
import React, { useRef, useState, useLayoutEffect, useCallback } from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { useClickAway } from 'react-use';
import { Portal } from '../Portal/Portal';
import { Menu, MenuItemsGroup } from '../Menu/Menu';
import { Menu } from '../Menu/Menu';
import { MenuGroup, MenuItemsGroup } from '../Menu/MenuGroup';
import { MenuItem } from '../Menu/MenuItem';
export interface ContextMenuProps {
/** Starting horizontal position for the menu */
@ -11,12 +14,12 @@ export interface ContextMenuProps {
/** Callback for closing the menu */
onClose?: () => void;
/** List of the menu items to display */
items?: MenuItemsGroup[];
itemsGroup?: MenuItemsGroup[];
/** A function that returns header element */
renderHeader?: () => React.ReactNode;
}
export const ContextMenu: React.FC<ContextMenuProps> = React.memo(({ x, y, onClose, items, renderHeader }) => {
export const ContextMenu: React.FC<ContextMenuProps> = React.memo(({ x, y, onClose, itemsGroup, renderHeader }) => {
const menuRef = useRef<HTMLDivElement>(null);
const [positionStyles, setPositionStyles] = useState({});
@ -44,10 +47,38 @@ export const ContextMenu: React.FC<ContextMenuProps> = React.memo(({ x, y, onClo
}
});
const onClick = useCallback(() => {
if (onClose) {
onClose();
}
}, [onClose]);
const header = renderHeader && renderHeader();
return (
<Portal>
<Menu header={header} items={items} onClose={onClose} ref={menuRef} style={positionStyles} />
<Menu
header={header}
ref={menuRef}
style={positionStyles}
ariaLabel={selectors.components.Menu.MenuComponent('Context')}
>
{itemsGroup?.map((group, index) => (
<MenuGroup key={`${group.label}${index}`} label={group.label} ariaLabel={group.label}>
{(group.items || []).map((item) => (
<MenuItem
key={`${item.label}`}
url={item.url}
label={item.label}
ariaLabel={item.label}
target={item.target}
icon={item.icon}
active={item.active}
onClick={onClick}
/>
))}
</MenuGroup>
))}
</Menu>
</Portal>
);
});

View File

@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { ContextMenu } from '../ContextMenu/ContextMenu';
import { MenuItemsGroup } from '../Menu/Menu';
import { MenuItemsGroup } from '../Menu/MenuGroup';
interface WithContextMenuProps {
/** Menu item trigger that accepts openMenu prop */
@ -30,7 +30,7 @@ export const WithContextMenu: React.FC<WithContextMenuProps> = ({ children, getC
onClose={() => setIsMenuOpen(false)}
x={menuPosition.x}
y={menuPosition.y}
items={getContextMenuItems()}
itemsGroup={getContextMenuItems()}
/>
)}
</>

View File

@ -5,7 +5,8 @@ import { ToolbarButtonVariant, ToolbarButton } from '../Button';
import { ClickOutsideWrapper } from '../ClickOutsideWrapper/ClickOutsideWrapper';
import { css } from 'emotion';
import { useStyles } from '../../themes/ThemeContext';
import { Menu, MenuItemsGroup } from '../Menu/Menu';
import { Menu } from '../Menu/Menu';
import { MenuItem } from '../Menu/MenuItem';
export interface Props<T> extends HTMLAttributes<HTMLButtonElement> {
className?: string;
@ -41,14 +42,6 @@ export const ButtonSelect = React.memo(<T,>(props: Props<T>) => {
setIsOpen(false);
};
const menuGroup: MenuItemsGroup = {
items: options.map((item) => ({
label: (item.label || item.value) as string,
onClick: () => onChangeInternal(item),
active: item.value === value?.value,
})),
};
return (
<>
<ToolbarButton
@ -64,7 +57,17 @@ export const ButtonSelect = React.memo(<T,>(props: Props<T>) => {
{isOpen && (
<div className={styles.menuWrapper}>
<ClickOutsideWrapper onClick={onCloseMenu} parent={document}>
<Menu items={[menuGroup]} />
<Menu>
{options.map((item) => (
<MenuItem
key={`${item.value}`}
label={(item.label || item.value) as string}
ariaLabel={(item.label || item.value) as string}
onClick={() => onChangeInternal(item)}
active={item.value === value?.value}
/>
))}
</Menu>
</ClickOutsideWrapper>
</div>
)}

View File

@ -29,7 +29,7 @@ export type GraphContextMenuProps = ContextMenuProps & {
export const GraphContextMenu: React.FC<GraphContextMenuProps> = ({
getContextMenuSource,
timeZone,
items,
itemsGroup,
dimensions,
contextDimensions,
...otherProps
@ -37,8 +37,8 @@ export const GraphContextMenu: React.FC<GraphContextMenuProps> = ({
const source = getContextMenuSource();
// Do not render items that do not have label specified
const itemsToRender = items
? items.map((group) => ({
const itemsToRender = itemsGroup
? itemsGroup.map((group) => ({
...group,
items: group.items.filter((item) => item.label),
}))
@ -81,7 +81,7 @@ export const GraphContextMenu: React.FC<GraphContextMenuProps> = ({
);
};
return <ContextMenu {...otherProps} items={itemsToRender} renderHeader={renderHeader} />;
return <ContextMenu {...otherProps} itemsGroup={itemsToRender} renderHeader={renderHeader} />;
};
/** @internal */

View File

@ -1,14 +1,18 @@
import React from 'react';
import { Story } from '@storybook/react';
import { Menu, MenuProps } from './Menu';
import { MenuItem } from './MenuItem';
import { MenuGroup } from './MenuGroup';
import { GraphContextMenuHeader } from '..';
import { StoryExample } from '../../utils/storybook/StoryExample';
import { VerticalGroup } from '../Layout/Layout';
export default {
title: 'General/Menu',
component: Menu,
argTypes: {
items: { control: { disable: true } },
header: { control: { disable: true } },
icon: { control: { type: 'select' } },
},
parameters: {
knobs: {
@ -23,39 +27,34 @@ export default {
},
};
export const Simple: Story<MenuProps> = (args) => (
<div>
<Menu {...args} />
</div>
);
export const Simple: Story<MenuProps> = (args) => {
return (
<VerticalGroup>
<StoryExample name="Simple">
<Menu>
<MenuItem label="Google" icon="search-plus" ariaLabel="Menu item" />
<MenuItem label="Filter" icon="filter" ariaLabel="Menu item" />
<MenuItem label="History" icon="history" ariaLabel="Menu item" />
<MenuItem label="Active" icon="history" active ariaLabel="Menu item" />
<MenuItem label="Apps" icon="apps" ariaLabel="Menu item" />
</Menu>
</StoryExample>
<StoryExample name="With header & groups">
<Menu header={args.header} ariaLabel="Menu header">
<MenuGroup label="Group 1" ariaLabel="Menu Group">
<MenuItem label="item1" icon="history" ariaLabel="Menu item" />
<MenuItem label="item2" icon="filter" ariaLabel="Menu item" />
</MenuGroup>
<MenuGroup label="Group 2" ariaLabel="Menu Group">
<MenuItem label="item1" icon="history" ariaLabel="Menu item" />
</MenuGroup>
</Menu>
</StoryExample>
</VerticalGroup>
);
};
Simple.args = {
items: [
{
label: 'Group 1',
items: [
{
label: 'Menu item 1',
icon: 'history',
},
{
label: 'Menu item 2',
icon: 'filter',
},
],
},
{
label: 'Group 2',
items: [
{
label: 'Menu item 1',
},
{
label: 'Menu item 2',
},
],
},
],
header: (
<GraphContextMenuHeader
timestamp="2020-11-25 19:04:25"

View File

@ -0,0 +1,45 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { selectors } from '@grafana/e2e-selectors';
import { Menu } from './Menu';
import { MenuGroup } from './MenuGroup';
import { MenuItem } from './MenuItem';
describe('Menu', () => {
it('renders items without error', () => {
expect(() => {
render(
<Menu ariaLabel={selectors.components.Menu.MenuComponent('Test')} header="mock header">
<MenuGroup ariaLabel={selectors.components.Menu.MenuGroup('Test')} label="Group 1">
<MenuItem
ariaLabel={selectors.components.Menu.MenuItem('Test')}
label="item1"
icon="history"
active={true}
/>
<MenuItem
ariaLabel={selectors.components.Menu.MenuItem('Test')}
label="item2"
icon="filter"
active={true}
/>
</MenuGroup>
</Menu>
);
});
});
it('renders correct contents', () => {
render(
<Menu ariaLabel={selectors.components.Menu.MenuComponent('Test')} header="mock header">
<MenuGroup ariaLabel={selectors.components.Menu.MenuGroup('Test')} label="Group 1">
<MenuItem ariaLabel={selectors.components.Menu.MenuItem('Test')} label="item1" icon="history" active={true} />
<MenuItem ariaLabel={selectors.components.Menu.MenuItem('Test')} label="item2" icon="filter" active={true} />
</MenuGroup>
</Menu>
);
expect(screen.getByLabelText(selectors.components.Menu.MenuComponent('Test'))).toBeInTheDocument();
expect(screen.getByLabelText(selectors.components.Menu.MenuGroup('Test'))).toBeInTheDocument();
expect(screen.getAllByLabelText(selectors.components.Menu.MenuItem('Test'))).toHaveLength(2);
});
});

View File

@ -1,166 +1,35 @@
import React, { useCallback } from 'react';
import { css, cx } from 'emotion';
import { GrafanaTheme, LinkTarget } from '@grafana/data';
import { List } from '../List/List';
import { styleMixins, useStyles } from '../../themes';
import { Icon } from '../Icon/Icon';
import { IconName } from '../../types';
/** @internal */
export interface MenuItem {
/** Label of the menu item */
label: string;
/** Target of the menu item (i.e. new window) */
target?: LinkTarget;
/** Icon of the menu item */
icon?: IconName;
/** Url of the menu item */
url?: string;
/** Handler for the click behaviour */
onClick?: (event?: React.SyntheticEvent<HTMLElement>) => void;
/** Handler for the click behaviour */
group?: string;
/** Active */
active?: boolean;
}
/** @internal */
export interface MenuItemsGroup {
/** Label for the menu items group */
label?: string;
/** Items of the group */
items: MenuItem[];
}
import React from 'react';
import { css } from 'emotion';
import { GrafanaTheme } from '@grafana/data';
import { useStyles } from '../../themes';
/** @internal */
export interface MenuProps extends React.HTMLAttributes<HTMLDivElement> {
/** React element rendered at the top of the menu */
header?: React.ReactNode;
/** Array of menu items */
items?: MenuItemsGroup[];
/** Callback performed when menu is closed */
onClose?: () => void;
children: React.ReactNode;
ariaLabel?: string;
}
/** @internal */
export const Menu = React.forwardRef<HTMLDivElement, MenuProps>(({ header, items, onClose, ...otherProps }, ref) => {
const styles = useStyles(getMenuStyles);
const onClick = useCallback(() => {
if (onClose) {
onClose();
}
}, [onClose]);
return (
<div {...otherProps} ref={ref} className={styles.wrapper}>
{header && <div className={styles.header}>{header}</div>}
<List
items={items || []}
renderItem={(item) => {
return <MenuGroup group={item} onClick={onClick} />;
}}
/>
</div>
);
});
Menu.displayName = 'Menu';
interface MenuGroupProps {
group: MenuItemsGroup;
onClick?: () => void; // Used with 'onClose'
}
const MenuGroup: React.FC<MenuGroupProps> = ({ group, onClick }) => {
const styles = useStyles(getMenuStyles);
if (group.items.length === 0) {
return null;
}
return (
<div>
{group.label && <div className={styles.groupLabel}>{group.label}</div>}
<List
items={group.items || []}
renderItem={(item) => {
return (
<MenuItemComponent
url={item.url}
label={item.label}
target={item.target}
icon={item.icon}
active={item.active}
onClick={(e: React.MouseEvent<HTMLElement>) => {
// We can have both url and onClick and we want to allow user to open the link in new tab/window
const isSpecialKeyPressed = e.ctrlKey || e.metaKey || e.shiftKey;
if (isSpecialKeyPressed && item.url) {
return;
}
if (item.onClick) {
e.preventDefault();
item.onClick(e);
}
// Typically closes the context menu
if (onClick) {
onClick();
}
}}
/>
);
}}
/>
</div>
);
};
MenuGroup.displayName = 'MenuGroup';
interface MenuItemProps {
label: string;
icon?: IconName;
url?: string;
target?: LinkTarget;
onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;
className?: string;
active?: boolean;
}
const MenuItemComponent: React.FC<MenuItemProps> = React.memo(
({ url, icon, label, target, onClick, className, active }) => {
const styles = useStyles(getMenuStyles);
const itemStyle = cx(
{
[styles.item]: true,
[styles.activeItem]: active,
},
className
);
export const Menu = React.forwardRef<HTMLDivElement, MenuProps>(
({ header, children, ariaLabel, ...otherProps }, ref) => {
const styles = useStyles(getStyles);
return (
<div className={itemStyle}>
<a
href={url ? url : undefined}
target={target}
className={styles.link}
onClick={onClick}
rel={target === '_blank' ? 'noopener noreferrer' : undefined}
>
{icon && <Icon name={icon} className={styles.icon} />} {label}
</a>
<div {...otherProps} ref={ref} className={styles.wrapper} aria-label={ariaLabel}>
{header && <div className={styles.header}>{header}</div>}
{children}
</div>
);
}
);
MenuItemComponent.displayName = 'MenuItemComponent';
Menu.displayName = 'Menu';
const getMenuStyles = (theme: GrafanaTheme) => {
const linkColor = theme.colors.text;
const linkColorHover = theme.colors.linkHover;
/** @internal */
const getStyles = (theme: GrafanaTheme) => {
const wrapperBg = theme.colors.formInputBg;
const wrapperShadow = theme.isDark ? theme.palette.black : theme.palette.gray3;
const groupLabelColor = theme.colors.textWeak;
const itemBgHover = styleMixins.hoverColor(theme.colors.bg1, theme);
const headerBg = theme.colors.formInputBg;
const headerSeparator = theme.colors.border3;
@ -178,42 +47,5 @@ const getMenuStyles = (theme: GrafanaTheme) => {
display: inline-block;
border-radius: ${theme.border.radius.sm};
`,
link: css`
color: ${linkColor};
display: flex;
cursor: pointer;
padding: 5px 12px 5px 10px;
&:hover {
color: ${linkColorHover};
text-decoration: none;
}
`,
item: css`
background: none;
border-left: 2px solid transparent;
cursor: pointer;
white-space: nowrap;
&:hover {
background: ${itemBgHover};
border-image: linear-gradient(#f05a28 30%, #fbca0a 99%);
border-image-slice: 1;
}
`,
activeItem: css`
background: ${theme.colors.bg2};
`,
groupLabel: css`
color: ${groupLabelColor};
font-size: ${theme.typography.size.sm};
line-height: ${theme.typography.lineHeight.md};
padding: ${theme.spacing.xs} ${theme.spacing.sm};
`,
icon: css`
opacity: 0.7;
margin-right: 10px;
color: ${theme.colors.linkDisabled};
`,
};
};

View File

@ -0,0 +1,50 @@
import React from 'react';
import { css } from 'emotion';
import { GrafanaTheme } from '@grafana/data';
import { useStyles } from '../../themes';
import { MenuItemProps } from './MenuItem';
/** @internal */
export interface MenuItemsGroup {
/** Label for the menu items group */
label?: string;
/** Aria label for accessibility support */
ariaLabel?: string;
/** Items of the group */
items: MenuItemProps[];
}
/** @internal */
export interface MenuGroupProps extends Partial<MenuItemsGroup> {
/** special children prop to pass children elements */
children: React.ReactNode;
}
/** @internal */
export const MenuGroup: React.FC<MenuGroupProps> = ({ label, children, ariaLabel }) => {
const styles = useStyles(getStyles);
return (
<div>
{label && (
<div className={styles.groupLabel} aria-label={ariaLabel}>
{label}
</div>
)}
{children}
</div>
);
};
MenuGroup.displayName = 'MenuGroup';
/** @internal */
const getStyles = (theme: GrafanaTheme) => {
const groupLabelColor = theme.colors.textWeak;
return {
groupLabel: css`
color: ${groupLabelColor};
font-size: ${theme.typography.size.sm};
line-height: ${theme.typography.lineHeight.md};
padding: ${theme.spacing.xs} ${theme.spacing.sm};
`,
};
};

View File

@ -0,0 +1,94 @@
import React from 'react';
import { css, cx } from 'emotion';
import { GrafanaTheme, LinkTarget } from '@grafana/data';
import { styleMixins, useStyles } from '../../themes';
import { Icon } from '../Icon/Icon';
import { IconName } from '../../types';
/** @internal */
export interface MenuItemProps {
/** Label of the menu item */
label: string;
/** Aria label for accessibility support */
ariaLabel: string;
/** Target of the menu item (i.e. new window) */
target?: LinkTarget;
/** Icon of the menu item */
icon?: IconName;
/** Url of the menu item */
url?: string;
/** Handler for the click behaviour */
onClick?: (event?: React.SyntheticEvent<HTMLElement>) => void;
/** Custom MenuItem styles*/
className?: string;
/** Active */
active?: boolean;
}
/** @internal */
export const MenuItem: React.FC<MenuItemProps> = React.memo(
({ url, icon, label, ariaLabel, target, onClick, className, active }) => {
const styles = useStyles(getStyles);
const itemStyle = cx(
{
[styles.item]: true,
[styles.activeItem]: active,
},
className
);
return (
<div className={itemStyle} aria-label={ariaLabel}>
<a
href={url ? url : undefined}
target={target}
className={styles.link}
onClick={onClick}
rel={target === '_blank' ? 'noopener noreferrer' : undefined}
>
{icon && <Icon name={icon} className={styles.icon} />} {label}
</a>
</div>
);
}
);
MenuItem.displayName = 'MenuItem';
/** @internal */
const getStyles = (theme: GrafanaTheme) => {
const linkColor = theme.colors.text;
const linkColorHover = theme.colors.linkHover;
const itemBgHover = styleMixins.hoverColor(theme.colors.bg1, theme);
return {
link: css`
color: ${linkColor};
display: flex;
cursor: pointer;
padding: 5px 12px 5px 10px;
&:hover {
color: ${linkColorHover};
text-decoration: none;
}
`,
item: css`
background: none;
border-left: 2px solid transparent;
cursor: pointer;
white-space: nowrap;
&:hover {
background: ${itemBgHover};
border-image: linear-gradient(#f05a28 30%, #fbca0a 99%);
border-image-slice: 1;
}
`,
activeItem: css`
background: ${theme.colors.bg2};
`,
icon: css`
opacity: 0.7;
margin-right: 10px;
color: ${theme.colors.linkDisabled};
`,
};
};

View File

@ -34,7 +34,7 @@ export function useContextMenu(
MenuComponent = (
<ContextMenu
renderHeader={() => <NodeHeader node={openedNode.node} nodes={nodes} />}
items={items}
itemsGroup={items}
onClose={() => setOpenedNode(undefined)}
x={openedNode.event.pageX}
y={openedNode.event.pageY}
@ -49,7 +49,7 @@ export function useContextMenu(
MenuComponent = (
<ContextMenu
renderHeader={() => <EdgeHeader edge={openedEdge.edge} edges={edges} />}
items={items}
itemsGroup={items}
onClose={() => setOpenedEdge(undefined)}
x={openedEdge.event.pageX}
y={openedEdge.event.pageY}
@ -82,8 +82,10 @@ function getItems(links: LinkModel[]) {
return Object.keys(groups).map((key) => {
return {
label: key,
ariaLabel: key,
items: groups[key].map((link) => ({
label: link.newTitle || link.l.title,
ariaLabel: link.newTitle || link.l.title,
url: link.l.href,
onClick: link.l.onClick,
})),

View File

@ -102,7 +102,9 @@ export { ClickOutsideWrapper } from './ClickOutsideWrapper/ClickOutsideWrapper';
export * from './SingleStatShared/index';
export { CallToActionCard } from './CallToActionCard/CallToActionCard';
export { ContextMenu, ContextMenuProps } from './ContextMenu/ContextMenu';
export { Menu, MenuItem, MenuItemsGroup } from './Menu/Menu';
export { Menu, MenuProps } from './Menu/Menu';
export { MenuGroup, MenuItemsGroup, MenuGroupProps } from './Menu/MenuGroup';
export { MenuItem, MenuItemProps } from './Menu/MenuItem';
export { WithContextMenu } from './ContextMenu/WithContextMenu';
export { DataLinksInlineEditor } from './DataLinks/DataLinksInlineEditor/DataLinksInlineEditor';
export { DataLinkInput } from './DataLinks/DataLinkInput';

View File

@ -1,14 +1,15 @@
import { LinkModel } from '@grafana/data';
import { MenuItem } from '../components/Menu/Menu';
import { MenuItemProps } from '../components/Menu/MenuItem';
import { IconName } from '../types';
/**
* Delays creating links until we need to open the ContextMenu
*/
export const linkModelToContextMenuItems: (links: () => LinkModel[]) => MenuItem[] = (links) => {
export const linkModelToContextMenuItems: (links: () => LinkModel[]) => MenuItemProps[] = (links) => {
return links().map((link) => {
return {
label: link.title,
ariaLabel: link.title,
// TODO: rename to href
url: link.href,
target: link.target,

View File

@ -141,7 +141,7 @@ export function registerAngularDirectives() {
react2AngularDirective('graphContextMenu', GraphContextMenu, [
'x',
'y',
'items',
'itemsGroup',
['onClose', { watchDepth: 'reference', wrapApply: true }],
['getContextMenuSource', { watchDepth: 'reference', wrapApply: true }],
['timeZone', { watchDepth: 'reference', wrapApply: true }],

View File

@ -1,10 +1,10 @@
import { MenuItem } from '@grafana/ui';
import { MenuItemProps } from '@grafana/ui';
import { FlotDataPoint } from '@grafana/data';
export class GraphContextMenuCtrl {
private source?: FlotDataPoint | null;
private scope?: any;
menuItemsSupplier?: () => MenuItem[];
menuItemsSupplier?: () => MenuItemProps[];
scrollContextElement: HTMLElement | null;
position: {
x: number;
@ -61,7 +61,7 @@ export class GraphContextMenuCtrl {
return this.source;
};
setMenuItemsSupplier = (menuItemsSupplier: () => MenuItem[]) => {
setMenuItemsSupplier = (menuItemsSupplier: () => MenuItemProps[]) => {
this.menuItemsSupplier = menuItemsSupplier;
};
}

View File

@ -24,7 +24,7 @@ import ReactDOM from 'react-dom';
import { GraphLegendProps, Legend } from './Legend/Legend';
import { GraphCtrl } from './module';
import { graphTickFormatter, graphTimeFormat, IconName, MenuItem, MenuItemsGroup } from '@grafana/ui';
import { graphTickFormatter, graphTimeFormat, IconName, MenuItemProps, MenuItemsGroup } from '@grafana/ui';
import { provideTheme } from 'app/core/utils/ConfigProvider';
import {
DataFrame,
@ -207,6 +207,7 @@ class GraphElement {
items: [
{
label: 'Add annotation',
ariaLabel: 'Add annotation',
icon: 'comment-alt',
onClick: () => this.eventManager.updateTime({ from: flotPosition.x, to: null }),
},
@ -221,9 +222,10 @@ class GraphElement {
const dataLinks = [
{
items: linksSupplier.getLinks(this.panel.replaceVariables).map<MenuItem>((link) => {
items: linksSupplier.getLinks(this.panel.replaceVariables).map<MenuItemProps>((link) => {
return {
label: link.title,
ariaLabel: link.title,
url: link.href,
target: link.target,
icon: `${link.target === '_self' ? 'link' : 'external-link-alt'}` as IconName,

View File

@ -8,7 +8,7 @@ const template = `
</div>
<div ng-if="ctrl.contextMenuCtrl.isVisible">
<graph-context-menu
items="ctrl.contextMenuCtrl.menuItemsSupplier()"
itemsGroup="ctrl.contextMenuCtrl.menuItemsSupplier()"
onClose="ctrl.onContextMenuClose"
getContextMenuSource="ctrl.contextMenuCtrl.getSource"
timeZone="ctrl.getTimeZone()"

View File

@ -4,7 +4,7 @@ import {
ContextMenu,
GraphContextMenuHeader,
IconName,
MenuItem,
MenuItemProps,
MenuItemsGroup,
Portal,
useGraphNGContext,
@ -135,9 +135,10 @@ export const ContextMenuView: React.FC<ContextMenuProps> = ({
if (linksSupplier) {
items.push({
items: linksSupplier.getLinks(replaceVariables).map<MenuItem>((link) => {
items: linksSupplier.getLinks(replaceVariables).map<MenuItemProps>((link) => {
return {
label: link.title,
ariaLabel: link.title,
url: link.href,
target: link.target,
icon: `${link.target === '_self' ? 'link' : 'external-link-alt'}` as IconName,
@ -161,7 +162,7 @@ export const ContextMenuView: React.FC<ContextMenuProps> = ({
return (
<ContextMenu
items={items}
itemsGroup={items}
renderHeader={renderHeader}
x={selection.coords.viewport.x}
y={selection.coords.viewport.y}