grafana/public/app/plugins/panel/canvas/utils.ts

412 lines
15 KiB
TypeScript
Raw Normal View History

import { isNumber, isString } from 'lodash';
2023-08-17 00:32:20 +08:00
import { AppEvents, Field, getFieldDisplayName, LinkModel, PluginState, SelectableValue } from '@grafana/data';
import appEvents from 'app/core/app_events';
import { hasAlphaPanels, config } from 'app/core/config';
import {
defaultElementItems,
advancedElementItems,
CanvasElementItem,
canvasElementRegistry,
CanvasElementOptions,
CanvasConnection,
} from 'app/features/canvas';
import { notFoundItem } from 'app/features/canvas/elements/notFound';
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 { DimensionContext } from 'app/features/dimensions';
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));
}
}
/*
* Provided a given field add any matching data links
* Mutates the links object in place which is then returned by the `getDataLinks` function downstream
*/
const addDataLinkForField = (
field: Field<unknown>,
data: string | undefined,
linkLookup: Set<string>,
links: Array<LinkModel<Field>>
): void => {
if (field?.getLinks) {
const disp = field.display ? field.display(data) : { text: `${data}`, numeric: +data! };
field.getLinks({ calculatedValue: disp }).forEach((link) => {
const key = `${link.title}/${link.href}`;
if (!linkLookup.has(key)) {
links.push(link);
linkLookup.add(key);
}
});
}
};
// TODO: This could be refactored a fair amount, ideally the element specific config code should be owned by each element and not in this shared util file
export function getDataLinks(
dimensionContext: DimensionContext,
elementOptions: CanvasElementOptions,
data: string | undefined
): LinkModel[] {
const panelData = dimensionContext.getPanelData();
const frames = panelData?.series;
const links: Array<LinkModel<Field>> = [];
const linkLookup = new Set<string>();
const elementConfig = elementOptions.config;
frames?.forEach((frame) => {
const visibleFields = frame.fields.filter((field) => !Boolean(field.config.custom?.hideFrom?.tooltip));
// Text config
const isTextTiedToFieldData =
elementConfig.text?.field &&
visibleFields.some((field) => getFieldDisplayName(field, frame) === elementConfig.text?.field);
const isTextColorTiedToFieldData =
elementConfig.color?.field &&
visibleFields.some((field) => getFieldDisplayName(field, frame) === elementConfig.color?.field);
// General element config
const isElementBackgroundColorTiedToFieldData =
elementOptions?.background?.color?.field &&
visibleFields.some((field) => getFieldDisplayName(field, frame) === elementOptions?.background?.color?.field);
const isElementBackgroundImageTiedToFieldData =
elementOptions?.background?.image?.field &&
visibleFields.some((field) => getFieldDisplayName(field, frame) === elementOptions?.background?.image?.field);
const isElementBorderColorTiedToFieldData =
elementOptions?.border?.color?.field &&
visibleFields.some((field) => getFieldDisplayName(field, frame) === elementOptions?.border?.color?.field);
// Icon config
const isIconSVGTiedToFieldData =
elementConfig.path?.field &&
visibleFields.some((field) => getFieldDisplayName(field, frame) === elementConfig.path?.field);
const isIconColorTiedToFieldData =
elementConfig.fill?.field &&
visibleFields.some((field) => getFieldDisplayName(field, frame) === elementConfig.fill?.field);
// Wind turbine config (maybe remove / not support this?)
const isWindTurbineRPMTiedToFieldData =
elementConfig.rpm?.field &&
visibleFields.some((field) => getFieldDisplayName(field, frame) === elementConfig.rpm?.field);
// Server config
const isServerBlinkRateTiedToFieldData =
elementConfig.blinkRate?.field &&
visibleFields.some((field) => getFieldDisplayName(field, frame) === elementConfig.blinkRate?.field);
const isServerStatusColorTiedToFieldData =
elementConfig.statusColor?.field &&
visibleFields.some((field) => getFieldDisplayName(field, frame) === elementConfig.statusColor?.field);
const isServerBulbColorTiedToFieldData =
elementConfig.bulbColor?.field &&
visibleFields.some((field) => getFieldDisplayName(field, frame) === elementConfig.bulbColor?.field);
if (isTextTiedToFieldData) {
const field = visibleFields.filter((field) => getFieldDisplayName(field, frame) === elementConfig.text?.field)[0];
addDataLinkForField(field, data, linkLookup, links);
}
if (isTextColorTiedToFieldData) {
const field = visibleFields.filter(
(field) => getFieldDisplayName(field, frame) === elementConfig.color?.field
)[0];
addDataLinkForField(field, data, linkLookup, links);
}
if (isElementBackgroundColorTiedToFieldData) {
const field = visibleFields.filter(
(field) => getFieldDisplayName(field, frame) === elementOptions?.background?.color?.field
)[0];
addDataLinkForField(field, data, linkLookup, links);
}
if (isElementBackgroundImageTiedToFieldData) {
const field = visibleFields.filter(
(field) => getFieldDisplayName(field, frame) === elementOptions?.background?.image?.field
)[0];
addDataLinkForField(field, data, linkLookup, links);
}
if (isElementBorderColorTiedToFieldData) {
const field = visibleFields.filter(
(field) => getFieldDisplayName(field, frame) === elementOptions?.border?.color?.field
)[0];
addDataLinkForField(field, data, linkLookup, links);
}
if (isIconSVGTiedToFieldData) {
const field = visibleFields.filter((field) => getFieldDisplayName(field, frame) === elementConfig.path?.field)[0];
addDataLinkForField(field, data, linkLookup, links);
}
if (isIconColorTiedToFieldData) {
const field = visibleFields.filter((field) => getFieldDisplayName(field, frame) === elementConfig.fill?.field)[0];
addDataLinkForField(field, data, linkLookup, links);
}
if (isWindTurbineRPMTiedToFieldData) {
const field = visibleFields.filter((field) => getFieldDisplayName(field, frame) === elementConfig.rpm?.field)[0];
addDataLinkForField(field, data, linkLookup, links);
}
if (isServerBlinkRateTiedToFieldData) {
const field = visibleFields.filter(
(field) => getFieldDisplayName(field, frame) === elementConfig.blinkRate?.field
)[0];
addDataLinkForField(field, data, linkLookup, links);
}
if (isServerStatusColorTiedToFieldData) {
const field = visibleFields.filter(
(field) => getFieldDisplayName(field, frame) === elementConfig.statusColor?.field
)[0];
addDataLinkForField(field, data, linkLookup, links);
}
if (isServerBulbColorTiedToFieldData) {
const field = visibleFields.filter(
(field) => getFieldDisplayName(field, frame) === elementConfig.bulbColor?.field
)[0];
addDataLinkForField(field, data, linkLookup, links);
}
});
return links;
}
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,
});
}
});
}
}
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 });
});
}
export const calculateCoordinates = (
sourceRect: DOMRect,
parentRect: DOMRect,
info: CanvasConnection,
Canvas: Add Pan and Zoom (#76705) * Canvas: Add Zoom * Scale selecto components based on zoom state * Fix pan by reverting to 3.1.0 for zoom-pan * Update to latest library that fixes pan regression * Add mini map to canvas pan zoom * Fix selecto and anchors on hover * Update naming to be more clear * Switch back to contentComponent * Apply transformScale to drag and resize * Update connection source and target scaling * Add option to display mini map * Update yarn lock * Revert "Update yarn lock" This reverts commit 3d1dd65d5726fb0fd0813347451884a4034ae5d3. * Set yarn lock to main * Revert "Set yarn lock to main" This reverts commit 64bc50557e75657fae14f81077d1d08b4e9e9029. * Update to Yarn 4 * Add react-zoom-pan-pinch * Update react-zoom-pan checksum * Revert changes to json files * Remove last line of api merged * Remove last lines of all impacted jsons * Update home json * Update coordinate calc function to include scale * Fix types in coordinate calc function * Fix util calculation for transform * Fix arrow anchor shift behavior * Fix scale offset when adding elements during zoom * Fix drag of selected group during zoom * Add feature flag for canvas pan zoom * Revert "Add feature flag for canvas pan zoom" This reverts commit b026e31d8d9ed64b1fe307f852df10292fffadf4. * Regenerate feature flag after merge * Apply feature flag to enable pan zoom wrappers * Add mini map toggle behind feature flag * Simplify minimap behavior * Update feature flag registry * Set minimap to false by default * fix gen-cue * Set toggles gen to main Add blank line to toggle gen csv * Add canvas pan zoom to csv * Remove old comment * Change ref parameter to be more descriptive * Rename visibleFun to be more descriptive * Consolidate transformScale transformRef in util * Remove non-null assertion on connection parentRect * Consolidate parentRect null coalescing into object * Remove minimap and change toggle * Add controls inline help for pan and zoom * Clean up mouse events * Pull scale out of ref and isolate transform * Remove transform ref from scene div * Fix context menu visible behavior * Fix connections and update util functions * Move transform component instance to util * fix backend test * minor updates * Clean up connections / fix minor bug where offset of arrow wasn't being calculated correctly * missed connection code cleanup * cleanup scene code a bit more * actually fix backend test * move eslint disable line closer to actual issue --------- Co-authored-by: nmarrs <nathanielmarrs@gmail.com>
2024-01-03 03:52:21 +08:00
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
Canvas: Add Pan and Zoom (#76705) * Canvas: Add Zoom * Scale selecto components based on zoom state * Fix pan by reverting to 3.1.0 for zoom-pan * Update to latest library that fixes pan regression * Add mini map to canvas pan zoom * Fix selecto and anchors on hover * Update naming to be more clear * Switch back to contentComponent * Apply transformScale to drag and resize * Update connection source and target scaling * Add option to display mini map * Update yarn lock * Revert "Update yarn lock" This reverts commit 3d1dd65d5726fb0fd0813347451884a4034ae5d3. * Set yarn lock to main * Revert "Set yarn lock to main" This reverts commit 64bc50557e75657fae14f81077d1d08b4e9e9029. * Update to Yarn 4 * Add react-zoom-pan-pinch * Update react-zoom-pan checksum * Revert changes to json files * Remove last line of api merged * Remove last lines of all impacted jsons * Update home json * Update coordinate calc function to include scale * Fix types in coordinate calc function * Fix util calculation for transform * Fix arrow anchor shift behavior * Fix scale offset when adding elements during zoom * Fix drag of selected group during zoom * Add feature flag for canvas pan zoom * Revert "Add feature flag for canvas pan zoom" This reverts commit b026e31d8d9ed64b1fe307f852df10292fffadf4. * Regenerate feature flag after merge * Apply feature flag to enable pan zoom wrappers * Add mini map toggle behind feature flag * Simplify minimap behavior * Update feature flag registry * Set minimap to false by default * fix gen-cue * Set toggles gen to main Add blank line to toggle gen csv * Add canvas pan zoom to csv * Remove old comment * Change ref parameter to be more descriptive * Rename visibleFun to be more descriptive * Consolidate transformScale transformRef in util * Remove non-null assertion on connection parentRect * Consolidate parentRect null coalescing into object * Remove minimap and change toggle * Add controls inline help for pan and zoom * Clean up mouse events * Pull scale out of ref and isolate transform * Remove transform ref from scene div * Fix context menu visible behavior * Fix connections and update util functions * Move transform component instance to util * fix backend test * minor updates * Clean up connections / fix minor bug where offset of arrow wasn't being calculated correctly * missed connection code cleanup * cleanup scene code a bit more * actually fix backend test * move eslint disable line closer to actual issue --------- Co-authored-by: nmarrs <nathanielmarrs@gmail.com>
2024-01-03 03:52:21 +08:00
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;
}
Canvas: Add Pan and Zoom (#76705) * Canvas: Add Zoom * Scale selecto components based on zoom state * Fix pan by reverting to 3.1.0 for zoom-pan * Update to latest library that fixes pan regression * Add mini map to canvas pan zoom * Fix selecto and anchors on hover * Update naming to be more clear * Switch back to contentComponent * Apply transformScale to drag and resize * Update connection source and target scaling * Add option to display mini map * Update yarn lock * Revert "Update yarn lock" This reverts commit 3d1dd65d5726fb0fd0813347451884a4034ae5d3. * Set yarn lock to main * Revert "Set yarn lock to main" This reverts commit 64bc50557e75657fae14f81077d1d08b4e9e9029. * Update to Yarn 4 * Add react-zoom-pan-pinch * Update react-zoom-pan checksum * Revert changes to json files * Remove last line of api merged * Remove last lines of all impacted jsons * Update home json * Update coordinate calc function to include scale * Fix types in coordinate calc function * Fix util calculation for transform * Fix arrow anchor shift behavior * Fix scale offset when adding elements during zoom * Fix drag of selected group during zoom * Add feature flag for canvas pan zoom * Revert "Add feature flag for canvas pan zoom" This reverts commit b026e31d8d9ed64b1fe307f852df10292fffadf4. * Regenerate feature flag after merge * Apply feature flag to enable pan zoom wrappers * Add mini map toggle behind feature flag * Simplify minimap behavior * Update feature flag registry * Set minimap to false by default * fix gen-cue * Set toggles gen to main Add blank line to toggle gen csv * Add canvas pan zoom to csv * Remove old comment * Change ref parameter to be more descriptive * Rename visibleFun to be more descriptive * Consolidate transformScale transformRef in util * Remove non-null assertion on connection parentRect * Consolidate parentRect null coalescing into object * Remove minimap and change toggle * Add controls inline help for pan and zoom * Clean up mouse events * Pull scale out of ref and isolate transform * Remove transform ref from scene div * Fix context menu visible behavior * Fix connections and update util functions * Move transform component instance to util * fix backend test * minor updates * Clean up connections / fix minor bug where offset of arrow wasn't being calculated correctly * missed connection code cleanup * cleanup scene code a bit more * actually fix backend test * move eslint disable line closer to actual issue --------- Co-authored-by: nmarrs <nathanielmarrs@gmail.com>
2024-01-03 03:52:21 +08:00
x2 /= transformScale;
y2 /= transformScale;
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
) => {
return { x: valueX * (x2 - x1) + x1, y: valueY * (y2 - y1) + y1 };
};
export const calculateAngle = (x1: number, y1: number, x2: number, y2: number) => {
return (Math.atan2(y2 - y1, x2 - x1) * 180) / Math.PI;
};
// @TODO revisit, currently returning last row index for field
export const getRowIndex = (fieldName: string | undefined, scene: Scene) => {
if (fieldName) {
const series = scene.context.getPanelData()?.series[0];
const field = series?.fields.find((f) => (f.name = fieldName));
const data = field?.values;
return data ? data.length - 1 : 0;
}
return 0;
};
export const getConnectionStyles = (info: CanvasConnection, scene: Scene, defaultArrowSize: number) => {
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 lineStyle = info.lineStyle === LineStyle.Dashed ? StrokeDasharray.Dashed : StrokeDasharray.Solid;
return { strokeColor, strokeWidth, lineStyle };
};
Canvas: Add Pan and Zoom (#76705) * Canvas: Add Zoom * Scale selecto components based on zoom state * Fix pan by reverting to 3.1.0 for zoom-pan * Update to latest library that fixes pan regression * Add mini map to canvas pan zoom * Fix selecto and anchors on hover * Update naming to be more clear * Switch back to contentComponent * Apply transformScale to drag and resize * Update connection source and target scaling * Add option to display mini map * Update yarn lock * Revert "Update yarn lock" This reverts commit 3d1dd65d5726fb0fd0813347451884a4034ae5d3. * Set yarn lock to main * Revert "Set yarn lock to main" This reverts commit 64bc50557e75657fae14f81077d1d08b4e9e9029. * Update to Yarn 4 * Add react-zoom-pan-pinch * Update react-zoom-pan checksum * Revert changes to json files * Remove last line of api merged * Remove last lines of all impacted jsons * Update home json * Update coordinate calc function to include scale * Fix types in coordinate calc function * Fix util calculation for transform * Fix arrow anchor shift behavior * Fix scale offset when adding elements during zoom * Fix drag of selected group during zoom * Add feature flag for canvas pan zoom * Revert "Add feature flag for canvas pan zoom" This reverts commit b026e31d8d9ed64b1fe307f852df10292fffadf4. * Regenerate feature flag after merge * Apply feature flag to enable pan zoom wrappers * Add mini map toggle behind feature flag * Simplify minimap behavior * Update feature flag registry * Set minimap to false by default * fix gen-cue * Set toggles gen to main Add blank line to toggle gen csv * Add canvas pan zoom to csv * Remove old comment * Change ref parameter to be more descriptive * Rename visibleFun to be more descriptive * Consolidate transformScale transformRef in util * Remove non-null assertion on connection parentRect * Consolidate parentRect null coalescing into object * Remove minimap and change toggle * Add controls inline help for pan and zoom * Clean up mouse events * Pull scale out of ref and isolate transform * Remove transform ref from scene div * Fix context menu visible behavior * Fix connections and update util functions * Move transform component instance to util * fix backend test * minor updates * Clean up connections / fix minor bug where offset of arrow wasn't being calculated correctly * missed connection code cleanup * cleanup scene code a bit more * actually fix backend test * move eslint disable line closer to actual issue --------- Co-authored-by: nmarrs <nathanielmarrs@gmail.com>
2024-01-03 03:52:21 +08:00
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;
};