import { css } from '@emotion/css'; import React, { MouseEvent, useCallback, useState } from 'react'; import { DataFrame, Field, GrafanaTheme, LinkModel } from '@grafana/data'; import { ContextMenu, MenuGroup, MenuItem, stylesFactory, useTheme } from '@grafana/ui'; import { Config } from './layout'; import { EdgeDatum, NodeDatum } from './types'; import { getEdgeFields, getNodeFields } from './utils'; /** * Hook that contains state of the context menu, both for edges and nodes and provides appropriate component when * opened context menu should be opened. */ export function useContextMenu( getLinks: (dataFrame: DataFrame, rowIndex: number) => LinkModel[], nodes: DataFrame, edges: DataFrame, config: Config, setConfig: (config: Config) => void, setFocusedNodeId: (id: string) => void ): { onEdgeOpen: (event: MouseEvent, edge: EdgeDatum) => void; onNodeOpen: (event: MouseEvent, node: NodeDatum) => void; MenuComponent: React.ReactNode; } { const [menu, setMenu] = useState(undefined); const onNodeOpen = useCallback( (event, node) => { const extraNodeItem = config.gridLayout ? [ { label: 'Show in Graph layout', onClick: (node: NodeDatum) => { setFocusedNodeId(node.id); setConfig({ ...config, gridLayout: false }); }, }, ] : undefined; const renderer = getItemsRenderer(getLinks(nodes, node.dataFrameRowIndex), node, extraNodeItem); if (renderer) { setMenu( } renderMenuItems={renderer} onClose={() => setMenu(undefined)} x={event.pageX} y={event.pageY} /> ); } }, [config, nodes, getLinks, setMenu, setConfig, setFocusedNodeId] ); const onEdgeOpen = useCallback( (event, edge) => { const renderer = getItemsRenderer(getLinks(edges, edge.dataFrameRowIndex), edge); if (renderer) { setMenu( } renderMenuItems={renderer} onClose={() => setMenu(undefined)} x={event.pageX} y={event.pageY} /> ); } }, [edges, getLinks, setMenu] ); return { onEdgeOpen, onNodeOpen, MenuComponent: menu }; } function getItemsRenderer( links: LinkModel[], item: T, extraItems?: Array> | undefined ) { if (!(links.length || extraItems?.length)) { return undefined; } const items = getItems(links); return () => { let groups = items?.map((group, index) => ( {(group.items || []).map(mapMenuItem(item))} )); if (extraItems) { groups = [...extraItems.map(mapMenuItem(item)), ...groups]; } return groups; }; } function mapMenuItem(item: T) { return function NodeGraphMenuItem(link: LinkData) { return ( link.onClick?.(item) : undefined} target={'_self'} /> ); }; } type LinkData = { label: string; ariaLabel?: string; url?: string; onClick?: (item: T) => void; }; function getItems(links: LinkModel[]) { const defaultGroup = 'Open in Explore'; const groups = links.reduce<{ [group: string]: Array<{ l: LinkModel; newTitle?: string }> }>((acc, l) => { let group; let title; if (l.title.indexOf('/') !== -1) { group = l.title.split('/')[0]; title = l.title.split('/')[1]; acc[group] = acc[group] || []; acc[group].push({ l, newTitle: title }); } else { acc[defaultGroup] = acc[defaultGroup] || []; acc[defaultGroup].push({ l }); } return acc; }, {}); 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, })), }; }); } function NodeHeader(props: { node: NodeDatum; nodes: DataFrame }) { const index = props.node.dataFrameRowIndex; const fields = getNodeFields(props.nodes); return (
{fields.title &&
); } function EdgeHeader(props: { edge: EdgeDatum; edges: DataFrame }) { const index = props.edge.dataFrameRowIndex; const fields = getEdgeFields(props.edges); return (
{fields.details.map((f) => (
); } export const getLabelStyles = stylesFactory((theme: GrafanaTheme) => { return { label: css` label: Label; line-height: 1.25; margin: ${theme.spacing.formLabelMargin}; padding: ${theme.spacing.formLabelPadding}; color: ${theme.colors.textFaint}; font-size: ${theme.typography.size.sm}; font-weight: ${theme.typography.weight.semibold}; `, value: css` label: Value; font-size: ${theme.typography.size.sm}; font-weight: ${theme.typography.weight.semibold}; color: ${theme.colors.formLabel}; margin-top: ${theme.spacing.xxs}; display: block; `, }; }); function Label(props: { field: Field; index: number }) { const { field, index } = props; const value = field.values.get(index) || ''; const styles = getLabelStyles(useTheme()); return (
{field.config.displayName || field.name}
{value}
); }