From 52c1d7301f5dc1fbcb592e398b7811978a3c4411 Mon Sep 17 00:00:00 2001 From: Uchechukwu Obasi Date: Thu, 18 Mar 2021 12:58:07 +0100 Subject: [PATCH] ContextMenu: changed menu item rendering to render prop pattern (#31993) * ContextMenu: changed menu item rendering to render prop pattern to enable manual composition of menu items * fixes affected components * fixes small nits * added some changes * used a more descriptive variable name --- .../ContextMenu/ContextMenu.story.tsx | 16 ++- .../components/ContextMenu/ContextMenu.tsx | 102 ++++++++---------- .../ContextMenu/WithContextMenu.tsx | 8 +- .../DataLinks/DataLinksContextMenu.tsx | 24 ++++- .../src/components/Graph/GraphContextMenu.tsx | 25 ++++- .../components/NodeGraph/useContextMenu.tsx | 36 ++++++- .../timeseries/plugins/ContextMenuPlugin.tsx | 26 ++++- 7 files changed, 162 insertions(+), 75 deletions(-) diff --git a/packages/grafana-ui/src/components/ContextMenu/ContextMenu.story.tsx b/packages/grafana-ui/src/components/ContextMenu/ContextMenu.story.tsx index 08714bb0dd3..773b78d2584 100644 --- a/packages/grafana-ui/src/components/ContextMenu/ContextMenu.story.tsx +++ b/packages/grafana-ui/src/components/ContextMenu/ContextMenu.story.tsx @@ -4,6 +4,8 @@ import { IconButton } from '../IconButton/IconButton'; import { ContextMenu } from './ContextMenu'; import { WithContextMenu } from './WithContextMenu'; import mdx from './ContextMenu.mdx'; +import { MenuGroup } from '../Menu/MenuGroup'; +import { MenuItem } from '../Menu/MenuItem'; export default { title: 'General/ContextMenu', @@ -26,13 +28,23 @@ const menuItems = [ }, ]; +const renderMenuItems = () => { + return menuItems?.map((group, index) => ( + + {(group.items || []).map((item) => ( + + ))} + + )); +}; + export const Basic = () => { - return {}} itemsGroup={menuItems} />; + return {}} renderMenuItems={renderMenuItems} />; }; export const WithState = () => { return ( - menuItems}> + {({ openMenu }) => } ); diff --git a/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx b/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx index cc106c22ad0..26b7019e857 100644 --- a/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx +++ b/packages/grafana-ui/src/components/ContextMenu/ContextMenu.tsx @@ -3,8 +3,6 @@ import { selectors } from '@grafana/e2e-selectors'; import { useClickAway } from 'react-use'; import { Portal } from '../Portal/Portal'; 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 */ @@ -13,69 +11,57 @@ export interface ContextMenuProps { y: number; /** Callback for closing the menu */ onClose?: () => void; - /** List of the menu items to display */ - itemsGroup?: MenuItemsGroup[]; + /** RenderProp function that returns menu items to display */ + renderMenuItems?: () => React.ReactNode; /** A function that returns header element */ renderHeader?: () => React.ReactNode; } -export const ContextMenu: React.FC = React.memo(({ x, y, onClose, itemsGroup, renderHeader }) => { - const menuRef = useRef(null); - const [positionStyles, setPositionStyles] = useState({}); +export const ContextMenu: React.FC = React.memo( + ({ x, y, onClose, renderMenuItems, renderHeader }) => { + const menuRef = useRef(null); + const [positionStyles, setPositionStyles] = useState({}); - useLayoutEffect(() => { - const menuElement = menuRef.current; - if (menuElement) { - const rect = menuElement.getBoundingClientRect(); - const OFFSET = 5; - const collisions = { - right: window.innerWidth < x + rect.width, - bottom: window.innerHeight < rect.bottom + rect.height + OFFSET, - }; + useLayoutEffect(() => { + const menuElement = menuRef.current; + if (menuElement) { + const rect = menuElement.getBoundingClientRect(); + const OFFSET = 5; + const collisions = { + right: window.innerWidth < x + rect.width, + bottom: window.innerHeight < rect.bottom + rect.height + OFFSET, + }; - setPositionStyles({ - position: 'fixed', - left: collisions.right ? x - rect.width - OFFSET : x - OFFSET, - top: collisions.bottom ? y - rect.height - OFFSET : y + OFFSET, - }); - } - }, [x, y]); + setPositionStyles({ + position: 'fixed', + left: collisions.right ? x - rect.width - OFFSET : x - OFFSET, + top: collisions.bottom ? y - rect.height - OFFSET : y + OFFSET, + }); + } + }, [x, y]); - useClickAway(menuRef, () => { - if (onClose) { - onClose(); - } - }); + useClickAway(menuRef, () => { + if (onClose) { + onClose(); + } + }); + const header = renderHeader && renderHeader(); + const menuItems = renderMenuItems && renderMenuItems(); - const header = renderHeader && renderHeader(); - return ( - - - {itemsGroup?.map((group, index) => ( - - {(group.items || []).map((item) => ( - - ))} - - ))} - - - ); -}); + return ( + + + {menuItems} + + + ); + } +); ContextMenu.displayName = 'ContextMenu'; diff --git a/packages/grafana-ui/src/components/ContextMenu/WithContextMenu.tsx b/packages/grafana-ui/src/components/ContextMenu/WithContextMenu.tsx index 51a14559805..f7c69446d0b 100644 --- a/packages/grafana-ui/src/components/ContextMenu/WithContextMenu.tsx +++ b/packages/grafana-ui/src/components/ContextMenu/WithContextMenu.tsx @@ -1,18 +1,16 @@ import React, { useState } from 'react'; import { ContextMenu } from '../ContextMenu/ContextMenu'; -import { MenuItemsGroup } from '../Menu/MenuGroup'; interface WithContextMenuProps { /** Menu item trigger that accepts openMenu prop */ children: (props: { openMenu: React.MouseEventHandler }) => JSX.Element; /** A function that returns an array of menu items */ - getContextMenuItems: () => MenuItemsGroup[]; + renderMenuItems: () => React.ReactNode; } -export const WithContextMenu: React.FC = ({ children, getContextMenuItems }) => { +export const WithContextMenu: React.FC = ({ children, renderMenuItems }) => { const [isMenuOpen, setIsMenuOpen] = useState(false); const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 }); - return ( <> {children({ @@ -30,7 +28,7 @@ export const WithContextMenu: React.FC = ({ children, getC onClose={() => setIsMenuOpen(false)} x={menuPosition.x} y={menuPosition.y} - itemsGroup={getContextMenuItems()} + renderMenuItems={renderMenuItems} /> )} diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx index 14a300e92e4..1effb787e7f 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx @@ -4,6 +4,8 @@ import { selectors } from '@grafana/e2e-selectors'; import { css } from 'emotion'; import { WithContextMenu } from '../ContextMenu/WithContextMenu'; import { linkModelToContextMenuItems } from '../../utils/dataLinks'; +import { MenuGroup, MenuItemsGroup } from '../Menu/MenuGroup'; +import { MenuItem } from '../Menu/MenuItem'; interface DataLinksContextMenuProps { children: (props: DataLinksContextMenuApi) => JSX.Element; @@ -18,8 +20,24 @@ export interface DataLinksContextMenuApi { export const DataLinksContextMenu: React.FC = ({ children, links, config }) => { const linksCounter = config.links!.length; - const getDataLinksContextMenuItems = () => { - return [{ items: linkModelToContextMenuItems(links), label: 'Data links' }]; + const itemsGroup: MenuItemsGroup[] = [{ items: linkModelToContextMenuItems(links), label: 'Data links' }]; + const renderMenuGroupItems = () => { + return itemsGroup.map((group, index) => ( + + {(group.items || []).map((item) => ( + + ))} + + )); }; // Use this class name (exposed via render prop) to add context menu indicator to the click target of the visualization @@ -29,7 +47,7 @@ export const DataLinksContextMenu: React.FC = ({ chil if (linksCounter > 1) { return ( - + {({ openMenu }) => { return children({ openMenu, targetClassName }); }} diff --git a/packages/grafana-ui/src/components/Graph/GraphContextMenu.tsx b/packages/grafana-ui/src/components/Graph/GraphContextMenu.tsx index 28aa8b385d6..bce86baa805 100644 --- a/packages/grafana-ui/src/components/Graph/GraphContextMenu.tsx +++ b/packages/grafana-ui/src/components/Graph/GraphContextMenu.tsx @@ -15,12 +15,15 @@ import { HorizontalGroup } from '../Layout/Layout'; import { FormattedValueDisplay } from '../FormattedValueDisplay/FormattedValueDisplay'; import { SeriesIcon } from '../VizLegend/SeriesIcon'; import { css } from 'emotion'; +import { MenuGroup, MenuGroupProps } from '../Menu/MenuGroup'; +import { MenuItem } from '../Menu/MenuItem'; export type ContextDimensions = { [key in keyof T]: [number, number | undefined] | null }; export type GraphContextMenuProps = ContextMenuProps & { getContextMenuSource: () => FlotDataPoint | null; timeZone?: TimeZone; + itemsGroup?: MenuGroupProps[]; dimensions?: GraphDimensions; contextDimensions?: ContextDimensions; }; @@ -40,7 +43,7 @@ export const GraphContextMenu: React.FC = ({ const itemsToRender = itemsGroup ? itemsGroup.map((group) => ({ ...group, - items: group.items.filter((item) => item.label), + items: group.items?.filter((item) => item.label), })) : []; @@ -80,8 +83,26 @@ export const GraphContextMenu: React.FC = ({ /> ); }; + const renderMenuGroupItems = () => { + return itemsToRender?.map((group, index) => ( + + {(group.items || []).map((item) => ( + + ))} + + )); + }; - return ; + return ; }; /** @internal */ diff --git a/packages/grafana-ui/src/components/NodeGraph/useContextMenu.tsx b/packages/grafana-ui/src/components/NodeGraph/useContextMenu.tsx index 6303376c8e2..5bb5f6666ae 100644 --- a/packages/grafana-ui/src/components/NodeGraph/useContextMenu.tsx +++ b/packages/grafana-ui/src/components/NodeGraph/useContextMenu.tsx @@ -6,6 +6,8 @@ import { useTheme } from '../../themes/ThemeContext'; import { stylesFactory } from '../../themes/stylesFactory'; import { getEdgeFields, getNodeFields } from './utils'; import { css } from 'emotion'; +import { MenuGroup } from '../Menu/MenuGroup'; +import { MenuItem } from '../Menu/MenuItem'; /** * Hook that contains state of the context menu, both for edges and nodes and provides appropriate component when @@ -30,11 +32,26 @@ export function useContextMenu( if (openedNode) { const items = getItems(getLinks(nodes, openedNode.node.dataFrameRowIndex)); + const renderMenuGroupItems = () => { + return items?.map((group, index) => ( + + {(group.items || []).map((item) => ( + + ))} + + )); + }; if (items.length) { MenuComponent = ( } - itemsGroup={items} + renderMenuItems={renderMenuGroupItems} onClose={() => setOpenedNode(undefined)} x={openedNode.event.pageX} y={openedNode.event.pageY} @@ -45,11 +62,26 @@ export function useContextMenu( if (openedEdge) { const items = getItems(getLinks(edges, openedEdge.edge.dataFrameRowIndex)); + const renderMenuGroupItems = () => { + return items?.map((group, index) => ( + + {(group.items || []).map((item) => ( + + ))} + + )); + }; if (items.length) { MenuComponent = ( } - itemsGroup={items} + renderMenuItems={renderMenuGroupItems} onClose={() => setOpenedEdge(undefined)} x={openedEdge.event.pageX} y={openedEdge.event.pageY} diff --git a/public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx b/public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx index 8b120a24ca6..f276d2c356f 100644 --- a/public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx +++ b/public/app/plugins/panel/timeseries/plugins/ContextMenuPlugin.tsx @@ -6,6 +6,8 @@ import { IconName, MenuItemProps, MenuItemsGroup, + MenuGroup, + MenuItem, Portal, useGraphNGContext, } from '@grafana/ui'; @@ -31,9 +33,9 @@ interface ContextMenuPluginProps { export const ContextMenuPlugin: React.FC = ({ data, + defaultItems, onClose, timeZone, - defaultItems, replaceVariables, }) => { const [isOpen, setIsOpen] = useState(false); @@ -105,7 +107,6 @@ export const ContextMenuView: React.FC = ({ if (!xField) { return null; } - const items = defaultItems ? [...defaultItems] : []; let renderHeader: () => JSX.Element | null = () => null; @@ -160,9 +161,28 @@ export const ContextMenuView: React.FC = ({ ); } + const renderMenuGroupItems = () => { + return items?.map((group, index) => ( + + {(group.items || []).map((item) => ( + + ))} + + )); + }; + return (