The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/plugins/panel/canvas/utils.ts

295 lines
9.9 KiB

import { isNumber, isString } from 'lodash';
import { AppEvents, PluginState, SelectableValue } from '@grafana/data';
import appEvents from 'app/core/app_events';
import { hasAlphaPanels, config } from 'app/core/config';
import {
CanvasConnection,
CanvasElementItem,
CanvasElementOptions,
ConnectionDirection,
} from 'app/features/canvas/element';
import { notFoundItem } from 'app/features/canvas/elements/notFound';
import { advancedElementItems, canvasElementRegistry, defaultElementItems } from 'app/features/canvas/registry';
import { ElementState } from 'app/features/canvas/runtime/element';
import { FrameState } from 'app/features/canvas/runtime/frame';
import { Scene, SelectionParams } from 'app/features/canvas/runtime/scene';
import { AnchorPoint, ConnectionState, LineStyle, StrokeDasharray } from './types';
export function doSelect(scene: Scene, element: ElementState | FrameState) {
try {
let selection: SelectionParams = { targets: [] };
if (element instanceof FrameState) {
const targetElements: HTMLDivElement[] = [];
targetElements.push(element?.div!);
selection.targets = targetElements;
selection.frame = element;
scene.select(selection);
} else {
scene.currentLayer = element.parent;
selection.targets = [element?.div!];
scene.select(selection);
}
} catch (error) {
appEvents.emit(AppEvents.alertError, ['Unable to select element, try selecting element in panel instead']);
}
}
export function getElementTypes(shouldShowAdvancedTypes: boolean | undefined, current?: string): RegistrySelectInfo {
if (shouldShowAdvancedTypes) {
return getElementTypesOptions([...defaultElementItems, ...advancedElementItems], current);
}
return getElementTypesOptions([...defaultElementItems], current);
}
interface RegistrySelectInfo {
options: Array<SelectableValue<string>>;
current: Array<SelectableValue<string>>;
}
export function getElementTypesOptions(items: CanvasElementItem[], current: string | undefined): RegistrySelectInfo {
const selectables: RegistrySelectInfo = { options: [], current: [] };
const alpha: Array<SelectableValue<string>> = [];
for (const item of items) {
const option: SelectableValue<string> = { label: item.name, value: item.id, description: item.description };
if (item.state === PluginState.alpha) {
if (!hasAlphaPanels) {
continue;
}
option.label = `${item.name} (Alpha)`;
alpha.push(option);
} else {
selectables.options.push(option);
}
if (item.id === current) {
selectables.current.push(option);
}
}
for (const a of alpha) {
selectables.options.push(a);
}
return selectables;
}
export function onAddItem(sel: SelectableValue<string>, rootLayer: FrameState | undefined, anchorPoint?: AnchorPoint) {
const newItem = canvasElementRegistry.getIfExists(sel.value) ?? notFoundItem;
const newElementOptions: CanvasElementOptions = {
...newItem.getNewOptions(),
type: newItem.id,
name: '',
};
if (anchorPoint) {
newElementOptions.placement = { ...newElementOptions.placement, top: anchorPoint.y, left: anchorPoint.x };
}
if (newItem.defaultSize) {
newElementOptions.placement = { ...newElementOptions.placement, ...newItem.defaultSize };
}
if (rootLayer) {
const newElement = new ElementState(newItem, newElementOptions, rootLayer);
newElement.updateData(rootLayer.scene.context);
rootLayer.elements.push(newElement);
rootLayer.scene.save();
rootLayer.reinitializeMoveable();
setTimeout(() => doSelect(rootLayer.scene, newElement));
}
}
export function isConnectionSource(element: ElementState) {
return element.options.connections && element.options.connections.length > 0;
}
export function isConnectionTarget(element: ElementState, sceneByName: Map<string, ElementState>) {
const connections = getConnections(sceneByName);
return connections.some((connection) => connection.target === element);
}
export function getConnections(sceneByName: Map<string, ElementState>) {
const connections: ConnectionState[] = [];
for (let v of sceneByName.values()) {
if (v.options.connections) {
v.options.connections.forEach((c, index) => {
// @TODO Remove after v10.x
if (isString(c.color)) {
c.color = { fixed: c.color };
}
if (isNumber(c.size)) {
c.size = { fixed: 2, min: 1, max: 10 };
}
const target = c.targetName ? sceneByName.get(c.targetName) : v.parent;
if (target) {
connections.push({
index,
source: v,
target,
info: c,
vertices: c.vertices ?? undefined,
sourceOriginal: c.sourceOriginal ?? undefined,
targetOriginal: c.targetOriginal ?? undefined,
});
}
});
}
}
return connections;
}
export function getConnectionsByTarget(element: ElementState, scene: Scene) {
return scene.connections.state.filter((connection) => connection.target === element);
}
export function updateConnectionsForSource(element: ElementState, scene: Scene) {
const targetConnections = getConnectionsByTarget(element, scene);
targetConnections.forEach((connection) => {
const sourceConnections = connection.source.options.connections?.splice(0) ?? [];
const connections = sourceConnections.filter((con) => con.targetName !== element.getName());
connection.source.onChange({ ...connection.source.options, connections });
});
// Update scene connection state to clear out old connections
scene.connections.updateState();
}
export const calculateCoordinates = (
sourceRect: DOMRect,
parentRect: DOMRect,
info: CanvasConnection,
target: ElementState,
transformScale: number
) => {
const sourceHorizontalCenter = sourceRect.left - parentRect.left + sourceRect.width / 2;
const sourceVerticalCenter = sourceRect.top - parentRect.top + sourceRect.height / 2;
// Convert from connection coords to DOM coords
const x1 = (sourceHorizontalCenter + (info.source.x * sourceRect.width) / 2) / transformScale;
const y1 = (sourceVerticalCenter - (info.source.y * sourceRect.height) / 2) / transformScale;
let x2: number;
let y2: number;
const targetRect = target.div?.getBoundingClientRect();
if (info.targetName && targetRect) {
const targetHorizontalCenter = targetRect.left - parentRect.left + targetRect.width / 2;
const targetVerticalCenter = targetRect.top - parentRect.top + targetRect.height / 2;
x2 = targetHorizontalCenter + (info.target.x * targetRect.width) / 2;
y2 = targetVerticalCenter - (info.target.y * targetRect.height) / 2;
} else {
const parentHorizontalCenter = parentRect.width / 2;
const parentVerticalCenter = parentRect.height / 2;
x2 = parentHorizontalCenter + (info.target.x * parentRect.width) / 2;
y2 = parentVerticalCenter - (info.target.y * parentRect.height) / 2;
}
x2 /= transformScale;
y2 /= transformScale;
// TODO look into a better way to avoid division by zero
if (x2 - x1 === 0) {
x2 += 1;
}
if (y2 - y1 === 0) {
y2 += 1;
}
return { x1, y1, x2, y2 };
};
export const calculateMidpoint = (x1: number, y1: number, x2: number, y2: number) => {
return { x: (x1 + x2) / 2, y: (y1 + y2) / 2 };
};
export const calculateAbsoluteCoords = (
x1: number,
y1: number,
x2: number,
y2: number,
valueX: number,
valueY: number,
deltaX: number,
deltaY: number
) => {
return { x: valueX * deltaX + x1, y: valueY * deltaY + y1 };
};
// Calculate angle between two points and return angle in radians
export const calculateAngle = (x1: number, y1: number, x2: number, y2: number) => {
return Math.atan2(y2 - y1, x2 - x1);
};
export const calculateDistance = (x1: number, y1: number, x2: number, y2: number) => {
//TODO add sqrt approx option
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
};
// @TODO revisit, currently returning last row index for field
export const getRowIndex = (fieldName: string | undefined, scene: Scene) => {
if (fieldName) {
const series = scene.data?.series[0];
const field = series?.fields.find((field) => field.name === fieldName);
const data = field?.values;
return data ? data.length - 1 : 0;
}
return 0;
};
export const getConnectionStyles = (
info: CanvasConnection,
scene: Scene,
defaultArrowSize: number,
defaultArrowDirection: ConnectionDirection
) => {
const defaultArrowColor = config.theme2.colors.text.primary;
const lastRowIndex = getRowIndex(info.size?.field, scene);
const strokeColor = info.color ? scene.context.getColor(info.color).value() : defaultArrowColor;
const strokeWidth = info.size ? scene.context.getScale(info.size).get(lastRowIndex) : defaultArrowSize;
const strokeRadius = info.radius ? scene.context.getScale(info.radius).get(lastRowIndex) : 0;
const arrowDirection = info.direction ? info.direction : defaultArrowDirection;
const lineStyle = getLineStyle(info.lineStyle?.style);
const shouldAnimate = info.lineStyle?.animate;
return { strokeColor, strokeWidth, strokeRadius, arrowDirection, lineStyle, shouldAnimate };
};
const getLineStyle = (lineStyle?: LineStyle) => {
switch (lineStyle) {
case LineStyle.Dashed:
return StrokeDasharray.Dashed;
case LineStyle.Dotted:
return StrokeDasharray.Dotted;
default:
return StrokeDasharray.Solid;
}
};
export const getParentBoundingClientRect = (scene: Scene) => {
if (config.featureToggles.canvasPanelPanZoom) {
const transformRef = scene.transformComponentRef?.current;
return transformRef?.instance.contentComponent?.getBoundingClientRect();
}
return scene.div?.getBoundingClientRect();
};
export const getTransformInstance = (scene: Scene) => {
if (config.featureToggles.canvasPanelPanZoom) {
return scene.transformComponentRef?.current?.instance;
}
return undefined;
};
export const getParent = (scene: Scene) => {
if (config.featureToggles.canvasPanelPanZoom) {
return scene.transformComponentRef?.current?.instance.contentComponent;
}
return scene.div;
};