mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Geomap: Add network layer (#70192)
* Geomap: Add network layer * Support text labels for nodes * Add solid styling for edges * Remove symbol option for edge style menu * Add support for edge text labels * Fix linter issues * Simplify multiple data frame handling * Add TODO notes * Add node and edge style categories for options * Remove data frame hardcoding * Hide legend, attempt to hide tooltip by default * Mark network layer as beta * refactor updateEdge * Fix some linter issues * Remove attempt at disabling tooltip for network layer * For edge text add a stroke and increase z index * Restrict field selection based on frame type * refactor * add basic bad data handling (prevent entire panel from breaking) * generate non hard coded graph frames for style editor filtering * code cleanup; remove hardcoded reference to "edges" frame * fix select clearing for Data option * fix styling * fix lookup --------- Co-authored-by: nmarrs <nathanielmarrs@gmail.com> Co-authored-by: Adela Almasan <adela.almasan@grafana.com>
This commit is contained in:
parent
982624cf51
commit
59bed9e156
@ -208,7 +208,9 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "7"]
|
||||
],
|
||||
"packages/grafana-data/src/geo/layer.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
|
||||
],
|
||||
"packages/grafana-data/src/panel/PanelPlugin.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
|
@ -5,6 +5,7 @@ import { ReactNode } from 'react';
|
||||
import { MapLayerOptions, FrameGeometrySourceMode } from '@grafana/schema';
|
||||
|
||||
import { EventBus } from '../events';
|
||||
import { StandardEditorContext } from '../field/standardFieldConfigEditorRegistry';
|
||||
import { GrafanaTheme2 } from '../themes';
|
||||
import { PanelData } from '../types';
|
||||
import { PanelOptionsEditorBuilder } from '../utils';
|
||||
@ -39,7 +40,10 @@ export interface MapLayerHandler<TConfig = any> {
|
||||
/**
|
||||
* Show custom elements in the panel edit UI
|
||||
*/
|
||||
registerOptionsUI?: (builder: PanelOptionsEditorBuilder<MapLayerOptions<TConfig>>) => void;
|
||||
registerOptionsUI?: (
|
||||
builder: PanelOptionsEditorBuilder<MapLayerOptions<TConfig>>,
|
||||
context: StandardEditorContext<any, any>
|
||||
) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -56,7 +56,7 @@ export function RefIDPicker({ value, data, onChange, placeholder }: Props) {
|
||||
|
||||
const onFilterChange = useCallback(
|
||||
(v: SelectableValue<string>) => {
|
||||
onChange(v.value!);
|
||||
onChange(v?.value!);
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
@ -9,7 +9,7 @@ import { getGeometryField, LocationFieldMatchers } from './location';
|
||||
export interface FrameVectorSourceOptions {}
|
||||
|
||||
export class FrameVectorSource<T extends Geometry = Geometry> extends VectorSource<T> {
|
||||
constructor(private location: LocationFieldMatchers) {
|
||||
constructor(public location: LocationFieldMatchers) {
|
||||
super({});
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { capitalize } from 'lodash';
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { useObservable } from 'react-use';
|
||||
import { Observable, of } from 'rxjs';
|
||||
|
||||
import { FieldConfigPropertyItem, StandardEditorProps, StandardEditorsRegistryItem } from '@grafana/data';
|
||||
import { FieldConfigPropertyItem, StandardEditorProps, StandardEditorsRegistryItem, FrameMatcher } from '@grafana/data';
|
||||
import {
|
||||
ScaleDimensionConfig,
|
||||
ResourceDimensionConfig,
|
||||
@ -39,11 +39,22 @@ export interface StyleEditorOptions {
|
||||
layerInfo?: Observable<LayerContentInfo>;
|
||||
simpleFixedValues?: boolean;
|
||||
displayRotation?: boolean;
|
||||
hideSymbol?: boolean;
|
||||
frameMatcher?: FrameMatcher;
|
||||
}
|
||||
|
||||
type Props = StandardEditorProps<StyleConfig, StyleEditorOptions>;
|
||||
|
||||
export const StyleEditor = ({ value, context, onChange, item }: Props) => {
|
||||
export const StyleEditor = (props: Props) => {
|
||||
const { value, onChange, item } = props;
|
||||
const context = useMemo(() => {
|
||||
if (!item.settings?.frameMatcher) {
|
||||
return props.context;
|
||||
}
|
||||
|
||||
return { ...props.context, data: props.context.data.filter(item.settings.frameMatcher) };
|
||||
}, [props.context, item.settings]);
|
||||
|
||||
const settings = item.settings;
|
||||
|
||||
const onSizeChange = (sizeValue: ScaleDimensionConfig | undefined) => {
|
||||
@ -188,24 +199,26 @@ export const StyleEditor = ({ value, context, onChange, item }: Props) => {
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
<Field label={'Symbol'}>
|
||||
<ResourceDimensionEditor
|
||||
value={value?.symbol ?? defaultStyleConfig.symbol}
|
||||
context={context}
|
||||
onChange={onSymbolChange}
|
||||
item={
|
||||
{
|
||||
settings: {
|
||||
resourceType: MediaType.Icon,
|
||||
folderName: ResourceFolderName.Marker,
|
||||
placeholderText: hasTextLabel ? 'Select a symbol' : 'Select a symbol or add a text label',
|
||||
placeholderValue: defaultStyleConfig.symbol.fixed,
|
||||
showSourceRadio: false,
|
||||
},
|
||||
} as StandardEditorsRegistryItem
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
{!settings?.hideSymbol && (
|
||||
<Field label={'Symbol'}>
|
||||
<ResourceDimensionEditor
|
||||
value={value?.symbol ?? defaultStyleConfig.symbol}
|
||||
context={context}
|
||||
onChange={onSymbolChange}
|
||||
item={
|
||||
{
|
||||
settings: {
|
||||
resourceType: MediaType.Icon,
|
||||
folderName: ResourceFolderName.Marker,
|
||||
placeholderText: hasTextLabel ? 'Select a symbol' : 'Select a symbol or add a text label',
|
||||
placeholderValue: defaultStyleConfig.symbol.fixed,
|
||||
showSourceRadio: false,
|
||||
},
|
||||
} as StandardEditorsRegistryItem
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
)}
|
||||
<Field label={'Color'}>
|
||||
<ColorDimensionEditor
|
||||
value={value?.color ?? defaultStyleConfig.color}
|
||||
|
@ -106,7 +106,7 @@ export function getLayerEditor(opts: LayerEditorOptions): NestedPanelOptions<Map
|
||||
addLocationFields('Location', 'location.', builder, options.location, data);
|
||||
}
|
||||
if (handler.registerOptionsUI) {
|
||||
handler.registerOptionsUI(builder);
|
||||
handler.registerOptionsUI(builder, context);
|
||||
}
|
||||
if (!isEqual(opts.category, ['Base layer'])) {
|
||||
if (!layer.hideOpacity) {
|
||||
|
@ -4,6 +4,7 @@ import { geojsonLayer } from './geojsonLayer';
|
||||
import { heatmapLayer } from './heatMap';
|
||||
import { lastPointTracker } from './lastPointTracker';
|
||||
import { markersLayer } from './markersLayer';
|
||||
import { networkLayer } from './networkLayer';
|
||||
import { photosLayer } from './photosLayer';
|
||||
import { routeLayer } from './routeLayer';
|
||||
|
||||
@ -11,12 +12,13 @@ import { routeLayer } from './routeLayer';
|
||||
* Registry for layer handlers
|
||||
*/
|
||||
export const dataLayers = [
|
||||
markersLayer,
|
||||
heatmapLayer,
|
||||
lastPointTracker,
|
||||
geojsonLayer,
|
||||
dynamicGeoJSONLayer,
|
||||
dayNightLayer,
|
||||
routeLayer,
|
||||
photosLayer
|
||||
markersLayer,
|
||||
heatmapLayer,
|
||||
lastPointTracker,
|
||||
geojsonLayer,
|
||||
dynamicGeoJSONLayer,
|
||||
dayNightLayer,
|
||||
routeLayer,
|
||||
photosLayer,
|
||||
networkLayer,
|
||||
];
|
||||
|
364
public/app/plugins/panel/geomap/layers/data/networkLayer.tsx
Normal file
364
public/app/plugins/panel/geomap/layers/data/networkLayer.tsx
Normal file
@ -0,0 +1,364 @@
|
||||
import { isNumber } from 'lodash';
|
||||
import { Feature } from 'ol';
|
||||
import { FeatureLike } from 'ol/Feature';
|
||||
import Map from 'ol/Map';
|
||||
import { Geometry, LineString, Point, SimpleGeometry } from 'ol/geom';
|
||||
import VectorLayer from 'ol/layer/Vector';
|
||||
import { Fill, Stroke, Style, Text } from 'ol/style';
|
||||
import FlowLine from 'ol-ext/style/FlowLine';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { ReplaySubject } from 'rxjs';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
import {
|
||||
MapLayerRegistryItem,
|
||||
MapLayerOptions,
|
||||
PanelData,
|
||||
GrafanaTheme2,
|
||||
FrameGeometrySourceMode,
|
||||
EventBus,
|
||||
DataFrame,
|
||||
Field,
|
||||
PluginState,
|
||||
} from '@grafana/data';
|
||||
import { FrameVectorSource } from 'app/features/geo/utils/frameVectorSource';
|
||||
import { getGeometryField, getLocationMatchers } from 'app/features/geo/utils/location';
|
||||
import { GraphFrame } from 'app/plugins/panel/nodeGraph/types';
|
||||
import { getGraphFrame } from 'app/plugins/panel/nodeGraph/utils';
|
||||
|
||||
import { MarkersLegendProps, MarkersLegend } from '../../components/MarkersLegend';
|
||||
import { ObservablePropsWrapper } from '../../components/ObservablePropsWrapper';
|
||||
import { StyleEditor } from '../../editor/StyleEditor';
|
||||
import { StyleConfig, defaultStyleConfig } from '../../style/types';
|
||||
import { getStyleConfigState } from '../../style/utils';
|
||||
import { getStyleDimension } from '../../utils/utils';
|
||||
|
||||
export interface NetworkConfig {
|
||||
style: StyleConfig;
|
||||
showLegend?: boolean;
|
||||
edgeStyle: StyleConfig;
|
||||
arrow?: 0 | 1 | -1 | 2;
|
||||
}
|
||||
|
||||
const defaultOptions: NetworkConfig = {
|
||||
style: defaultStyleConfig,
|
||||
showLegend: false,
|
||||
edgeStyle: defaultStyleConfig,
|
||||
arrow: 0,
|
||||
};
|
||||
|
||||
export const NETWORK_LAYER_ID = 'network';
|
||||
|
||||
// Used by default when nothing is configured
|
||||
export const defaultMarkersConfig: MapLayerOptions<NetworkConfig> = {
|
||||
type: NETWORK_LAYER_ID,
|
||||
name: '', // will get replaced
|
||||
config: defaultOptions,
|
||||
location: {
|
||||
mode: FrameGeometrySourceMode.Auto,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Map layer configuration for network overlay
|
||||
*/
|
||||
export const networkLayer: MapLayerRegistryItem<NetworkConfig> = {
|
||||
id: NETWORK_LAYER_ID,
|
||||
name: 'Network',
|
||||
description: 'Render a node graph as a map layer',
|
||||
isBaseMap: false,
|
||||
showLocation: true,
|
||||
hideOpacity: true,
|
||||
state: PluginState.beta,
|
||||
|
||||
/**
|
||||
* Function that configures transformation and returns a transformer
|
||||
* @param map
|
||||
* @param options
|
||||
* @param eventBus
|
||||
* @param theme
|
||||
*/
|
||||
create: async (map: Map, options: MapLayerOptions<NetworkConfig>, eventBus: EventBus, theme: GrafanaTheme2) => {
|
||||
// Assert default values
|
||||
const config = {
|
||||
...defaultOptions,
|
||||
...options?.config,
|
||||
};
|
||||
|
||||
const style = await getStyleConfigState(config.style);
|
||||
const edgeStyle = await getStyleConfigState(config.edgeStyle);
|
||||
const location = await getLocationMatchers(options.location);
|
||||
const source = new FrameVectorSource(location);
|
||||
|
||||
const vectorLayer = new VectorLayer({
|
||||
source,
|
||||
});
|
||||
const hasArrows = config.arrow === 1 || config.arrow === -1 || config.arrow === 2;
|
||||
|
||||
// TODO update legend to display edges as well
|
||||
const legendProps = new ReplaySubject<MarkersLegendProps>(1);
|
||||
let legend: ReactNode = null;
|
||||
if (config.showLegend) {
|
||||
legend = <ObservablePropsWrapper watch={legendProps} initialSubProps={{}} child={MarkersLegend} />;
|
||||
}
|
||||
|
||||
vectorLayer.setStyle((feature: FeatureLike) => {
|
||||
const geom = feature.getGeometry();
|
||||
const idx = feature.get('rowIndex');
|
||||
const dims = style.dims;
|
||||
|
||||
if (!style.fields && !edgeStyle.fields && !hasArrows && geom?.getType() !== 'LineString') {
|
||||
// Set a global style
|
||||
return style.maker(style.base);
|
||||
}
|
||||
|
||||
// For edges
|
||||
if (geom?.getType() === 'LineString' && geom instanceof SimpleGeometry) {
|
||||
const edgeDims = edgeStyle.dims;
|
||||
const edgeTextConfig = edgeStyle.config.textConfig;
|
||||
const edgeId = Number(feature.getId());
|
||||
const coordinates = geom.getCoordinates();
|
||||
const opacity = edgeStyle.config.opacity ?? 1;
|
||||
if (coordinates && edgeDims) {
|
||||
const segmentStartCoords = coordinates[0];
|
||||
const segmentEndCoords = coordinates[1];
|
||||
const color1 = tinycolor(
|
||||
theme.visualization.getColorByName((edgeDims.color && edgeDims.color.get(edgeId)) ?? edgeStyle.base.color)
|
||||
)
|
||||
.setAlpha(opacity)
|
||||
.toString();
|
||||
const color2 = tinycolor(
|
||||
theme.visualization.getColorByName((edgeDims.color && edgeDims.color.get(edgeId)) ?? edgeStyle.base.color)
|
||||
)
|
||||
.setAlpha(opacity)
|
||||
.toString();
|
||||
const arrowSize1 = (edgeDims.size && edgeDims.size.get(edgeId)) ?? edgeStyle.base.size;
|
||||
const arrowSize2 = (edgeDims.size && edgeDims.size.get(edgeId)) ?? edgeStyle.base.size;
|
||||
const styles = [];
|
||||
|
||||
const flowStyle = new FlowLine({
|
||||
visible: true,
|
||||
lineCap: config.arrow === 0 ? 'round' : 'square',
|
||||
color: color1,
|
||||
color2: color2,
|
||||
width: (edgeDims.size && edgeDims.size.get(edgeId)) ?? edgeStyle.base.size,
|
||||
width2: (edgeDims.size && edgeDims.size.get(edgeId)) ?? edgeStyle.base.size,
|
||||
});
|
||||
|
||||
if (config.arrow) {
|
||||
flowStyle.setArrow(config.arrow);
|
||||
if (config.arrow > 0) {
|
||||
flowStyle.setArrowColor(color2);
|
||||
flowStyle.setArrowSize((arrowSize2 ?? 0) * 2);
|
||||
} else {
|
||||
flowStyle.setArrowColor(color1);
|
||||
flowStyle.setArrowSize((arrowSize1 ?? 0) * 2);
|
||||
}
|
||||
}
|
||||
const LS = new LineString([segmentStartCoords, segmentEndCoords]);
|
||||
flowStyle.setGeometry(LS);
|
||||
|
||||
const fontFamily = theme.typography.fontFamily;
|
||||
if (edgeDims.text) {
|
||||
const labelStyle = new Style({
|
||||
zIndex: 10,
|
||||
text: new Text({
|
||||
text: edgeDims.text.get(edgeId),
|
||||
font: `normal ${edgeTextConfig?.fontSize}px ${fontFamily}`,
|
||||
fill: new Fill({ color: color1 ?? defaultStyleConfig.color.fixed }),
|
||||
stroke: new Stroke({
|
||||
color: tinycolor(theme.visualization.getColorByName('text')).setAlpha(opacity).toString(),
|
||||
width: Math.max(edgeTextConfig?.fontSize! / 10, 1),
|
||||
}),
|
||||
...edgeTextConfig,
|
||||
}),
|
||||
});
|
||||
labelStyle.setGeometry(LS);
|
||||
styles.push(labelStyle);
|
||||
}
|
||||
styles.push(flowStyle);
|
||||
return styles;
|
||||
}
|
||||
}
|
||||
if (!dims || !isNumber(idx)) {
|
||||
return style.maker(style.base);
|
||||
}
|
||||
|
||||
const values = { ...style.base };
|
||||
|
||||
if (dims.color) {
|
||||
values.color = dims.color.get(idx);
|
||||
}
|
||||
if (dims.size) {
|
||||
values.size = dims.size.get(idx);
|
||||
}
|
||||
if (dims.text) {
|
||||
values.text = dims.text.get(idx);
|
||||
}
|
||||
if (dims.rotation) {
|
||||
values.rotation = dims.rotation.get(idx);
|
||||
}
|
||||
return style.maker(values);
|
||||
});
|
||||
|
||||
return {
|
||||
init: () => vectorLayer,
|
||||
legend: legend,
|
||||
update: (data: PanelData) => {
|
||||
if (!data.series?.length) {
|
||||
source.clear();
|
||||
return; // ignore empty
|
||||
}
|
||||
|
||||
// Post updates to the legend component
|
||||
if (legend) {
|
||||
legendProps.next({
|
||||
styleConfig: style,
|
||||
size: style.dims?.size,
|
||||
layerName: options.name,
|
||||
layer: vectorLayer,
|
||||
});
|
||||
}
|
||||
const graphFrames = getGraphFrame(data.series);
|
||||
|
||||
for (const frame of data.series) {
|
||||
if (frame === graphFrames.edges[0]) {
|
||||
edgeStyle.dims = getStyleDimension(frame, edgeStyle, theme);
|
||||
} else {
|
||||
style.dims = getStyleDimension(frame, style, theme);
|
||||
}
|
||||
|
||||
updateEdge(source, graphFrames);
|
||||
}
|
||||
},
|
||||
|
||||
// Marker overlay options
|
||||
registerOptionsUI: (builder, context) => {
|
||||
const networkFrames = getGraphFrame(context.data);
|
||||
const frameNodes = networkFrames.nodes[0];
|
||||
const frameEdges = networkFrames.edges[0];
|
||||
|
||||
builder
|
||||
.addCustomEditor({
|
||||
id: 'config.style',
|
||||
category: ['Node Styles'],
|
||||
path: 'config.style',
|
||||
name: 'Node Styles',
|
||||
editor: StyleEditor,
|
||||
settings: {
|
||||
displayRotation: true,
|
||||
frameMatcher: (frame: DataFrame) => frame === frameNodes,
|
||||
},
|
||||
defaultValue: defaultOptions.style,
|
||||
})
|
||||
.addCustomEditor({
|
||||
id: 'config.edgeStyle',
|
||||
category: ['Edge Styles'],
|
||||
path: 'config.edgeStyle',
|
||||
name: 'Edge Styles',
|
||||
editor: StyleEditor,
|
||||
settings: {
|
||||
hideSymbol: true,
|
||||
frameMatcher: (frame: DataFrame) => frame === frameEdges,
|
||||
},
|
||||
defaultValue: defaultOptions.style,
|
||||
})
|
||||
.addRadio({
|
||||
path: 'config.arrow',
|
||||
name: 'Arrow',
|
||||
settings: {
|
||||
options: [
|
||||
{ label: 'None', value: 0 },
|
||||
{ label: 'Forward', value: 1 },
|
||||
{ label: 'Reverse', value: -1 },
|
||||
{ label: 'Both', value: 2 },
|
||||
],
|
||||
},
|
||||
defaultValue: defaultOptions.arrow,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'config.showLegend',
|
||||
name: 'Show legend',
|
||||
description: 'Show map legend',
|
||||
defaultValue: defaultOptions.showLegend,
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
// fill in the default values
|
||||
defaultOptions,
|
||||
};
|
||||
|
||||
function updateEdge(source: FrameVectorSource, graphFrames: GraphFrame) {
|
||||
source.clear(true);
|
||||
|
||||
const frameNodes = graphFrames.nodes[0];
|
||||
const frameEdges = graphFrames.edges[0];
|
||||
|
||||
if (!frameNodes || !frameEdges) {
|
||||
// TODO: provide helpful error message / link to docs for how to format data
|
||||
return;
|
||||
}
|
||||
|
||||
const info = getGeometryField(frameNodes, source.location);
|
||||
if (!info.field) {
|
||||
source.changed();
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Fix this
|
||||
// eslint-disable-next-line
|
||||
const field = info.field as unknown as Field<Point>;
|
||||
|
||||
// TODO for nodes, don't hard code id field name
|
||||
const nodeIdIndex = frameNodes.fields.findIndex((f: Field) => f.name === 'id');
|
||||
const nodeIdValues = frameNodes.fields[nodeIdIndex].values;
|
||||
|
||||
// Edges
|
||||
// TODO for edges, don't hard code source and target fields
|
||||
const sourceIndex = frameEdges.fields.findIndex((f: Field) => f.name === 'source');
|
||||
const targetIndex = frameEdges.fields.findIndex((f: Field) => f.name === 'target');
|
||||
|
||||
const sources = frameEdges.fields[sourceIndex].values;
|
||||
const targets = frameEdges.fields[targetIndex].values;
|
||||
|
||||
// Loop through edges, referencing node locations
|
||||
for (let i = 0; i < sources.length; i++) {
|
||||
// Create linestring for each edge
|
||||
const sourceId = sources[i];
|
||||
const targetId = targets[i];
|
||||
|
||||
const sourceNodeIndex = nodeIdValues.findIndex((value: string) => value === sourceId);
|
||||
const targetNodeIndex = nodeIdValues.findIndex((value: string) => value === targetId);
|
||||
|
||||
if (!field.values[sourceNodeIndex] || !field.values[targetNodeIndex]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const geometryEdge: Geometry = new LineString([
|
||||
field.values[sourceNodeIndex].getCoordinates(),
|
||||
field.values[targetNodeIndex].getCoordinates(),
|
||||
]);
|
||||
|
||||
const edgeFeature = new Feature({
|
||||
geometry: geometryEdge,
|
||||
});
|
||||
edgeFeature.setId(i);
|
||||
source['addFeatureInternal'](edgeFeature); // @TODO revisit?
|
||||
}
|
||||
|
||||
// Nodes
|
||||
for (let i = 0; i < frameNodes.length; i++) {
|
||||
source['addFeatureInternal'](
|
||||
new Feature({
|
||||
frameNodes,
|
||||
rowIndex: i,
|
||||
geometry: info.field.values[i],
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// only call source at the end
|
||||
source.changed();
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import { SimulationNodeDatum, SimulationLinkDatum } from 'd3-force';
|
||||
|
||||
import { Field, IconName } from '@grafana/data';
|
||||
import { DataFrame, Field, IconName } from '@grafana/data';
|
||||
|
||||
export { Options as NodeGraphOptions, ArcOption } from './panelcfg.gen';
|
||||
|
||||
@ -43,3 +43,8 @@ export type NodesMarker = {
|
||||
node: NodeDatum;
|
||||
count: number;
|
||||
};
|
||||
|
||||
export type GraphFrame = {
|
||||
nodes: DataFrame[];
|
||||
edges: DataFrame[];
|
||||
};
|
||||
|
@ -2,6 +2,8 @@ import { useMemo } from 'react';
|
||||
|
||||
import { DataFrame } from '@grafana/data';
|
||||
|
||||
import { getGraphFrame } from './utils';
|
||||
|
||||
/**
|
||||
* As we need 2 dataframes for the service map, one with nodes and one with edges we have to figure out which is which.
|
||||
* Right now we do not have any metadata for it so we just check preferredVisualisationType and then column names.
|
||||
@ -9,20 +11,6 @@ import { DataFrame } from '@grafana/data';
|
||||
*/
|
||||
export function useCategorizeFrames(series: DataFrame[]) {
|
||||
return useMemo(() => {
|
||||
return series.reduce<{
|
||||
nodes: DataFrame[];
|
||||
edges: DataFrame[];
|
||||
}>(
|
||||
(acc, frame) => {
|
||||
const sourceField = frame.fields.filter((f) => f.name === 'source');
|
||||
if (sourceField.length) {
|
||||
acc.edges.push(frame);
|
||||
} else {
|
||||
acc.nodes.push(frame);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{ edges: [], nodes: [] }
|
||||
);
|
||||
return getGraphFrame(series);
|
||||
}, [series]);
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import {
|
||||
NodeGraphDataFrameFieldNames,
|
||||
} from '@grafana/data';
|
||||
|
||||
import { EdgeDatum, NodeDatum, NodeDatumFromEdge, NodeGraphOptions } from './types';
|
||||
import { EdgeDatum, GraphFrame, NodeDatum, NodeDatumFromEdge, NodeGraphOptions } from './types';
|
||||
|
||||
type Line = { x1: number; y1: number; x2: number; y2: number };
|
||||
|
||||
@ -593,3 +593,18 @@ export const findConnectedNodesForNode = (nodes: NodeDatum[], edges: EdgeDatum[]
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const getGraphFrame = (frames: DataFrame[]) => {
|
||||
return frames.reduce<GraphFrame>(
|
||||
(acc, frame) => {
|
||||
const sourceField = frame.fields.filter((f) => f.name === 'source');
|
||||
if (sourceField.length) {
|
||||
acc.edges.push(frame);
|
||||
} else {
|
||||
acc.nodes.push(frame);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{ edges: [], nodes: [] }
|
||||
);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user