mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Canvas: use name as UID (#42696)
This commit is contained in:
parent
a345b06095
commit
2af5ad4764
@ -50,6 +50,7 @@ export const LayerDragDropList = <T extends LayerElement>({
|
|||||||
// reverse order
|
// reverse order
|
||||||
const rows: any = [];
|
const rows: any = [];
|
||||||
const lastLayerIndex = excludeBaseLayer ? 1 : 0;
|
const lastLayerIndex = excludeBaseLayer ? 1 : 0;
|
||||||
|
const shouldRenderDragIconLengthThreshold = excludeBaseLayer ? 2 : 1;
|
||||||
for (let i = layers.length - 1; i >= lastLayerIndex; i--) {
|
for (let i = layers.length - 1; i >= lastLayerIndex; i--) {
|
||||||
const element = layers[i];
|
const element = layers[i];
|
||||||
const uid = element.getName();
|
const uid = element.getName();
|
||||||
@ -91,7 +92,7 @@ export const LayerDragDropList = <T extends LayerElement>({
|
|||||||
onClick={() => onDelete(element)}
|
onClick={() => onDelete(element)}
|
||||||
surface="header"
|
surface="header"
|
||||||
/>
|
/>
|
||||||
{layers.length > 2 && (
|
{layers.length > shouldRenderDragIconLengthThreshold && (
|
||||||
<Icon
|
<Icon
|
||||||
title="Drag and drop to reorder"
|
title="Drag and drop to reorder"
|
||||||
name="draggabledots"
|
name="draggabledots"
|
||||||
|
@ -40,7 +40,7 @@ export const LayerName = ({ name, onChange, verifyLayerNameUniqueness }: LayerNa
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (verifyLayerNameUniqueness && !verifyLayerNameUniqueness(newName)) {
|
if (verifyLayerNameUniqueness && !verifyLayerNameUniqueness(newName) && newName !== name) {
|
||||||
setValidationError('Layer name already exists');
|
setValidationError('Layer name already exists');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ import { textBoxItem } from './elements/textBox';
|
|||||||
export const DEFAULT_CANVAS_ELEMENT_CONFIG: CanvasElementOptions = {
|
export const DEFAULT_CANVAS_ELEMENT_CONFIG: CanvasElementOptions = {
|
||||||
...iconItem.getNewOptions(),
|
...iconItem.getNewOptions(),
|
||||||
type: iconItem.id,
|
type: iconItem.id,
|
||||||
name: `Group ${Date.now()}.${Math.floor(Math.random() * 100)}`,
|
name: `Element 1`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const canvasElementRegistry = new Registry<CanvasElementItem>(() => [
|
export const canvasElementRegistry = new Registry<CanvasElementItem>(() => [
|
||||||
|
@ -13,12 +13,13 @@ 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';
|
||||||
import { LayerElement } from 'app/core/components/Layers/types';
|
import { LayerElement } from 'app/core/components/Layers/types';
|
||||||
|
import { Scene } from './scene';
|
||||||
|
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
|
|
||||||
export class ElementState implements LayerElement {
|
export class ElementState implements LayerElement {
|
||||||
|
// UID necessary for moveable to work (for now)
|
||||||
readonly UID = counter++;
|
readonly UID = counter++;
|
||||||
|
|
||||||
revId = 0;
|
revId = 0;
|
||||||
sizeStyle: CSSProperties = {};
|
sizeStyle: CSSProperties = {};
|
||||||
dataStyle: CSSProperties = {};
|
dataStyle: CSSProperties = {};
|
||||||
@ -36,17 +37,34 @@ export class ElementState implements LayerElement {
|
|||||||
placement: Placement;
|
placement: Placement;
|
||||||
|
|
||||||
constructor(public item: CanvasElementItem, public options: CanvasElementOptions, public parent?: GroupState) {
|
constructor(public item: CanvasElementItem, public options: CanvasElementOptions, public parent?: GroupState) {
|
||||||
|
const fallbackName = `Element ${Date.now()}`;
|
||||||
if (!options) {
|
if (!options) {
|
||||||
this.options = { type: item.id, name: `Element ${this.UID}` };
|
this.options = { type: item.id, name: fallbackName };
|
||||||
}
|
}
|
||||||
this.anchor = options.anchor ?? {};
|
this.anchor = options.anchor ?? {};
|
||||||
this.placement = options.placement ?? {};
|
this.placement = options.placement ?? {};
|
||||||
options.anchor = this.anchor;
|
options.anchor = this.anchor;
|
||||||
options.placement = this.placement;
|
options.placement = this.placement;
|
||||||
|
|
||||||
|
const scene = this.getScene();
|
||||||
if (!options.name) {
|
if (!options.name) {
|
||||||
options.name = `Element ${this.UID}`;
|
const newName = scene?.getNextElementName();
|
||||||
|
options.name = newName ?? fallbackName;
|
||||||
}
|
}
|
||||||
|
scene?.byName.set(options.name, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getScene(): Scene | undefined {
|
||||||
|
let trav = this.parent;
|
||||||
|
while (trav) {
|
||||||
|
if (trav.isRoot()) {
|
||||||
|
return trav.scene;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
trav = trav.parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
@ -106,8 +124,6 @@ export class ElementState implements LayerElement {
|
|||||||
|
|
||||||
this.options.anchor = this.anchor;
|
this.options.anchor = this.anchor;
|
||||||
this.options.placement = this.placement;
|
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
|
||||||
@ -191,6 +207,10 @@ export class ElementState implements LayerElement {
|
|||||||
this.item = canvasElementRegistry.getIfExists(options.type) ?? notFoundItem;
|
this.item = canvasElementRegistry.getIfExists(options.type) ?? notFoundItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// rename handling
|
||||||
|
const oldName = this.options.name;
|
||||||
|
const newName = options.name;
|
||||||
|
|
||||||
this.revId++;
|
this.revId++;
|
||||||
this.options = { ...options };
|
this.options = { ...options };
|
||||||
let trav = this.parent;
|
let trav = this.parent;
|
||||||
@ -202,6 +222,12 @@ export class ElementState implements LayerElement {
|
|||||||
trav.revId++;
|
trav.revId++;
|
||||||
trav = trav.parent;
|
trav = trav.parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const scene = this.getScene();
|
||||||
|
if (oldName !== newName && scene) {
|
||||||
|
scene.byName.delete(oldName);
|
||||||
|
scene.byName.set(newName, this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getSaveModel() {
|
getSaveModel() {
|
||||||
|
@ -103,6 +103,7 @@ export class GroupState extends ElementState {
|
|||||||
switch (action) {
|
switch (action) {
|
||||||
case LayerActionID.Delete:
|
case LayerActionID.Delete:
|
||||||
this.elements = this.elements.filter((e) => e !== element);
|
this.elements = this.elements.filter((e) => e !== element);
|
||||||
|
this.scene.byName.delete(element.options.name);
|
||||||
this.scene.save();
|
this.scene.save();
|
||||||
this.reinitializeMoveable();
|
this.reinitializeMoveable();
|
||||||
break;
|
break;
|
||||||
@ -129,9 +130,10 @@ export class GroupState extends ElementState {
|
|||||||
copy.updateSize(element.width, element.height);
|
copy.updateSize(element.width, element.height);
|
||||||
copy.updateData(this.scene.context);
|
copy.updateData(this.scene.context);
|
||||||
if (updateName) {
|
if (updateName) {
|
||||||
copy.options.name = `Element ${copy.UID} (duplicate)`;
|
copy.options.name = this.scene.getNextElementName();
|
||||||
}
|
}
|
||||||
this.elements.push(copy);
|
this.elements.push(copy);
|
||||||
|
this.scene.byName.set(copy.options.name, copy);
|
||||||
this.scene.save();
|
this.scene.save();
|
||||||
this.reinitializeMoveable();
|
this.reinitializeMoveable();
|
||||||
break;
|
break;
|
||||||
|
@ -36,6 +36,7 @@ export class Scene {
|
|||||||
styles = getStyles(config.theme2);
|
styles = getStyles(config.theme2);
|
||||||
readonly selection = new ReplaySubject<ElementState[]>(1);
|
readonly selection = new ReplaySubject<ElementState[]>(1);
|
||||||
readonly moved = new Subject<number>(); // called after resize/drag for editor updates
|
readonly moved = new Subject<number>(); // called after resize/drag for editor updates
|
||||||
|
readonly byName = new Map<string, ElementState>();
|
||||||
root: RootElement;
|
root: RootElement;
|
||||||
|
|
||||||
revId = 0;
|
revId = 0;
|
||||||
@ -53,6 +54,25 @@ export class Scene {
|
|||||||
this.root = this.load(cfg, enableEditing);
|
this.root = this.load(cfg, enableEditing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getNextElementName = (isGroup = false) => {
|
||||||
|
const label = isGroup ? 'Group' : 'Element';
|
||||||
|
let idx = this.byName.size + 1;
|
||||||
|
|
||||||
|
const max = idx + 100;
|
||||||
|
while (true && idx < max) {
|
||||||
|
const name = `${label} ${idx++}`;
|
||||||
|
if (!this.byName.has(name)) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${label} ${Date.now()}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
canRename = (v: string) => {
|
||||||
|
return !this.byName.has(v);
|
||||||
|
};
|
||||||
|
|
||||||
load(cfg: CanvasGroupOptions, enableEditing: boolean) {
|
load(cfg: CanvasGroupOptions, enableEditing: boolean) {
|
||||||
this.root = new RootElement(
|
this.root = new RootElement(
|
||||||
cfg ?? {
|
cfg ?? {
|
||||||
@ -103,7 +123,7 @@ export class Scene {
|
|||||||
const newLayer = new GroupState(
|
const newLayer = new GroupState(
|
||||||
{
|
{
|
||||||
type: 'group',
|
type: 'group',
|
||||||
name: `Group ${Date.now()}.${Math.floor(Math.random() * 100)}`,
|
name: this.getNextElementName(true),
|
||||||
elements: [],
|
elements: [],
|
||||||
},
|
},
|
||||||
this,
|
this,
|
||||||
@ -111,12 +131,14 @@ export class Scene {
|
|||||||
);
|
);
|
||||||
|
|
||||||
currentSelectedElements.forEach((element: ElementState) => {
|
currentSelectedElements.forEach((element: ElementState) => {
|
||||||
newLayer.doAction(LayerActionID.Duplicate, element, false);
|
|
||||||
currentLayer.doAction(LayerActionID.Delete, element);
|
currentLayer.doAction(LayerActionID.Delete, element);
|
||||||
|
newLayer.doAction(LayerActionID.Duplicate, element, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
currentLayer.elements.push(newLayer);
|
currentLayer.elements.push(newLayer);
|
||||||
|
|
||||||
|
this.byName.set(newLayer.getName(), newLayer);
|
||||||
|
|
||||||
this.save();
|
this.save();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -133,7 +155,6 @@ export class Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleAnchor(element: ElementState, k: keyof Anchor) {
|
toggleAnchor(element: ElementState, k: keyof Anchor) {
|
||||||
console.log('TODO, smarter toggle', element.UID, element.anchor, k);
|
|
||||||
const { div } = element;
|
const { div } = element;
|
||||||
if (!div) {
|
if (!div) {
|
||||||
console.log('Not ready');
|
console.log('Not ready');
|
||||||
|
@ -126,10 +126,10 @@ export class LayerElementListEditor extends PureComponent<Props> {
|
|||||||
|
|
||||||
const { layer } = settings;
|
const { layer } = settings;
|
||||||
|
|
||||||
|
this.deleteGroup();
|
||||||
layer.elements.forEach((element: ElementState) => {
|
layer.elements.forEach((element: ElementState) => {
|
||||||
layer.parent?.doAction(LayerActionID.Duplicate, element, false);
|
layer.parent?.doAction(LayerActionID.Duplicate, element, false);
|
||||||
});
|
});
|
||||||
this.deleteGroup();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private onDecoupleGroup = () => {
|
private onDecoupleGroup = () => {
|
||||||
@ -156,7 +156,11 @@ export class LayerElementListEditor extends PureComponent<Props> {
|
|||||||
|
|
||||||
const { layer } = settings;
|
const { layer } = settings;
|
||||||
|
|
||||||
|
const scene = this.getScene();
|
||||||
|
scene?.byName.delete(layer.getName());
|
||||||
|
layer.elements.forEach((element) => scene?.byName.delete(element.getName()));
|
||||||
layer.parent?.doAction(LayerActionID.Delete, layer);
|
layer.parent?.doAction(LayerActionID.Delete, layer);
|
||||||
|
|
||||||
this.goUpLayer();
|
this.goUpLayer();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -215,6 +219,12 @@ export class LayerElementListEditor extends PureComponent<Props> {
|
|||||||
return element instanceof GroupState;
|
return element instanceof GroupState;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const verifyLayerNameUniqueness = (nameToVerify: string) => {
|
||||||
|
const scene = this.getScene();
|
||||||
|
|
||||||
|
return Boolean(scene?.canRename(nameToVerify));
|
||||||
|
};
|
||||||
|
|
||||||
const selection: string[] = settings.selected ? settings.selected.map((v) => v.getName()) : [];
|
const selection: string[] = settings.selected ? settings.selected.map((v) => v.getName()) : [];
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -241,6 +251,7 @@ export class LayerElementListEditor extends PureComponent<Props> {
|
|||||||
onDuplicate={onDuplicate}
|
onDuplicate={onDuplicate}
|
||||||
getLayerInfo={getLayerInfo}
|
getLayerInfo={getLayerInfo}
|
||||||
onNameChange={onNameChange}
|
onNameChange={onNameChange}
|
||||||
|
verifyLayerNameUniqueness={verifyLayerNameUniqueness}
|
||||||
isGroup={isGroup}
|
isGroup={isGroup}
|
||||||
layers={layer.elements}
|
layers={layer.elements}
|
||||||
selection={selection}
|
selection={selection}
|
||||||
|
@ -28,7 +28,7 @@ export const plugin = new PanelPlugin<PanelOptions>(CanvasPanel)
|
|||||||
if (!(element instanceof GroupState)) {
|
if (!(element instanceof GroupState)) {
|
||||||
builder.addNestedOptions(
|
builder.addNestedOptions(
|
||||||
getElementEditor({
|
getElementEditor({
|
||||||
category: [`Selected element (id: ${element.UID})`], // changing the ID forces reload
|
category: [`Selected element (${element.options.name})`],
|
||||||
element,
|
element,
|
||||||
scene: state.scene,
|
scene: state.scene,
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user