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/features/canvas/runtime/frame.tsx

262 lines
7.7 KiB

import { cloneDeep } from 'lodash';
import React from 'react';
import { canvasElementRegistry, CanvasFrameOptions } from 'app/features/canvas';
import { notFoundItem } from 'app/features/canvas/elements/notFound';
import { DimensionContext } from 'app/features/dimensions';
import { LayerActionID } from 'app/plugins/panel/canvas/types';
import { updateConnectionsForSource } from '../../../plugins/panel/canvas/utils';
import { CanvasElementItem } from '../element';
import { HorizontalConstraint, Placement, VerticalConstraint } from '../types';
import { ElementState } from './element';
import { RootElement } from './root';
import { Scene } from './scene';
const DEFAULT_OFFSET = 10;
const HORIZONTAL_OFFSET = 50;
export const frameItemDummy: CanvasElementItem = {
id: 'frame',
name: 'Frame',
description: 'Frame',
getNewOptions: () => ({
config: {},
}),
// eslint-disable-next-line react/display-name
display: () => {
return <div>FRAME!</div>;
},
};
export class FrameState extends ElementState {
elements: ElementState[] = [];
scene: Scene;
constructor(public options: CanvasFrameOptions, scene: Scene, public parent?: FrameState) {
super(frameItemDummy, options, parent);
this.scene = scene;
// mutate options object
let { elements } = this.options;
if (!elements) {
this.options.elements = elements = [];
}
for (const c of elements) {
if (c.type === 'frame') {
this.elements.push(new FrameState(c as CanvasFrameOptions, scene, this));
} else {
const item = canvasElementRegistry.getIfExists(c.type) ?? notFoundItem;
this.elements.push(new ElementState(item, c, this));
}
}
}
isRoot(): this is RootElement {
return false;
}
updateData(ctx: DimensionContext) {
super.updateData(ctx);
for (const elem of this.elements) {
elem.updateData(ctx);
}
}
// used in the layer editor
reorder(startIndex: number, endIndex: number) {
const result = Array.from(this.elements);
const [removed] = result.splice(startIndex, 1);
result.splice(endIndex, 0, removed);
this.elements = result;
this.reinitializeMoveable();
}
// used for tree view
reorderTree(src: ElementState, dest: ElementState, firstPosition = false) {
const result = Array.from(this.elements);
const srcIndex = this.elements.indexOf(src);
const destIndex = firstPosition ? this.elements.length - 1 : this.elements.indexOf(dest);
const [removed] = result.splice(srcIndex, 1);
result.splice(destIndex, 0, removed);
this.elements = result;
this.reinitializeMoveable();
}
doMove(child: ElementState, action: LayerActionID) {
const vals = this.elements.filter((v) => v !== child);
if (action === LayerActionID.MoveBottom) {
vals.unshift(child);
} else {
vals.push(child);
}
this.elements = vals;
this.scene.save();
this.reinitializeMoveable();
}
reinitializeMoveable() {
// Need to first clear current selection and then re-init moveable with slight delay
this.scene.clearCurrentSelection();
setTimeout(() => this.scene.initMoveable(true, this.scene.isEditingEnabled));
}
// ??? or should this be on the element directly?
// are actions scoped to layers?
doAction = (action: LayerActionID, element: ElementState, updateName = true, shiftItemsOnDuplicate = true) => {
switch (action) {
case LayerActionID.Delete:
this.elements = this.elements.filter((e) => e !== element);
updateConnectionsForSource(element, this.scene);
this.scene.byName.delete(element.options.name);
this.scene.save();
this.reinitializeMoveable();
break;
case LayerActionID.Duplicate:
if (element.item.id === 'frame') {
console.log('Can not duplicate frames (yet)', action, element);
return;
}
const opts = cloneDeep(element.options);
if (shiftItemsOnDuplicate) {
const { constraint, placement: oldPlacement } = element.options;
const { vertical, horizontal } = constraint ?? {};
const placement = { ...oldPlacement } ?? ({} as Placement);
switch (vertical) {
case VerticalConstraint.Top:
if (placement.top == null) {
placement.top = 25;
} else {
placement.top += DEFAULT_OFFSET;
}
break;
case VerticalConstraint.Bottom:
if (placement.bottom == null) {
placement.bottom = 100;
} else {
placement.bottom -= DEFAULT_OFFSET;
}
break;
case VerticalConstraint.TopBottom:
if (placement.top == null) {
placement.top = 25;
} else {
placement.top += DEFAULT_OFFSET;
}
if (placement.bottom == null) {
placement.bottom = 100;
} else {
placement.bottom -= DEFAULT_OFFSET;
}
break;
case VerticalConstraint.Center:
if (placement.top != null) {
placement.top -= DEFAULT_OFFSET;
}
break;
}
switch (horizontal) {
case HorizontalConstraint.Left:
if (placement.left == null) {
placement.left = HORIZONTAL_OFFSET;
} else {
placement.left += DEFAULT_OFFSET;
}
break;
case HorizontalConstraint.Right:
if (placement.right == null) {
placement.right = HORIZONTAL_OFFSET;
} else {
placement.right -= DEFAULT_OFFSET;
}
break;
case HorizontalConstraint.LeftRight:
if (placement.left == null) {
placement.left = HORIZONTAL_OFFSET;
} else {
placement.left += DEFAULT_OFFSET;
}
if (placement.right == null) {
placement.right = HORIZONTAL_OFFSET;
} else {
placement.right -= DEFAULT_OFFSET;
}
break;
case HorizontalConstraint.Center:
if (placement.left != null) {
placement.left -= DEFAULT_OFFSET;
}
break;
}
opts.placement = placement;
}
// Clear connections on duplicate
opts.connections = undefined;
const copy = new ElementState(element.item, opts, this);
copy.updateData(this.scene.context);
if (updateName) {
copy.options.name = this.scene.getNextElementName();
}
this.elements.push(copy);
this.scene.byName.set(copy.options.name, copy);
// Update scene byName map for original element (to avoid stale references (e.g. for connections))
this.scene.byName.set(element.options.name, element);
this.scene.save();
this.reinitializeMoveable();
setTimeout(() => {
this.scene.targetsToSelect.add(copy.div!);
});
break;
case LayerActionID.MoveTop:
case LayerActionID.MoveBottom:
element.parent?.doMove(element, action);
break;
default:
console.log('DO action', action, element);
return;
}
};
render() {
return (
<div key={this.UID} ref={this.initElement}>
{this.elements.map((v) => v.render())}
</div>
);
}
/** Recursively visit all nodes */
visit(visitor: (v: ElementState) => void) {
super.visit(visitor);
for (const e of this.elements) {
visitor(e);
}
}
getSaveModel() {
return {
...this.options,
elements: this.elements.map((v) => v.getSaveModel()),
};
}
}