|
|
|
@ -1,10 +1,10 @@ |
|
|
|
|
import { css } from '@emotion/css'; |
|
|
|
|
import cx from 'classnames'; |
|
|
|
|
import { memo, MouseEvent, useCallback, useEffect, useMemo, useState } from 'react'; |
|
|
|
|
import { memo, MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'; |
|
|
|
|
import useMeasure from 'react-use/lib/useMeasure'; |
|
|
|
|
|
|
|
|
|
import { DataFrame, GrafanaTheme2, LinkModel } from '@grafana/data'; |
|
|
|
|
import { Icon, Spinner, useStyles2 } from '@grafana/ui'; |
|
|
|
|
import { Icon, RadioButtonGroup, Spinner, useStyles2 } from '@grafana/ui'; |
|
|
|
|
|
|
|
|
|
import { Edge } from './Edge'; |
|
|
|
|
import { EdgeLabel } from './EdgeLabel'; |
|
|
|
@ -12,7 +12,8 @@ import { Legend } from './Legend'; |
|
|
|
|
import { Marker } from './Marker'; |
|
|
|
|
import { Node } from './Node'; |
|
|
|
|
import { ViewControls } from './ViewControls'; |
|
|
|
|
import { Config, defaultConfig, useLayout } from './layout'; |
|
|
|
|
import { Config, defaultConfig, useLayout, LayoutCache } from './layout'; |
|
|
|
|
import { LayoutAlgorithm } from './panelcfg.gen'; |
|
|
|
|
import { EdgeDatumLayout, NodeDatum, NodesMarker, ZoomMode } from './types'; |
|
|
|
|
import { useCategorizeFrames } from './useCategorizeFrames'; |
|
|
|
|
import { useContextMenu } from './useContextMenu'; |
|
|
|
@ -70,6 +71,14 @@ const getStyles = (theme: GrafanaTheme2) => ({ |
|
|
|
|
justifyContent: 'space-between', |
|
|
|
|
pointerEvents: 'none', |
|
|
|
|
}), |
|
|
|
|
layoutAlgorithm: css({ |
|
|
|
|
label: 'layoutAlgorithm', |
|
|
|
|
pointerEvents: 'all', |
|
|
|
|
position: 'absolute', |
|
|
|
|
top: '8px', |
|
|
|
|
right: '8px', |
|
|
|
|
zIndex: 1, |
|
|
|
|
}), |
|
|
|
|
legend: css({ |
|
|
|
|
label: 'legend', |
|
|
|
|
background: theme.colors.background.secondary, |
|
|
|
@ -88,7 +97,6 @@ const getStyles = (theme: GrafanaTheme2) => ({ |
|
|
|
|
borderRadius: theme.shape.radius.default, |
|
|
|
|
alignItems: 'center', |
|
|
|
|
position: 'absolute', |
|
|
|
|
top: 0, |
|
|
|
|
right: 0, |
|
|
|
|
background: theme.colors.warning.main, |
|
|
|
|
color: theme.colors.warning.contrastText, |
|
|
|
@ -107,20 +115,39 @@ const getStyles = (theme: GrafanaTheme2) => ({ |
|
|
|
|
// interactions will be without any lag for most users.
|
|
|
|
|
const defaultNodeCountLimit = 200; |
|
|
|
|
|
|
|
|
|
export const layeredLayoutThreshold = 500; |
|
|
|
|
|
|
|
|
|
interface Props { |
|
|
|
|
dataFrames: DataFrame[]; |
|
|
|
|
getLinks: (dataFrame: DataFrame, rowIndex: number) => LinkModel[]; |
|
|
|
|
nodeLimit?: number; |
|
|
|
|
panelId?: string; |
|
|
|
|
zoomMode?: ZoomMode; |
|
|
|
|
layoutAlgorithm?: LayoutAlgorithm; |
|
|
|
|
} |
|
|
|
|
export function NodeGraph({ getLinks, dataFrames, nodeLimit, panelId, zoomMode }: Props) { |
|
|
|
|
export function NodeGraph({ getLinks, dataFrames, nodeLimit, panelId, zoomMode, layoutAlgorithm }: Props) { |
|
|
|
|
const nodeCountLimit = nodeLimit || defaultNodeCountLimit; |
|
|
|
|
const { edges: edgesDataFrames, nodes: nodesDataFrames } = useCategorizeFrames(dataFrames); |
|
|
|
|
|
|
|
|
|
const [measureRef, { width, height }] = useMeasure(); |
|
|
|
|
const [config, setConfig] = useState<Config>(defaultConfig); |
|
|
|
|
|
|
|
|
|
// Layout cache to avoid recalculating layouts
|
|
|
|
|
const layoutCacheRef = useRef<LayoutCache>({}); |
|
|
|
|
|
|
|
|
|
// Update the config when layoutAlgorithm changes via the panel options
|
|
|
|
|
useEffect(() => { |
|
|
|
|
if (layoutAlgorithm) { |
|
|
|
|
setConfig((prevConfig) => { |
|
|
|
|
return { |
|
|
|
|
...prevConfig, |
|
|
|
|
gridLayout: layoutAlgorithm === LayoutAlgorithm.Grid, |
|
|
|
|
layoutAlgorithm, |
|
|
|
|
}; |
|
|
|
|
}); |
|
|
|
|
} |
|
|
|
|
}, [layoutAlgorithm]); |
|
|
|
|
|
|
|
|
|
const firstNodesDataFrame = nodesDataFrames[0]; |
|
|
|
|
const firstEdgesDataFrame = edgesDataFrames[0]; |
|
|
|
|
|
|
|
|
@ -166,7 +193,8 @@ export function NodeGraph({ getLinks, dataFrames, nodeLimit, panelId, zoomMode } |
|
|
|
|
nodeCountLimit, |
|
|
|
|
width, |
|
|
|
|
focusedNodeId, |
|
|
|
|
processed.hasFixedPositions |
|
|
|
|
processed.hasFixedPositions, |
|
|
|
|
layoutCacheRef.current |
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
// If we move from grid to graph layout, and we have focused node lets get its position to center there. We want to
|
|
|
|
@ -199,6 +227,18 @@ export function NodeGraph({ getLinks, dataFrames, nodeLimit, panelId, zoomMode } |
|
|
|
|
|
|
|
|
|
const highlightId = useHighlight(focusedNodeId); |
|
|
|
|
|
|
|
|
|
const handleLayoutChange = (cfg: Config) => { |
|
|
|
|
if (cfg.layoutAlgorithm !== config.layoutAlgorithm) { |
|
|
|
|
setFocusedNodeId(undefined); |
|
|
|
|
} |
|
|
|
|
setConfig(cfg); |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
// Clear the layout cache when data changes
|
|
|
|
|
useEffect(() => { |
|
|
|
|
layoutCacheRef.current = {}; |
|
|
|
|
}, [firstNodesDataFrame, firstEdgesDataFrame]); |
|
|
|
|
|
|
|
|
|
return ( |
|
|
|
|
<div ref={topLevelRef} className={styles.wrapper}> |
|
|
|
|
{loading ? ( |
|
|
|
@ -208,6 +248,27 @@ export function NodeGraph({ getLinks, dataFrames, nodeLimit, panelId, zoomMode } |
|
|
|
|
</div> |
|
|
|
|
) : null} |
|
|
|
|
|
|
|
|
|
{!panelId && ( |
|
|
|
|
<div className={styles.layoutAlgorithm}> |
|
|
|
|
<RadioButtonGroup |
|
|
|
|
size="sm" |
|
|
|
|
options={[ |
|
|
|
|
{ label: 'Layered', value: LayoutAlgorithm.Layered }, |
|
|
|
|
{ label: 'Force', value: LayoutAlgorithm.Force }, |
|
|
|
|
{ label: 'Grid', value: LayoutAlgorithm.Grid }, |
|
|
|
|
]} |
|
|
|
|
value={config.gridLayout ? LayoutAlgorithm.Grid : config.layoutAlgorithm} |
|
|
|
|
onChange={(value) => { |
|
|
|
|
handleLayoutChange({ |
|
|
|
|
...config, |
|
|
|
|
gridLayout: value === LayoutAlgorithm.Grid, |
|
|
|
|
layoutAlgorithm: value, |
|
|
|
|
}); |
|
|
|
|
}} |
|
|
|
|
/> |
|
|
|
|
</div> |
|
|
|
|
)} |
|
|
|
|
|
|
|
|
|
{dataFrames.length && processed.nodes.length ? ( |
|
|
|
|
<svg |
|
|
|
|
ref={panRef} |
|
|
|
@ -267,12 +328,7 @@ export function NodeGraph({ getLinks, dataFrames, nodeLimit, panelId, zoomMode } |
|
|
|
|
<div className={styles.viewControlsWrapper}> |
|
|
|
|
<ViewControls<Config> |
|
|
|
|
config={config} |
|
|
|
|
onConfigChange={(cfg) => { |
|
|
|
|
if (cfg.gridLayout !== config.gridLayout) { |
|
|
|
|
setFocusedNodeId(undefined); |
|
|
|
|
} |
|
|
|
|
setConfig(cfg); |
|
|
|
|
}} |
|
|
|
|
onConfigChange={handleLayoutChange} |
|
|
|
|
onMinus={onStepDown} |
|
|
|
|
onPlus={onStepUp} |
|
|
|
|
scale={scale} |
|
|
|
@ -283,11 +339,26 @@ export function NodeGraph({ getLinks, dataFrames, nodeLimit, panelId, zoomMode } |
|
|
|
|
</div> |
|
|
|
|
|
|
|
|
|
{hiddenNodesCount > 0 && ( |
|
|
|
|
<div className={styles.alert} aria-label={'Nodes hidden warning'}> |
|
|
|
|
<div |
|
|
|
|
className={styles.alert} |
|
|
|
|
style={{ top: panelId ? '0px' : '40px' }} // panelId is undefined in Explore
|
|
|
|
|
aria-label={'Nodes hidden warning'} |
|
|
|
|
> |
|
|
|
|
<Icon size="sm" name={'info-circle'} /> {hiddenNodesCount} nodes are hidden for performance reasons. |
|
|
|
|
</div> |
|
|
|
|
)} |
|
|
|
|
|
|
|
|
|
{config.layoutAlgorithm === LayoutAlgorithm.Layered && processed.nodes.length > layeredLayoutThreshold && ( |
|
|
|
|
<div |
|
|
|
|
className={styles.alert} |
|
|
|
|
style={{ top: panelId ? '30px' : '70px' }} |
|
|
|
|
aria-label={'Layered layout performance warning'} |
|
|
|
|
> |
|
|
|
|
<Icon size="sm" name={'exclamation-triangle'} /> Layered layout may be slow with {processed.nodes.length}{' '} |
|
|
|
|
nodes. |
|
|
|
|
</div> |
|
|
|
|
)} |
|
|
|
|
|
|
|
|
|
{MenuComponent} |
|
|
|
|
</div> |
|
|
|
|
); |
|
|
|
|