mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Canvas: Add basic responsive design and layer editor UX (#40404)
This commit is contained in:
@@ -6,12 +6,14 @@ import {
|
|||||||
CanvasElementItem,
|
CanvasElementItem,
|
||||||
CanvasElementOptions,
|
CanvasElementOptions,
|
||||||
canvasElementRegistry,
|
canvasElementRegistry,
|
||||||
|
Placement,
|
||||||
|
Anchor,
|
||||||
} from 'app/features/canvas';
|
} from 'app/features/canvas';
|
||||||
import { DimensionContext } from 'app/features/dimensions';
|
import { DimensionContext } from 'app/features/dimensions';
|
||||||
import { notFoundItem } from 'app/features/canvas/elements/notFound';
|
import { notFoundItem } from 'app/features/canvas/elements/notFound';
|
||||||
import { GroupState } from './group';
|
import { GroupState } from './group';
|
||||||
|
|
||||||
let counter = 100;
|
let counter = 0;
|
||||||
|
|
||||||
export class ElementState {
|
export class ElementState {
|
||||||
readonly UID = counter++;
|
readonly UID = counter++;
|
||||||
@@ -28,16 +30,82 @@ export class ElementState {
|
|||||||
height = 100;
|
height = 100;
|
||||||
data?: any; // depends on the type
|
data?: any; // depends on the type
|
||||||
|
|
||||||
|
// From options, but always set and always valid
|
||||||
|
anchor: Anchor;
|
||||||
|
placement: Placement;
|
||||||
|
|
||||||
constructor(public item: CanvasElementItem, public options: CanvasElementOptions, public parent?: GroupState) {
|
constructor(public item: CanvasElementItem, public options: CanvasElementOptions, public parent?: GroupState) {
|
||||||
if (!options) {
|
if (!options) {
|
||||||
this.options = { type: item.id };
|
this.options = { type: item.id };
|
||||||
}
|
}
|
||||||
|
this.anchor = options.anchor ?? {};
|
||||||
|
this.placement = options.placement ?? {};
|
||||||
|
options.anchor = this.anchor;
|
||||||
|
options.placement = this.placement;
|
||||||
|
}
|
||||||
|
|
||||||
|
validatePlacement() {
|
||||||
|
const { anchor, placement } = this;
|
||||||
|
if (!(anchor.left || anchor.right)) {
|
||||||
|
anchor.left = true;
|
||||||
|
}
|
||||||
|
if (!(anchor.top || anchor.bottom)) {
|
||||||
|
anchor.top = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const w = placement.width ?? 100; // this.div ? this.div.clientWidth : this.width;
|
||||||
|
const h = placement.height ?? 100; // this.div ? this.div.clientHeight : this.height;
|
||||||
|
|
||||||
|
if (anchor.top) {
|
||||||
|
if (!placement.top) {
|
||||||
|
placement.top = 0;
|
||||||
|
}
|
||||||
|
if (anchor.bottom) {
|
||||||
|
delete placement.height;
|
||||||
|
} else {
|
||||||
|
placement.height = h;
|
||||||
|
delete placement.bottom;
|
||||||
|
}
|
||||||
|
} else if (anchor.bottom) {
|
||||||
|
if (!placement.bottom) {
|
||||||
|
placement.bottom = 0;
|
||||||
|
}
|
||||||
|
placement.height = h;
|
||||||
|
delete placement.top;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anchor.left) {
|
||||||
|
if (!placement.left) {
|
||||||
|
placement.left = 0;
|
||||||
|
}
|
||||||
|
if (anchor.right) {
|
||||||
|
delete placement.width;
|
||||||
|
} else {
|
||||||
|
placement.width = w;
|
||||||
|
delete placement.right;
|
||||||
|
}
|
||||||
|
} else if (anchor.right) {
|
||||||
|
if (!placement.right) {
|
||||||
|
placement.right = 0;
|
||||||
|
}
|
||||||
|
placement.width = w;
|
||||||
|
delete placement.left;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.width = w;
|
||||||
|
this.height = h;
|
||||||
|
|
||||||
|
this.options.anchor = this.anchor;
|
||||||
|
this.options.placement = this.placement;
|
||||||
|
|
||||||
|
// console.log('validate', this.UID, this.item.id, this.placement, this.anchor);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The parent size, need to set our own size based on offsets
|
// The parent size, need to set our own size based on offsets
|
||||||
updateSize(width: number, height: number) {
|
updateSize(width: number, height: number) {
|
||||||
this.width = width;
|
this.width = width;
|
||||||
this.height = height;
|
this.height = height;
|
||||||
|
this.validatePlacement();
|
||||||
|
|
||||||
// Update the CSS position
|
// Update the CSS position
|
||||||
this.sizeStyle = {
|
this.sizeStyle = {
|
||||||
@@ -129,33 +197,95 @@ export class ElementState {
|
|||||||
|
|
||||||
initElement = (target: HTMLDivElement) => {
|
initElement = (target: HTMLDivElement) => {
|
||||||
this.div = target;
|
this.div = target;
|
||||||
|
|
||||||
let placement = this.options.placement;
|
|
||||||
if (!placement) {
|
|
||||||
placement = {
|
|
||||||
left: 0,
|
|
||||||
top: 0,
|
|
||||||
};
|
|
||||||
this.options.placement = placement;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
applyDrag = (event: OnDrag) => {
|
applyDrag = (event: OnDrag) => {
|
||||||
const placement = this.options.placement;
|
const { placement, anchor } = this;
|
||||||
placement!.top = event.top;
|
|
||||||
placement!.left = event.left;
|
|
||||||
|
|
||||||
event.target.style.top = `${event.top}px`;
|
const deltaX = event.delta[0];
|
||||||
event.target.style.left = `${event.left}px`;
|
const deltaY = event.delta[1];
|
||||||
|
|
||||||
|
const style = event.target.style;
|
||||||
|
if (anchor.top) {
|
||||||
|
placement.top! += deltaY;
|
||||||
|
style.top = `${placement.top}px`;
|
||||||
|
}
|
||||||
|
if (anchor.bottom) {
|
||||||
|
placement.bottom! -= deltaY;
|
||||||
|
style.bottom = `${placement.bottom}px`;
|
||||||
|
}
|
||||||
|
if (anchor.left) {
|
||||||
|
placement.left! += deltaX;
|
||||||
|
style.left = `${placement.left}px`;
|
||||||
|
}
|
||||||
|
if (anchor.right) {
|
||||||
|
placement.right! -= deltaX;
|
||||||
|
style.right = `${placement.right}px`;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// kinda like:
|
||||||
|
// https://github.com/grafana/grafana-edge-app/blob/main/src/panels/draw/WrapItem.tsx#L44
|
||||||
applyResize = (event: OnResize) => {
|
applyResize = (event: OnResize) => {
|
||||||
const placement = this.options.placement;
|
const { placement, anchor } = this;
|
||||||
placement!.height = event.height;
|
|
||||||
placement!.width = event.width;
|
|
||||||
|
|
||||||
event.target.style.height = `${event.height}px`;
|
const style = event.target.style;
|
||||||
event.target.style.width = `${event.width}px`;
|
const deltaX = event.delta[0];
|
||||||
|
const deltaY = event.delta[1];
|
||||||
|
const dirLR = event.direction[0];
|
||||||
|
const dirTB = event.direction[1];
|
||||||
|
if (dirLR === 1) {
|
||||||
|
// RIGHT
|
||||||
|
if (anchor.right) {
|
||||||
|
placement.right! -= deltaX;
|
||||||
|
style.right = `${placement.right}px`;
|
||||||
|
if (!anchor.left) {
|
||||||
|
placement.width = event.width;
|
||||||
|
style.width = `${placement.width}px`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
placement.width! = event.width;
|
||||||
|
style.width = `${placement.width}px`;
|
||||||
|
}
|
||||||
|
} else if (dirLR === -1) {
|
||||||
|
// LEFT
|
||||||
|
if (anchor.left) {
|
||||||
|
placement.left! -= deltaX;
|
||||||
|
placement.width! = event.width;
|
||||||
|
style.left = `${placement.left}px`;
|
||||||
|
style.width = `${placement.width}px`;
|
||||||
|
} else {
|
||||||
|
placement.width! += deltaX;
|
||||||
|
style.width = `${placement.width}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dirTB === -1) {
|
||||||
|
// TOP
|
||||||
|
if (anchor.top) {
|
||||||
|
placement.top! -= deltaY;
|
||||||
|
placement.height = event.height;
|
||||||
|
style.top = `${placement.top}px`;
|
||||||
|
style.height = `${placement.height}px`;
|
||||||
|
} else {
|
||||||
|
placement.height = event.height;
|
||||||
|
style.height = `${placement.height}px`;
|
||||||
|
}
|
||||||
|
} else if (dirTB === 1) {
|
||||||
|
// BOTTOM
|
||||||
|
if (anchor.bottom) {
|
||||||
|
placement.bottom! -= deltaY;
|
||||||
|
placement.height! = event.height;
|
||||||
|
style.bottom = `${placement.bottom}px`;
|
||||||
|
style.height = `${placement.height}px`;
|
||||||
|
} else {
|
||||||
|
placement.height! = event.height;
|
||||||
|
style.height = `${placement.height}px`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.width = event.width;
|
||||||
|
this.height = event.height;
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { DimensionContext } from 'app/features/dimensions';
|
|||||||
import { notFoundItem } from 'app/features/canvas/elements/notFound';
|
import { notFoundItem } from 'app/features/canvas/elements/notFound';
|
||||||
import { ElementState } from './element';
|
import { ElementState } from './element';
|
||||||
import { CanvasElementItem } from '../element';
|
import { CanvasElementItem } from '../element';
|
||||||
|
import { LayerActionID } from 'app/plugins/panel/canvas/types';
|
||||||
|
import { cloneDeep } from 'lodash';
|
||||||
|
|
||||||
export const groupItemDummy: CanvasElementItem = {
|
export const groupItemDummy: CanvasElementItem = {
|
||||||
id: 'group',
|
id: 'group',
|
||||||
@@ -19,7 +21,7 @@ export const groupItemDummy: CanvasElementItem = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class GroupState extends ElementState {
|
export class GroupState extends ElementState {
|
||||||
readonly elements: ElementState[] = [];
|
elements: ElementState[] = [];
|
||||||
|
|
||||||
constructor(public options: CanvasGroupOptions, public parent?: GroupState) {
|
constructor(public options: CanvasGroupOptions, public parent?: GroupState) {
|
||||||
super(groupItemDummy, options, parent);
|
super(groupItemDummy, options, parent);
|
||||||
@@ -35,14 +37,24 @@ export class GroupState extends ElementState {
|
|||||||
this.elements.push(new GroupState(c as CanvasGroupOptions, this));
|
this.elements.push(new GroupState(c as CanvasGroupOptions, this));
|
||||||
} else {
|
} else {
|
||||||
const item = canvasElementRegistry.getIfExists(c.type) ?? notFoundItem;
|
const item = canvasElementRegistry.getIfExists(c.type) ?? notFoundItem;
|
||||||
this.elements.push(new ElementState(item, c, parent));
|
this.elements.push(new ElementState(item, c, this));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isRoot() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// The parent size, need to set our own size based on offsets
|
// The parent size, need to set our own size based on offsets
|
||||||
updateSize(width: number, height: number) {
|
updateSize(width: number, height: number) {
|
||||||
super.updateSize(width, height);
|
super.updateSize(width, height);
|
||||||
|
if (!this.parent) {
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
this.sizeStyle.width = width;
|
||||||
|
this.sizeStyle.height = height;
|
||||||
|
}
|
||||||
|
|
||||||
// Update children with calculated size
|
// Update children with calculated size
|
||||||
for (const elem of this.elements) {
|
for (const elem of this.elements) {
|
||||||
@@ -62,6 +74,54 @@ export class GroupState extends ElementState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.onChange(this.getSaveModel());
|
||||||
|
}
|
||||||
|
|
||||||
|
// ??? or should this be on the element directly?
|
||||||
|
// are actions scoped to layers?
|
||||||
|
doAction = (action: LayerActionID, element: ElementState) => {
|
||||||
|
switch (action) {
|
||||||
|
case LayerActionID.Delete:
|
||||||
|
this.elements = this.elements.filter((e) => e !== element);
|
||||||
|
break;
|
||||||
|
case LayerActionID.Duplicate:
|
||||||
|
if (element.item.id === 'group') {
|
||||||
|
console.log('Can not duplicate groups (yet)', action, element);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const opts = cloneDeep(element.options);
|
||||||
|
if (element.anchor.top) {
|
||||||
|
opts.placement!.top! += 10;
|
||||||
|
}
|
||||||
|
if (element.anchor.left) {
|
||||||
|
opts.placement!.left! += 10;
|
||||||
|
}
|
||||||
|
if (element.anchor.bottom) {
|
||||||
|
opts.placement!.bottom! += 10;
|
||||||
|
}
|
||||||
|
if (element.anchor.right) {
|
||||||
|
opts.placement!.right! += 10;
|
||||||
|
}
|
||||||
|
console.log('DUPLICATE', opts);
|
||||||
|
const copy = new ElementState(element.item, opts, this);
|
||||||
|
copy.updateSize(element.width, element.height);
|
||||||
|
copy.updateData(element.data); // :bomb: <-- need some way to tell the scene to re-init size and data
|
||||||
|
this.elements.push(copy);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.log('DO action', action, element);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onChange(this.getSaveModel());
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div key={`${this.UID}/${this.revId}`} style={{ ...this.sizeStyle, ...this.dataStyle }}>
|
<div key={`${this.UID}/${this.revId}`} style={{ ...this.sizeStyle, ...this.dataStyle }}>
|
||||||
|
|||||||
37
public/app/features/canvas/runtime/root.tsx
Normal file
37
public/app/features/canvas/runtime/root.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { CanvasGroupOptions, CanvasElementOptions } from 'app/features/canvas';
|
||||||
|
import { GroupState } from './group';
|
||||||
|
|
||||||
|
export class RootElement extends GroupState {
|
||||||
|
constructor(public options: CanvasGroupOptions, private changeCallback: () => void) {
|
||||||
|
super(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
isRoot() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The parent size is always fullsize
|
||||||
|
updateSize(width: number, height: number) {
|
||||||
|
super.updateSize(width, height);
|
||||||
|
this.width = width;
|
||||||
|
this.height = height;
|
||||||
|
this.sizeStyle.width = width;
|
||||||
|
this.sizeStyle.height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
// root type can not change
|
||||||
|
onChange(options: CanvasElementOptions) {
|
||||||
|
this.revId++;
|
||||||
|
this.options = { ...options } as CanvasGroupOptions;
|
||||||
|
this.changeCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
getSaveModel() {
|
||||||
|
const { placement, anchor, ...rest } = this.options;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...rest, // everything except placement & anchor
|
||||||
|
elements: this.elements.map((v) => v.getSaveModel()),
|
||||||
|
} as CanvasGroupOptions;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,19 @@
|
|||||||
import React, { CSSProperties } from 'react';
|
import React, { CSSProperties } from 'react';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { ReplaySubject } from 'rxjs';
|
import { ReplaySubject, Subject } from 'rxjs';
|
||||||
import Moveable from 'moveable';
|
import Moveable from 'moveable';
|
||||||
import Selecto from 'selecto';
|
import Selecto from 'selecto';
|
||||||
|
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
import { GrafanaTheme2, PanelData } from '@grafana/data';
|
import { GrafanaTheme2, PanelData } from '@grafana/data';
|
||||||
import { stylesFactory } from '@grafana/ui';
|
import { stylesFactory } from '@grafana/ui';
|
||||||
import { CanvasElementOptions, CanvasGroupOptions, DEFAULT_CANVAS_ELEMENT_CONFIG } from 'app/features/canvas';
|
import {
|
||||||
|
Anchor,
|
||||||
|
CanvasElementOptions,
|
||||||
|
CanvasGroupOptions,
|
||||||
|
DEFAULT_CANVAS_ELEMENT_CONFIG,
|
||||||
|
Placement,
|
||||||
|
} from 'app/features/canvas';
|
||||||
import {
|
import {
|
||||||
ColorDimensionConfig,
|
ColorDimensionConfig,
|
||||||
ResourceDimensionConfig,
|
ResourceDimensionConfig,
|
||||||
@@ -21,20 +27,23 @@ import {
|
|||||||
getResourceDimensionFromData,
|
getResourceDimensionFromData,
|
||||||
getTextDimensionFromData,
|
getTextDimensionFromData,
|
||||||
} from 'app/features/dimensions/utils';
|
} from 'app/features/dimensions/utils';
|
||||||
import { GroupState } from './group';
|
|
||||||
import { ElementState } from './element';
|
import { ElementState } from './element';
|
||||||
|
import { RootElement } from './root';
|
||||||
|
|
||||||
export class Scene {
|
export class Scene {
|
||||||
private root: GroupState;
|
|
||||||
private lookup = new Map<number, ElementState>();
|
private lookup = new Map<number, ElementState>();
|
||||||
styles = getStyles(config.theme2);
|
styles = getStyles(config.theme2);
|
||||||
readonly selected = new ReplaySubject<ElementState | undefined>(undefined);
|
readonly selection = new ReplaySubject<ElementState[]>(1);
|
||||||
|
readonly moved = new Subject<number>(); // called after resize/drag for editor updates
|
||||||
|
root: RootElement;
|
||||||
|
|
||||||
revId = 0;
|
revId = 0;
|
||||||
|
|
||||||
width = 0;
|
width = 0;
|
||||||
height = 0;
|
height = 0;
|
||||||
style: CSSProperties = {};
|
style: CSSProperties = {};
|
||||||
data?: PanelData;
|
data?: PanelData;
|
||||||
|
selecto?: Selecto | null;
|
||||||
|
|
||||||
constructor(cfg: CanvasGroupOptions, public onSave: (cfg: CanvasGroupOptions) => void) {
|
constructor(cfg: CanvasGroupOptions, public onSave: (cfg: CanvasGroupOptions) => void) {
|
||||||
this.root = this.load(cfg);
|
this.root = this.load(cfg);
|
||||||
@@ -42,23 +51,20 @@ export class Scene {
|
|||||||
|
|
||||||
load(cfg: CanvasGroupOptions) {
|
load(cfg: CanvasGroupOptions) {
|
||||||
console.log('LOAD', cfg, this);
|
console.log('LOAD', cfg, this);
|
||||||
this.root = new GroupState(
|
this.root = new RootElement(
|
||||||
cfg ?? {
|
cfg ?? {
|
||||||
type: 'group',
|
type: 'group',
|
||||||
elements: [DEFAULT_CANVAS_ELEMENT_CONFIG],
|
elements: [DEFAULT_CANVAS_ELEMENT_CONFIG],
|
||||||
}
|
},
|
||||||
|
this.save // callback when changes are made
|
||||||
);
|
);
|
||||||
|
|
||||||
// Build the scene registry
|
// Build the scene registry
|
||||||
this.lookup.clear();
|
this.lookup.clear();
|
||||||
this.root.visit((v) => {
|
this.root.visit((v) => {
|
||||||
this.lookup.set(v.UID, v);
|
this.lookup.set(v.UID, v);
|
||||||
|
|
||||||
// HACK! select the first/only item
|
|
||||||
if (v.item.id !== 'group') {
|
|
||||||
this.selected.next(v);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.root;
|
return this.root;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,10 +98,47 @@ export class Scene {
|
|||||||
this.save();
|
this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
save() {
|
toggleAnchor(element: ElementState, k: keyof Anchor) {
|
||||||
this.onSave(this.root.getSaveModel());
|
console.log('TODO, smarter toggle', element.UID, element.anchor, k);
|
||||||
|
const { div } = element;
|
||||||
|
if (!div) {
|
||||||
|
console.log('Not ready');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const w = element.parent?.width ?? 100;
|
||||||
|
const h = element.parent?.height ?? 100;
|
||||||
|
|
||||||
|
// Get computed position....
|
||||||
|
const info = div.getBoundingClientRect(); // getElementInfo(div, element.parent?.div);
|
||||||
|
console.log('DIV info', div);
|
||||||
|
|
||||||
|
const placement: Placement = {
|
||||||
|
top: info.top,
|
||||||
|
left: info.left,
|
||||||
|
width: info.width,
|
||||||
|
height: info.height,
|
||||||
|
bottom: h - info.bottom,
|
||||||
|
right: w - info.right,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('PPP', placement);
|
||||||
|
|
||||||
|
// // TODO: needs to recalculate placement based on absolute values...
|
||||||
|
// element.anchor[k] = !Boolean(element.anchor[k]);
|
||||||
|
// element.placement = placement;
|
||||||
|
// element.validatePlacement();
|
||||||
|
// element.revId++;
|
||||||
|
// this.revId++;
|
||||||
|
// this.save();
|
||||||
|
|
||||||
|
this.moved.next(Date.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
save = () => {
|
||||||
|
this.onSave(this.root.getSaveModel());
|
||||||
|
};
|
||||||
|
|
||||||
private findElementByTarget = (target: HTMLElement | SVGElement): ElementState | undefined => {
|
private findElementByTarget = (target: HTMLElement | SVGElement): ElementState | undefined => {
|
||||||
return this.root.elements.find((element) => element.div === target);
|
return this.root.elements.find((element) => element.div === target);
|
||||||
};
|
};
|
||||||
@@ -106,9 +149,10 @@ export class Scene {
|
|||||||
targetElements.push(element.div!);
|
targetElements.push(element.div!);
|
||||||
});
|
});
|
||||||
|
|
||||||
const selecto = new Selecto({
|
this.selecto = new Selecto({
|
||||||
container: sceneContainer,
|
container: sceneContainer,
|
||||||
selectableTargets: targetElements,
|
selectableTargets: targetElements,
|
||||||
|
selectByClick: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
const moveable = new Moveable(sceneContainer, {
|
const moveable = new Moveable(sceneContainer, {
|
||||||
@@ -116,63 +160,67 @@ export class Scene {
|
|||||||
resizable: true,
|
resizable: true,
|
||||||
})
|
})
|
||||||
.on('clickGroup', (event) => {
|
.on('clickGroup', (event) => {
|
||||||
selecto.clickTarget(event.inputEvent, event.inputTarget);
|
this.selecto!.clickTarget(event.inputEvent, event.inputTarget);
|
||||||
})
|
})
|
||||||
.on('drag', (event) => {
|
.on('drag', (event) => {
|
||||||
const targetedElement = this.findElementByTarget(event.target);
|
const targetedElement = this.findElementByTarget(event.target);
|
||||||
targetedElement!.applyDrag(event);
|
targetedElement!.applyDrag(event);
|
||||||
|
this.moved.next(Date.now()); // TODO only on end
|
||||||
})
|
})
|
||||||
.on('dragGroup', (e) => {
|
.on('dragGroup', (e) => {
|
||||||
e.events.forEach((event) => {
|
e.events.forEach((event) => {
|
||||||
const targetedElement = this.findElementByTarget(event.target);
|
const targetedElement = this.findElementByTarget(event.target);
|
||||||
targetedElement!.applyDrag(event);
|
targetedElement!.applyDrag(event);
|
||||||
});
|
});
|
||||||
|
this.moved.next(Date.now()); // TODO only on end
|
||||||
})
|
})
|
||||||
.on('resize', (event) => {
|
.on('resize', (event) => {
|
||||||
const targetedElement = this.findElementByTarget(event.target);
|
const targetedElement = this.findElementByTarget(event.target);
|
||||||
targetedElement!.applyResize(event);
|
targetedElement!.applyResize(event);
|
||||||
|
this.moved.next(Date.now()); // TODO only on end
|
||||||
})
|
})
|
||||||
.on('resizeGroup', (e) => {
|
.on('resizeGroup', (e) => {
|
||||||
e.events.forEach((event) => {
|
e.events.forEach((event) => {
|
||||||
const targetedElement = this.findElementByTarget(event.target);
|
const targetedElement = this.findElementByTarget(event.target);
|
||||||
targetedElement!.applyResize(event);
|
targetedElement!.applyResize(event);
|
||||||
});
|
});
|
||||||
|
this.moved.next(Date.now()); // TODO only on end
|
||||||
});
|
});
|
||||||
|
|
||||||
let targets: Array<HTMLElement | SVGElement> = [];
|
let targets: Array<HTMLElement | SVGElement> = [];
|
||||||
selecto
|
this.selecto!.on('dragStart', (event) => {
|
||||||
.on('dragStart', (event) => {
|
const selectedTarget = event.inputEvent.target;
|
||||||
const selectedTarget = event.inputEvent.target;
|
|
||||||
|
|
||||||
const isTargetMoveableElement =
|
const isTargetMoveableElement =
|
||||||
moveable.isMoveableElement(selectedTarget) ||
|
moveable.isMoveableElement(selectedTarget) ||
|
||||||
targets.some((target) => target === selectedTarget || target.contains(selectedTarget));
|
targets.some((target) => target === selectedTarget || target.contains(selectedTarget));
|
||||||
|
|
||||||
if (isTargetMoveableElement) {
|
if (isTargetMoveableElement) {
|
||||||
// Prevent drawing selection box when selected target is a moveable element
|
// Prevent drawing selection box when selected target is a moveable element
|
||||||
event.stop();
|
event.stop();
|
||||||
}
|
}
|
||||||
})
|
}).on('selectEnd', (event) => {
|
||||||
.on('selectEnd', (event) => {
|
targets = event.selected;
|
||||||
targets = event.selected;
|
moveable.target = targets;
|
||||||
moveable.target = targets;
|
|
||||||
|
|
||||||
if (event.isDragStart) {
|
const s = event.selected.map((t) => this.findElementByTarget(t)!);
|
||||||
event.inputEvent.preventDefault();
|
this.selection.next(s);
|
||||||
|
console.log('UPDATE selection', s);
|
||||||
|
|
||||||
setTimeout(() => {
|
if (event.isDragStart) {
|
||||||
moveable.dragStart(event.inputEvent);
|
event.inputEvent.preventDefault();
|
||||||
});
|
|
||||||
}
|
setTimeout(() => {
|
||||||
});
|
moveable.dragStart(event.inputEvent);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div key={this.revId} className={this.styles.wrap} style={this.style} ref={this.initMoveable}>
|
||||||
<div key={this.revId} className={this.styles.wrap} style={this.style} ref={this.initMoveable}>
|
{this.root.render()}
|
||||||
{this.root.render()}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { CanvasGroupOptions } from 'app/features/canvas';
|
|||||||
import { Scene } from 'app/features/canvas/runtime/scene';
|
import { Scene } from 'app/features/canvas/runtime/scene';
|
||||||
import { PanelContext, PanelContextRoot } from '@grafana/ui';
|
import { PanelContext, PanelContextRoot } from '@grafana/ui';
|
||||||
import { ElementState } from 'app/features/canvas/runtime/element';
|
import { ElementState } from 'app/features/canvas/runtime/element';
|
||||||
|
import { GroupState } from 'app/features/canvas/runtime/group';
|
||||||
|
|
||||||
interface Props extends PanelProps<PanelOptions> {}
|
interface Props extends PanelProps<PanelOptions> {}
|
||||||
|
|
||||||
@@ -16,7 +17,8 @@ interface State {
|
|||||||
|
|
||||||
export interface InstanceState {
|
export interface InstanceState {
|
||||||
scene: Scene;
|
scene: Scene;
|
||||||
selected?: ElementState;
|
selected: ElementState[];
|
||||||
|
layer: GroupState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CanvasPanel extends Component<Props, State> {
|
export class CanvasPanel extends Component<Props, State> {
|
||||||
@@ -53,14 +55,16 @@ export class CanvasPanel extends Component<Props, State> {
|
|||||||
if (this.panelContext.onInstanceStateChange && this.panelContext.app === CoreApp.PanelEditor) {
|
if (this.panelContext.onInstanceStateChange && this.panelContext.app === CoreApp.PanelEditor) {
|
||||||
this.panelContext.onInstanceStateChange({
|
this.panelContext.onInstanceStateChange({
|
||||||
scene: this.scene,
|
scene: this.scene,
|
||||||
|
layer: this.scene.root,
|
||||||
});
|
});
|
||||||
|
|
||||||
this.subs.add(
|
this.subs.add(
|
||||||
this.scene.selected.subscribe({
|
this.scene.selection.subscribe({
|
||||||
next: (v) => {
|
next: (v) => {
|
||||||
this.panelContext.onInstanceStateChange!({
|
this.panelContext.onInstanceStateChange!({
|
||||||
scene: this.scene,
|
scene: this.scene,
|
||||||
selected: v,
|
selected: v,
|
||||||
|
layer: this.scene.root,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,206 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { css, cx } from '@emotion/css';
|
||||||
|
import { Button, Container, Icon, IconButton, stylesFactory, ValuePicker } from '@grafana/ui';
|
||||||
|
import { GrafanaTheme, SelectableValue, StandardEditorProps } from '@grafana/data';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
|
||||||
|
|
||||||
|
import { PanelOptions } from '../models.gen';
|
||||||
|
import { InstanceState } from '../CanvasPanel';
|
||||||
|
import { LayerActionID } from '../types';
|
||||||
|
import { canvasElementRegistry } from 'app/features/canvas';
|
||||||
|
|
||||||
|
type Props = StandardEditorProps<any, InstanceState, PanelOptions>;
|
||||||
|
|
||||||
|
export class LayerElementListEditor extends PureComponent<Props> {
|
||||||
|
style = getStyles(config.theme);
|
||||||
|
|
||||||
|
onAddItem = (sel: SelectableValue<string>) => {
|
||||||
|
// const reg = drawItemsRegistry.getIfExists(sel.value);
|
||||||
|
// if (!reg) {
|
||||||
|
// console.error('NOT FOUND', sel);
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
// const layer = this.props.value;
|
||||||
|
// const item = newItem(reg, layer.items.length);
|
||||||
|
// const isList = this.props.context.options?.mode === LayoutMode.List;
|
||||||
|
// const items = isList ? [item, ...layer.items] : [...layer.items, item];
|
||||||
|
// this.props.onChange({
|
||||||
|
// ...layer,
|
||||||
|
// items,
|
||||||
|
// });
|
||||||
|
// this.onSelect(item);
|
||||||
|
};
|
||||||
|
|
||||||
|
onSelect = (item: any) => {
|
||||||
|
const { settings } = this.props.item;
|
||||||
|
|
||||||
|
if (settings?.scene && settings?.scene?.selecto) {
|
||||||
|
settings.scene.selecto.clickTarget(item, item?.div);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getRowStyle = (sel: boolean) => {
|
||||||
|
return sel ? `${this.style.row} ${this.style.sel}` : this.style.row;
|
||||||
|
};
|
||||||
|
|
||||||
|
onDragEnd = (result: DropResult) => {
|
||||||
|
if (!result.destination) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { settings } = this.props.item;
|
||||||
|
if (!settings?.layer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { layer } = settings;
|
||||||
|
|
||||||
|
const count = layer.elements.length - 1;
|
||||||
|
const src = (result.source.index - count) * -1;
|
||||||
|
const dst = (result.destination.index - count) * -1;
|
||||||
|
|
||||||
|
layer.reorder(src, dst);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const settings = this.props.item.settings;
|
||||||
|
if (!settings) {
|
||||||
|
return <div>No settings</div>;
|
||||||
|
}
|
||||||
|
const layer = settings.layer;
|
||||||
|
if (!layer) {
|
||||||
|
return <div>Missing layer?</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const styles = this.style;
|
||||||
|
const selection: number[] = settings.selected ? settings.selected.map((v) => v.UID) : [];
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<DragDropContext onDragEnd={this.onDragEnd}>
|
||||||
|
<Droppable droppableId="droppable">
|
||||||
|
{(provided, snapshot) => (
|
||||||
|
<div {...provided.droppableProps} ref={provided.innerRef}>
|
||||||
|
{(() => {
|
||||||
|
// reverse order
|
||||||
|
const rows: any = [];
|
||||||
|
for (let i = layer.elements.length - 1; i >= 0; i--) {
|
||||||
|
const element = layer.elements[i];
|
||||||
|
rows.push(
|
||||||
|
<Draggable key={element.UID} draggableId={`${element.UID}`} index={rows.length}>
|
||||||
|
{(provided, snapshot) => (
|
||||||
|
<div
|
||||||
|
className={this.getRowStyle(selection.includes(element.UID))}
|
||||||
|
ref={provided.innerRef}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
onMouseDown={() => this.onSelect(element)}
|
||||||
|
>
|
||||||
|
<span className={styles.typeWrapper}>{element.item.name}</span>
|
||||||
|
<div className={styles.textWrapper}>
|
||||||
|
{element.UID} ({i})
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
name="copy"
|
||||||
|
title={'duplicate'}
|
||||||
|
className={styles.actionIcon}
|
||||||
|
onClick={() => layer.doAction(LayerActionID.Duplicate, element)}
|
||||||
|
surface="header"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
name="trash-alt"
|
||||||
|
title={'remove'}
|
||||||
|
className={cx(styles.actionIcon, styles.dragIcon)}
|
||||||
|
onClick={() => layer.doAction(LayerActionID.Delete, element)}
|
||||||
|
surface="header"
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
title="Drag and drop to reorder"
|
||||||
|
name="draggabledots"
|
||||||
|
size="lg"
|
||||||
|
className={styles.dragIcon}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Draggable>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
})()}
|
||||||
|
|
||||||
|
{provided.placeholder}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Droppable>
|
||||||
|
</DragDropContext>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<Container>
|
||||||
|
<ValuePicker
|
||||||
|
icon="plus"
|
||||||
|
label="Add item"
|
||||||
|
variant="secondary"
|
||||||
|
options={canvasElementRegistry.selectOptions().options}
|
||||||
|
onChange={this.onAddItem}
|
||||||
|
isFullWidth={false}
|
||||||
|
/>
|
||||||
|
{selection.length > 0 && (
|
||||||
|
<Button size="sm" variant="secondary" onClick={() => console.log('TODO!')}>
|
||||||
|
Clear Selection
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Container>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
|
||||||
|
wrapper: css`
|
||||||
|
margin-bottom: ${theme.spacing.md};
|
||||||
|
`,
|
||||||
|
row: css`
|
||||||
|
padding: ${theme.spacing.xs} ${theme.spacing.sm};
|
||||||
|
border-radius: ${theme.border.radius.sm};
|
||||||
|
background: ${theme.colors.bg2};
|
||||||
|
min-height: ${theme.spacing.formInputHeight}px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
border: 1px solid ${theme.colors.formInputBorder};
|
||||||
|
&:hover {
|
||||||
|
border: 1px solid ${theme.colors.formInputBorderHover};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
sel: css`
|
||||||
|
border: 1px solid ${theme.colors.formInputBorderActive};
|
||||||
|
&:hover {
|
||||||
|
border: 1px solid ${theme.colors.formInputBorderActive};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
dragIcon: css`
|
||||||
|
cursor: drag;
|
||||||
|
`,
|
||||||
|
actionIcon: css`
|
||||||
|
color: ${theme.colors.textWeak};
|
||||||
|
&:hover {
|
||||||
|
color: ${theme.colors.text};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
typeWrapper: css`
|
||||||
|
color: ${theme.colors.textBlue};
|
||||||
|
margin-right: 5px;
|
||||||
|
`,
|
||||||
|
textWrapper: css`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-right: ${theme.spacing.sm};
|
||||||
|
`,
|
||||||
|
}));
|
||||||
66
public/app/plugins/panel/canvas/editor/PlacementEditor.tsx
Normal file
66
public/app/plugins/panel/canvas/editor/PlacementEditor.tsx
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import React, { FC } from 'react';
|
||||||
|
import { Button, Field, HorizontalGroup, InlineField, InlineFieldRow } from '@grafana/ui';
|
||||||
|
import { StandardEditorProps } from '@grafana/data';
|
||||||
|
|
||||||
|
import { PanelOptions } from '../models.gen';
|
||||||
|
import { useObservable } from 'react-use';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { CanvasEditorOptions } from './elementEditor';
|
||||||
|
import { Anchor, Placement } from 'app/features/canvas';
|
||||||
|
import { NumberInput } from 'app/features/dimensions/editors/NumberInput';
|
||||||
|
|
||||||
|
const anchors: Array<keyof Anchor> = ['top', 'left', 'bottom', 'right'];
|
||||||
|
const places: Array<keyof Placement> = ['top', 'left', 'bottom', 'right', 'width', 'height'];
|
||||||
|
|
||||||
|
export const PlacementEditor: FC<StandardEditorProps<any, CanvasEditorOptions, PanelOptions>> = ({ item }) => {
|
||||||
|
const settings = item.settings;
|
||||||
|
|
||||||
|
// Will force a rerender whenever the subject changes
|
||||||
|
useObservable(settings?.scene ? settings.scene.moved : new Subject());
|
||||||
|
|
||||||
|
if (!settings) {
|
||||||
|
return <div>Loading...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = settings.element;
|
||||||
|
if (!element) {
|
||||||
|
return <div>???</div>;
|
||||||
|
}
|
||||||
|
const { placement } = element;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<HorizontalGroup>
|
||||||
|
{anchors.map((a) => (
|
||||||
|
<Button
|
||||||
|
key={a}
|
||||||
|
size="sm"
|
||||||
|
variant={element.anchor[a] ? 'primary' : 'secondary'}
|
||||||
|
onClick={() => settings.scene.toggleAnchor(element, a)}
|
||||||
|
>
|
||||||
|
{a}
|
||||||
|
</Button>
|
||||||
|
))}
|
||||||
|
</HorizontalGroup>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<Field label="Position">
|
||||||
|
<>
|
||||||
|
{places.map((p) => {
|
||||||
|
const v = placement[p];
|
||||||
|
if (v == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<InlineFieldRow key={p}>
|
||||||
|
<InlineField label={p} labelWidth={8} grow={true}>
|
||||||
|
<NumberInput value={v} onChange={(v) => console.log('TODO, edit!!!', p, v)} />
|
||||||
|
</InlineField>
|
||||||
|
</InlineFieldRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -5,6 +5,7 @@ import { NestedPanelOptions, NestedValueAccess } from '@grafana/data/src/utils/O
|
|||||||
import { setOptionImmutably } from 'app/features/dashboard/components/PanelEditor/utils';
|
import { setOptionImmutably } from 'app/features/dashboard/components/PanelEditor/utils';
|
||||||
import { ElementState } from 'app/features/canvas/runtime/element';
|
import { ElementState } from 'app/features/canvas/runtime/element';
|
||||||
import { Scene } from 'app/features/canvas/runtime/scene';
|
import { Scene } from 'app/features/canvas/runtime/scene';
|
||||||
|
import { PlacementEditor } from './PlacementEditor';
|
||||||
|
|
||||||
export interface CanvasEditorOptions {
|
export interface CanvasEditorOptions {
|
||||||
element: ElementState;
|
element: ElementState;
|
||||||
@@ -44,6 +45,8 @@ export function getElementEditor(opts: CanvasEditorOptions): NestedPanelOptions<
|
|||||||
|
|
||||||
// Dynamically fill the selected element
|
// Dynamically fill the selected element
|
||||||
build: (builder, context) => {
|
build: (builder, context) => {
|
||||||
|
console.log('MAKE element editor', opts.element.UID);
|
||||||
|
|
||||||
const { options } = opts.element;
|
const { options } = opts.element;
|
||||||
const layerTypes = canvasElementRegistry.selectOptions(
|
const layerTypes = canvasElementRegistry.selectOptions(
|
||||||
options?.type // the selected value
|
options?.type // the selected value
|
||||||
@@ -70,6 +73,15 @@ export function getElementEditor(opts: CanvasEditorOptions): NestedPanelOptions<
|
|||||||
|
|
||||||
optionBuilder.addBackground(builder, ctx);
|
optionBuilder.addBackground(builder, ctx);
|
||||||
optionBuilder.addBorder(builder, ctx);
|
optionBuilder.addBorder(builder, ctx);
|
||||||
|
|
||||||
|
builder.addCustomEditor({
|
||||||
|
category: ['Layout'],
|
||||||
|
id: 'content',
|
||||||
|
path: '__', // not used
|
||||||
|
name: 'Anchor',
|
||||||
|
editor: PlacementEditor,
|
||||||
|
settings: opts,
|
||||||
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
56
public/app/plugins/panel/canvas/editor/layerEditor.tsx
Normal file
56
public/app/plugins/panel/canvas/editor/layerEditor.tsx
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import { get as lodashGet } from 'lodash';
|
||||||
|
import { optionBuilder } from './options';
|
||||||
|
import { NestedPanelOptions, NestedValueAccess } from '@grafana/data/src/utils/OptionsUIBuilders';
|
||||||
|
import { setOptionImmutably } from 'app/features/dashboard/components/PanelEditor/utils';
|
||||||
|
import { InstanceState } from '../CanvasPanel';
|
||||||
|
import { LayerElementListEditor } from './LayerElementListEditor';
|
||||||
|
|
||||||
|
export function getLayerEditor(opts: InstanceState): NestedPanelOptions<InstanceState> {
|
||||||
|
const { layer, scene } = opts;
|
||||||
|
const options = layer.options || { elements: [] };
|
||||||
|
|
||||||
|
return {
|
||||||
|
category: ['Layer'],
|
||||||
|
path: '--', // not used!
|
||||||
|
|
||||||
|
// Note that canvas editor writes things to the scene!
|
||||||
|
values: (parent: NestedValueAccess) => ({
|
||||||
|
getValue: (path: string) => {
|
||||||
|
return lodashGet(options, path);
|
||||||
|
},
|
||||||
|
onChange: (path: string, value: any) => {
|
||||||
|
if (path === 'type' && value) {
|
||||||
|
console.warn('unable to change layer type');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const c = setOptionImmutably(options, path, value);
|
||||||
|
scene.onChange(layer.UID, c);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Dynamically fill the selected element
|
||||||
|
build: (builder, context) => {
|
||||||
|
console.log('MAKE layer editor', layer.UID);
|
||||||
|
|
||||||
|
builder.addCustomEditor({
|
||||||
|
id: 'content',
|
||||||
|
path: 'root',
|
||||||
|
name: 'Elements',
|
||||||
|
editor: LayerElementListEditor,
|
||||||
|
settings: opts,
|
||||||
|
});
|
||||||
|
|
||||||
|
// // force clean layer configuration
|
||||||
|
// const layer = canvasElementRegistry.getIfExists(options?.type ?? DEFAULT_CANVAS_ELEMENT_CONFIG.type)!;
|
||||||
|
//const currentOptions = { ...options, type: layer.id, config: { ...layer.defaultConfig, ...options?.config } };
|
||||||
|
const ctx = { ...context, options };
|
||||||
|
|
||||||
|
// if (layer.registerOptionsUI) {
|
||||||
|
// layer.registerOptionsUI(builder, ctx);
|
||||||
|
// }
|
||||||
|
|
||||||
|
optionBuilder.addBackground(builder as any, ctx);
|
||||||
|
optionBuilder.addBorder(builder as any, ctx);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ import { PanelPlugin } from '@grafana/data';
|
|||||||
import { CanvasPanel, InstanceState } from './CanvasPanel';
|
import { CanvasPanel, InstanceState } from './CanvasPanel';
|
||||||
import { PanelOptions } from './models.gen';
|
import { PanelOptions } from './models.gen';
|
||||||
import { getElementEditor } from './editor/elementEditor';
|
import { getElementEditor } from './editor/elementEditor';
|
||||||
|
import { getLayerEditor } from './editor/layerEditor';
|
||||||
|
|
||||||
export const plugin = new PanelPlugin<PanelOptions>(CanvasPanel)
|
export const plugin = new PanelPlugin<PanelOptions>(CanvasPanel)
|
||||||
.setNoPadding() // extend to panel edges
|
.setNoPadding() // extend to panel edges
|
||||||
@@ -17,13 +18,20 @@ export const plugin = new PanelPlugin<PanelOptions>(CanvasPanel)
|
|||||||
defaultValue: true,
|
defaultValue: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (state?.selected) {
|
if (state) {
|
||||||
builder.addNestedOptions(
|
const selection = state.selected;
|
||||||
getElementEditor({
|
if (selection?.length === 1) {
|
||||||
category: ['Selected element'],
|
builder.addNestedOptions(
|
||||||
element: state.selected,
|
getElementEditor({
|
||||||
scene: state.scene,
|
category: [`Selected element (id: ${selection[0].UID})`], // changing the ID forces are reload
|
||||||
})
|
element: selection[0],
|
||||||
);
|
scene: state.scene,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log('NO Single seleciton', selection?.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.addNestedOptions(getLayerEditor(state));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
6
public/app/plugins/panel/canvas/types.ts
Normal file
6
public/app/plugins/panel/canvas/types.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export enum LayerActionID {
|
||||||
|
Delete = 'delete',
|
||||||
|
Duplicate = 'duplicate',
|
||||||
|
MoveTop = 'move-top',
|
||||||
|
MoveBottom = 'move-bottom',
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user