Canvas: Refactor group to frame (#48671)

This commit is contained in:
Nathan Marrs 2022-05-03 22:58:00 -07:00 committed by GitHub
parent 0d60b1ce0a
commit ff38f24044
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 94 additions and 95 deletions

View File

@ -16,7 +16,7 @@ type LayerDragDropListProps<T extends LayerElement> = {
onSelect: (element: T) => any;
onDelete: (element: T) => any;
onDuplicate?: (element: T) => any;
isGroup?: (element: T) => boolean;
isFrame?: (element: T) => boolean;
selection?: string[]; // list of unique ids (names)
excludeBaseLayer?: boolean;
onNameChange: (element: T, newName: string) => any;
@ -30,7 +30,7 @@ export const LayerDragDropList = <T extends LayerElement>({
onSelect,
onDelete,
onDuplicate,
isGroup,
isFrame,
selection,
excludeBaseLayer,
onNameChange,
@ -74,7 +74,7 @@ export const LayerDragDropList = <T extends LayerElement>({
/>
<div className={style.textWrapper}>&nbsp; {getLayerInfo(element)}</div>
{!isGroup!(element) && (
{!isFrame!(element) && (
<>
{onDuplicate ? (
<IconButton

View File

@ -0,0 +1,6 @@
import { CanvasElementOptions } from './element';
export interface CanvasFrameOptions extends CanvasElementOptions {
type: 'frame';
elements: CanvasElementOptions[];
}

View File

@ -1,7 +0,0 @@
import { CanvasElementOptions } from './element';
export interface CanvasGroupOptions extends CanvasElementOptions {
type: 'group';
elements: CanvasElementOptions[];
// layout? // absolute, list, grid?
}

View File

@ -1,4 +1,4 @@
export * from './types';
export * from './element';
export { CanvasGroupOptions } from './group';
export { CanvasFrameOptions } from './frame';
export * from './registry';

View File

@ -13,7 +13,7 @@ import { DimensionContext } from 'app/features/dimensions';
import { HorizontalConstraint, Placement, VerticalConstraint } from '../types';
import { GroupState } from './group';
import { FrameState } from './frame';
import { RootElement } from './root';
import { Scene } from './scene';
@ -32,7 +32,7 @@ export class ElementState implements LayerElement {
// Calculated
data?: any; // depends on the type
constructor(public item: CanvasElementItem, public options: CanvasElementOptions, public parent?: GroupState) {
constructor(public item: CanvasElementItem, public options: CanvasElementOptions, public parent?: FrameState) {
const fallbackName = `Element ${Date.now()}`;
if (!options) {
this.options = { type: item.id, name: fallbackName };

View File

@ -1,7 +1,7 @@
import { cloneDeep } from 'lodash';
import React from 'react';
import { CanvasGroupOptions, canvasElementRegistry } from 'app/features/canvas';
import { CanvasFrameOptions, canvasElementRegistry } 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';
@ -13,10 +13,10 @@ import { ElementState } from './element';
import { RootElement } from './root';
import { Scene } from './scene';
export const groupItemDummy: CanvasElementItem = {
id: 'group',
name: 'Group',
description: 'Group',
export const frameItemDummy: CanvasElementItem = {
id: 'frame',
name: 'Frame',
description: 'Frame',
getNewOptions: () => ({
config: {},
@ -24,16 +24,16 @@ export const groupItemDummy: CanvasElementItem = {
// eslint-disable-next-line react/display-name
display: () => {
return <div>GROUP!</div>;
return <div>FRAME!</div>;
},
};
export class GroupState extends ElementState {
export class FrameState extends ElementState {
elements: ElementState[] = [];
scene: Scene;
constructor(public options: CanvasGroupOptions, scene: Scene, public parent?: GroupState) {
super(groupItemDummy, options, parent);
constructor(public options: CanvasFrameOptions, scene: Scene, public parent?: FrameState) {
super(frameItemDummy, options, parent);
this.scene = scene;
@ -44,8 +44,8 @@ export class GroupState extends ElementState {
}
for (const c of elements) {
if (c.type === 'group') {
this.elements.push(new GroupState(c as CanvasGroupOptions, scene, this));
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));
@ -91,8 +91,8 @@ export class GroupState extends ElementState {
this.reinitializeMoveable();
break;
case LayerActionID.Duplicate:
if (element.item.id === 'group') {
console.log('Can not duplicate groups (yet)', action, element);
if (element.item.id === 'frame') {
console.log('Can not duplicate frames (yet)', action, element);
return;
}
const opts = cloneDeep(element.options);

View File

@ -1,12 +1,12 @@
import React from 'react';
import { CanvasGroupOptions, CanvasElementOptions } from 'app/features/canvas';
import { CanvasFrameOptions, CanvasElementOptions } from 'app/features/canvas';
import { GroupState } from './group';
import { FrameState } from './frame';
import { Scene } from './scene';
export class RootElement extends GroupState {
constructor(public options: CanvasGroupOptions, public scene: Scene, private changeCallback: () => void) {
export class RootElement extends FrameState {
constructor(public options: CanvasFrameOptions, public scene: Scene, private changeCallback: () => void) {
super(options, scene);
this.sizeStyle = {
@ -22,11 +22,11 @@ export class RootElement extends GroupState {
// root type can not change
onChange(options: CanvasElementOptions) {
this.revId++;
this.options = { ...options } as CanvasGroupOptions;
this.options = { ...options } as CanvasFrameOptions;
this.changeCallback();
}
getSaveModel(): CanvasGroupOptions {
getSaveModel(): CanvasFrameOptions {
const { placement, constraint, ...rest } = this.options;
return {

View File

@ -8,7 +8,7 @@ import Selecto from 'selecto';
import { GrafanaTheme2, PanelData } from '@grafana/data';
import { stylesFactory } from '@grafana/ui';
import { config } from 'app/core/config';
import { CanvasGroupOptions, DEFAULT_CANVAS_ELEMENT_CONFIG } from 'app/features/canvas';
import { CanvasFrameOptions, DEFAULT_CANVAS_ELEMENT_CONFIG } from 'app/features/canvas';
import {
ColorDimensionConfig,
ResourceDimensionConfig,
@ -29,12 +29,12 @@ import { LayerActionID } from 'app/plugins/panel/canvas/types';
import { Placement } from '../types';
import { ElementState } from './element';
import { GroupState } from './group';
import { FrameState } from './frame';
import { RootElement } from './root';
export interface SelectionParams {
targets: Array<HTMLElement | SVGElement>;
group?: GroupState;
frame?: FrameState;
}
export class Scene {
@ -53,15 +53,15 @@ export class Scene {
selecto?: Selecto;
moveable?: Moveable;
div?: HTMLDivElement;
currentLayer?: GroupState;
currentLayer?: FrameState;
isEditingEnabled?: boolean;
constructor(cfg: CanvasGroupOptions, enableEditing: boolean, public onSave: (cfg: CanvasGroupOptions) => void) {
constructor(cfg: CanvasFrameOptions, enableEditing: boolean, public onSave: (cfg: CanvasFrameOptions) => void) {
this.root = this.load(cfg, enableEditing);
}
getNextElementName = (isGroup = false) => {
const label = isGroup ? 'Group' : 'Element';
getNextElementName = (isFrame = false) => {
const label = isFrame ? 'Frame' : 'Element';
let idx = this.byName.size + 1;
const max = idx + 100;
@ -79,10 +79,10 @@ export class Scene {
return !this.byName.has(v);
};
load(cfg: CanvasGroupOptions, enableEditing: boolean) {
load(cfg: CanvasFrameOptions, enableEditing: boolean) {
this.root = new RootElement(
cfg ?? {
type: 'group',
type: 'frame',
elements: [DEFAULT_CANVAS_ELEMENT_CONFIG],
},
this,
@ -126,13 +126,13 @@ export class Scene {
}
}
groupSelection() {
frameSelection() {
this.selection.pipe(first()).subscribe((currentSelectedElements) => {
const currentLayer = currentSelectedElements[0].parent!;
const newLayer = new GroupState(
const newLayer = new FrameState(
{
type: 'group',
type: 'frame',
name: this.getNextElementName(true),
elements: [],
},
@ -140,18 +140,18 @@ export class Scene {
currentSelectedElements[0].parent
);
const groupPlacement = this.generateGroupContainer(currentSelectedElements);
const framePlacement = this.generateFrameContainer(currentSelectedElements);
newLayer.options.placement = groupPlacement;
newLayer.options.placement = framePlacement;
currentSelectedElements.forEach((element: ElementState) => {
const elementContainer = element.div?.getBoundingClientRect();
element.setPlacementFromConstraint(elementContainer, groupPlacement as DOMRect);
element.setPlacementFromConstraint(elementContainer, framePlacement as DOMRect);
currentLayer.doAction(LayerActionID.Delete, element);
newLayer.doAction(LayerActionID.Duplicate, element, false, false);
});
newLayer.setPlacementFromConstraint(groupPlacement as DOMRect, currentLayer.div?.getBoundingClientRect());
newLayer.setPlacementFromConstraint(framePlacement as DOMRect, currentLayer.div?.getBoundingClientRect());
currentLayer.elements.push(newLayer);
@ -161,7 +161,7 @@ export class Scene {
});
}
private generateGroupContainer = (elements: ElementState[]): Placement => {
private generateFrameContainer = (elements: ElementState[]): Placement => {
let minTop = Infinity;
let minLeft = Infinity;
let maxRight = 0;
@ -204,7 +204,7 @@ export class Scene {
this.selecto?.clickTarget(event, this.div);
}
updateCurrentLayer(newLayer: GroupState) {
updateCurrentLayer(newLayer: FrameState) {
this.currentLayer = newLayer;
this.clearCurrentSelection();
this.save();
@ -233,7 +233,7 @@ export class Scene {
return currentElement;
}
const nestedElements = currentElement instanceof GroupState ? currentElement.elements : [];
const nestedElements = currentElement instanceof FrameState ? currentElement.elements : [];
for (const nestedElement of nestedElements) {
stack.unshift(nestedElement);
}
@ -256,8 +256,8 @@ export class Scene {
private updateSelection = (selection: SelectionParams) => {
this.moveable!.target = selection.targets;
if (selection.group) {
this.selection.next([selection.group]);
if (selection.frame) {
this.selection.next([selection.frame]);
} else {
const s = selection.targets.map((t) => this.findElementByTarget(t)!);
this.selection.next(s);
@ -275,7 +275,7 @@ export class Scene {
targetElements.push(currentElement.div);
}
const nestedElements = currentElement instanceof GroupState ? currentElement.elements : [];
const nestedElements = currentElement instanceof FrameState ? currentElement.elements : [];
for (const nestedElement of nestedElements) {
stack.unshift(nestedElement);
}

View File

@ -3,7 +3,7 @@ import { Subscription } from 'rxjs';
import { PanelProps } from '@grafana/data';
import { PanelContext, PanelContextRoot } from '@grafana/ui';
import { CanvasGroupOptions } from 'app/features/canvas';
import { CanvasFrameOptions } from 'app/features/canvas';
import { ElementState } from 'app/features/canvas/runtime/element';
import { Scene } from 'app/features/canvas/runtime/scene';
import { PanelEditEnteredEvent, PanelEditExitedEvent } from 'app/types/events';
@ -85,7 +85,7 @@ export class CanvasPanel extends Component<Props, State> {
// NOTE, all changes to the scene flow through this function
// even the editor gets current state from the same scene instance!
onUpdateScene = (root: CanvasGroupOptions) => {
onUpdateScene = (root: CanvasFrameOptions) => {
const { onOptionsChange, options } = this.props;
onOptionsChange({
...options,

View File

@ -9,7 +9,7 @@ import { LayerDragDropList } from 'app/core/components/Layers/LayerDragDropList'
import { CanvasElementOptions, canvasElementRegistry } from 'app/features/canvas';
import { notFoundItem } from 'app/features/canvas/elements/notFound';
import { ElementState } from 'app/features/canvas/runtime/element';
import { GroupState } from 'app/features/canvas/runtime/group';
import { FrameState } from 'app/features/canvas/runtime/frame';
import { SelectionParams } from 'app/features/canvas/runtime/scene';
import { ShowConfirmModalEvent } from 'app/types/events';
@ -53,11 +53,11 @@ export class LayerElementListEditor extends PureComponent<Props> {
if (settings?.scene) {
try {
let selection: SelectionParams = { targets: [] };
if (item instanceof GroupState) {
if (item instanceof FrameState) {
const targetElements: HTMLDivElement[] = [];
targetElements.push(item?.div!);
selection.targets = targetElements;
selection.group = item;
selection.frame = item;
settings.scene.select(selection);
} else if (item instanceof ElementState) {
const targetElement = [item?.div!];
@ -115,7 +115,7 @@ export class LayerElementListEditor extends PureComponent<Props> {
}
};
private decoupleGroup = () => {
private decoupleFrame = () => {
const settings = this.props.item.settings;
if (!settings?.layer) {
@ -124,7 +124,7 @@ export class LayerElementListEditor extends PureComponent<Props> {
const { layer } = settings;
this.deleteGroup();
this.deleteFrame();
layer.elements.forEach((element: ElementState) => {
const elementContainer = element.div?.getBoundingClientRect();
element.setPlacementFromConstraint(elementContainer, layer.parent?.div?.getBoundingClientRect());
@ -132,22 +132,22 @@ export class LayerElementListEditor extends PureComponent<Props> {
});
};
private onDecoupleGroup = () => {
private onDecoupleFrame = () => {
appEvents.publish(
new ShowConfirmModalEvent({
title: 'Decouple group',
text: `Are you sure you want to decouple this group?`,
text2: 'This will remove the group and push nested elements in the next level up.',
title: 'Decouple frame',
text: `Are you sure you want to decouple this frame?`,
text2: 'This will remove the frame and push nested elements in the next level up.',
confirmText: 'Yes',
yesText: 'Decouple',
onConfirm: async () => {
this.decoupleGroup();
this.decoupleFrame();
},
})
);
};
private deleteGroup = () => {
private deleteFrame = () => {
const settings = this.props.item.settings;
if (!settings?.layer) {
@ -164,26 +164,26 @@ export class LayerElementListEditor extends PureComponent<Props> {
this.goUpLayer();
};
private onGroupSelection = () => {
private onFrameSelection = () => {
const scene = this.getScene();
if (scene) {
scene.groupSelection();
scene.frameSelection();
} else {
console.warn('no scene!');
}
};
private onDeleteGroup = () => {
private onDeleteFrame = () => {
appEvents.publish(
new ShowConfirmModalEvent({
title: 'Delete group',
text: `Are you sure you want to delete this group?`,
text2: 'This will delete the group and all nested elements.',
title: 'Delete frame',
text: `Are you sure you want to delete this frame?`,
text2: 'This will delete the frame and all nested elements.',
icon: 'trash-alt',
confirmText: 'Delete',
yesText: 'Delete',
onConfirm: async () => {
this.deleteGroup();
this.deleteFrame();
},
})
);
@ -215,8 +215,8 @@ export class LayerElementListEditor extends PureComponent<Props> {
element.onChange({ ...element.options, name });
};
const isGroup = (element: ElementState) => {
return element instanceof GroupState;
const isFrame = (element: ElementState) => {
return element instanceof FrameState;
};
const verifyLayerNameUniqueness = (nameToVerify: string) => {
@ -231,16 +231,16 @@ export class LayerElementListEditor extends PureComponent<Props> {
{!layer.isRoot() && (
<>
<Button icon="angle-up" size="sm" variant="secondary" onClick={this.goUpLayer}>
Go Up Level
Go up level
</Button>
<Button size="sm" variant="secondary" onClick={() => this.onSelect(layer)}>
Select Group
Select frame
</Button>
<Button size="sm" variant="secondary" onClick={() => this.onDecoupleGroup()}>
Decouple Group
<Button size="sm" variant="secondary" onClick={() => this.onDecoupleFrame()}>
Decouple frame
</Button>
<Button size="sm" variant="secondary" onClick={() => this.onDeleteGroup()}>
Delete Group
<Button size="sm" variant="secondary" onClick={() => this.onDeleteFrame()}>
Delete frame
</Button>
</>
)}
@ -252,7 +252,7 @@ export class LayerElementListEditor extends PureComponent<Props> {
getLayerInfo={getLayerInfo}
onNameChange={onNameChange}
verifyLayerNameUniqueness={verifyLayerNameUniqueness}
isGroup={isGroup}
isFrame={isFrame}
layers={layer.elements}
selection={selection}
/>
@ -266,12 +266,12 @@ export class LayerElementListEditor extends PureComponent<Props> {
/>
{selection.length > 0 && (
<Button size="sm" variant="secondary" onClick={this.onClearSelection}>
Clear Selection
Clear selection
</Button>
)}
{selection.length > 1 && (
<Button size="sm" variant="secondary" onClick={this.onGroupSelection}>
Group items
<Button size="sm" variant="secondary" onClick={this.onFrameSelection}>
Frame selection
</Button>
)}
</HorizontalGroup>

View File

@ -2,7 +2,7 @@ import { get as lodashGet } from 'lodash';
import { NestedPanelOptions, NestedValueAccess } from '@grafana/data/src/utils/OptionsUIBuilders';
import { ElementState } from 'app/features/canvas/runtime/element';
import { GroupState } from 'app/features/canvas/runtime/group';
import { FrameState } from 'app/features/canvas/runtime/frame';
import { Scene } from 'app/features/canvas/runtime/scene';
import { setOptionImmutably } from 'app/features/dashboard/components/PanelEditor/utils';
@ -14,7 +14,7 @@ import { optionBuilder } from './options';
export interface LayerEditorProps {
scene: Scene;
layer: GroupState;
layer: FrameState;
selected: ElementState[];
}
@ -22,12 +22,12 @@ export function getLayerEditor(opts: InstanceState): NestedPanelOptions<LayerEdi
const { selected, scene } = opts;
if (!scene.currentLayer) {
scene.currentLayer = scene.root as GroupState;
scene.currentLayer = scene.root as FrameState;
}
if (selected) {
for (const element of selected) {
if (element instanceof GroupState) {
if (element instanceof FrameState) {
scene.currentLayer = element;
break;
}

View File

@ -3,13 +3,13 @@
// It is currenty hand written but will serve as the target for cuetsy
//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
import { CanvasGroupOptions, DEFAULT_CANVAS_ELEMENT_CONFIG } from 'app/features/canvas';
import { CanvasFrameOptions, DEFAULT_CANVAS_ELEMENT_CONFIG } from 'app/features/canvas';
export const modelVersion = Object.freeze([1, 0]);
export interface PanelOptions {
inlineEditing: boolean;
root: CanvasGroupOptions;
root: CanvasFrameOptions;
}
export const defaultPanelOptions: PanelOptions = {
@ -20,5 +20,5 @@ export const defaultPanelOptions: PanelOptions = {
...DEFAULT_CANVAS_ELEMENT_CONFIG,
},
],
} as unknown as CanvasGroupOptions,
} as unknown as CanvasFrameOptions,
};

View File

@ -1,5 +1,5 @@
import { PanelPlugin } from '@grafana/data';
import { GroupState } from 'app/features/canvas/runtime/group';
import { FrameState } from 'app/features/canvas/runtime/frame';
import { CanvasPanel, InstanceState } from './CanvasPanel';
import { getElementEditor } from './editor/elementEditor';
@ -25,7 +25,7 @@ export const plugin = new PanelPlugin<PanelOptions>(CanvasPanel)
const selection = state.selected;
if (selection?.length === 1) {
const element = selection[0];
if (!(element instanceof GroupState)) {
if (!(element instanceof FrameState)) {
builder.addNestedOptions(
getElementEditor({
category: [`Selected element (${element.options.name})`],