mirror of
https://github.com/grafana/grafana.git
synced 2024-12-28 18:01:40 -06:00
Canvas: Tree View Navigation (#51855)
* tree navigation using rc-tree library
This commit is contained in:
parent
db9c9b5354
commit
c73d78eaac
@ -8943,6 +8943,9 @@ exports[`better eslint`] = {
|
||||
"public/app/plugins/panel/canvas/editor/PlacementEditor.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/canvas/editor/TreeNavigationEditor.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/plugins/panel/canvas/editor/elementEditor.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
@ -10402,4 +10405,4 @@ exports[`no undocumented stories`] = {
|
||||
[0, 0, 0, "No undocumented stories are allowed, please add an .mdx file with some documentation", "5381"]
|
||||
]
|
||||
}`
|
||||
};
|
||||
};
|
||||
|
@ -285,6 +285,7 @@
|
||||
"@sentry/browser": "6.19.7",
|
||||
"@sentry/types": "6.19.7",
|
||||
"@sentry/utils": "6.19.7",
|
||||
"@types/rc-tree": "^3.0.0",
|
||||
"@types/react-resizable": "3.0.0",
|
||||
"@types/webpack-env": "^1.17.0",
|
||||
"@visx/event": "2.6.0",
|
||||
@ -349,6 +350,7 @@
|
||||
"rc-drawer": "4.4.3",
|
||||
"rc-slider": "9.7.5",
|
||||
"rc-time-picker": "3.7.3",
|
||||
"rc-tree": "^5.6.5",
|
||||
"re-resizable": "6.9.9",
|
||||
"react": "17.0.2",
|
||||
"react-awesome-query-builder": "^5.1.2",
|
||||
|
@ -74,6 +74,19 @@ export class FrameState extends ElementState {
|
||||
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) {
|
||||
@ -176,7 +189,7 @@ export class FrameState extends ElementState {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div key={this.UID} ref={this.initElement} style={{ overflow: 'hidden' }}>
|
||||
<div key={this.UID} ref={this.initElement}>
|
||||
{this.elements.map((v) => v.render())}
|
||||
</div>
|
||||
);
|
||||
|
@ -417,6 +417,65 @@ export class Scene {
|
||||
});
|
||||
};
|
||||
|
||||
reorderElements = (src: ElementState, dest: ElementState, dragToGap: boolean, destPosition: number) => {
|
||||
switch (dragToGap) {
|
||||
case true:
|
||||
switch (destPosition) {
|
||||
case -1:
|
||||
// top of the tree
|
||||
if (src.parent instanceof FrameState) {
|
||||
// move outside the frame
|
||||
if (dest.parent) {
|
||||
this.updateElements(src, dest.parent, dest.parent.elements.length);
|
||||
src.updateData(dest.parent.scene.context);
|
||||
}
|
||||
} else {
|
||||
dest.parent?.reorderTree(src, dest, true);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (dest.parent) {
|
||||
this.updateElements(src, dest.parent, dest.parent.elements.indexOf(dest));
|
||||
src.updateData(dest.parent.scene.context);
|
||||
}
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case false:
|
||||
if (dest instanceof FrameState) {
|
||||
if (src.parent === dest) {
|
||||
// same frame parent
|
||||
src.parent?.reorderTree(src, dest, true);
|
||||
} else {
|
||||
this.updateElements(src, dest);
|
||||
src.updateData(dest.scene.context);
|
||||
}
|
||||
} else if (src.parent === dest.parent) {
|
||||
src.parent?.reorderTree(src, dest);
|
||||
} else {
|
||||
if (dest.parent) {
|
||||
this.updateElements(src, dest.parent);
|
||||
src.updateData(dest.parent.scene.context);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
private updateElements = (src: ElementState, dest: FrameState | RootElement, idx: number | null = null) => {
|
||||
src.parent?.doAction(LayerActionID.Delete, src);
|
||||
src.parent = dest;
|
||||
|
||||
const elementContainer = src.div?.getBoundingClientRect();
|
||||
src.setPlacementFromConstraint(elementContainer, dest.div?.getBoundingClientRect());
|
||||
|
||||
const destIndex = idx ?? dest.elements.length - 1;
|
||||
dest.elements.splice(destIndex, 0, src);
|
||||
dest.scene.save();
|
||||
|
||||
dest.reinitializeMoveable();
|
||||
};
|
||||
|
||||
render() {
|
||||
const canShowContextMenu = this.isPanelEditing || (!this.isPanelEditing && this.isEditingEnabled);
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { DropResult } from 'react-beautiful-dnd';
|
||||
|
||||
import { AppEvents, SelectableValue, StandardEditorProps } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime/src';
|
||||
import { SelectableValue, StandardEditorProps } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Button, HorizontalGroup } from '@grafana/ui';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { AddLayerButton } from 'app/core/components/Layers/AddLayerButton';
|
||||
@ -11,11 +11,11 @@ import { CanvasElementOptions, canvasElementRegistry } 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 { SelectionParams } from 'app/features/canvas/runtime/scene';
|
||||
import { ShowConfirmModalEvent } from 'app/types/events';
|
||||
|
||||
import { PanelOptions } from '../models.gen';
|
||||
import { LayerActionID } from '../types';
|
||||
import { doSelect } from '../utils';
|
||||
|
||||
import { LayerEditorProps } from './layerEditor';
|
||||
|
||||
@ -50,24 +50,8 @@ export class LayerElementListEditor extends PureComponent<Props> {
|
||||
|
||||
onSelect = (item: ElementState) => {
|
||||
const { settings } = this.props.item;
|
||||
|
||||
if (settings?.scene) {
|
||||
try {
|
||||
let selection: SelectionParams = { targets: [] };
|
||||
if (item instanceof FrameState) {
|
||||
const targetElements: HTMLDivElement[] = [];
|
||||
targetElements.push(item?.div!);
|
||||
selection.targets = targetElements;
|
||||
selection.frame = item;
|
||||
settings.scene.select(selection);
|
||||
} else if (item instanceof ElementState) {
|
||||
const targetElement = [item?.div!];
|
||||
selection.targets = targetElement;
|
||||
settings.scene.select(selection);
|
||||
}
|
||||
} catch (error) {
|
||||
appEvents.emit(AppEvents.alertError, ['Unable to select element, try selecting element in panel instead']);
|
||||
}
|
||||
doSelect(settings.scene, item);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -0,0 +1,99 @@
|
||||
import { Global } from '@emotion/react';
|
||||
import Tree from 'rc-tree';
|
||||
import React, { Key, useEffect, useState } from 'react';
|
||||
import SVG from 'react-inlinesvg';
|
||||
|
||||
import { StandardEditorProps } from '@grafana/data';
|
||||
import { useTheme2 } from '@grafana/ui';
|
||||
import { ElementState } from 'app/features/canvas/runtime/element';
|
||||
|
||||
import { getGlobalStyles } from '../globalStyles';
|
||||
import { PanelOptions } from '../models.gen';
|
||||
import { getTreeData, onNodeDrop } from '../tree';
|
||||
import { DragNode, DropNode } from '../types';
|
||||
import { doSelect } from '../utils';
|
||||
|
||||
import { TreeViewEditorProps } from './treeViewEditor';
|
||||
|
||||
export const TreeNavigationEditor = ({ item }: StandardEditorProps<any, TreeViewEditorProps, PanelOptions>) => {
|
||||
const [treeData, setTreeData] = useState(getTreeData(item?.settings?.scene.root));
|
||||
const [autoExpandParent, setAutoExpandParent] = useState(true);
|
||||
const [expandedKeys, setExpandedKeys] = useState<Key[]>([]);
|
||||
|
||||
const theme = useTheme2();
|
||||
const globalCSS = getGlobalStyles(theme);
|
||||
const selectedBgColor = theme.colors.background.secondary;
|
||||
const { settings } = item;
|
||||
|
||||
useEffect(() => {
|
||||
const selection: string[] = settings?.selected ? settings.selected.map((v) => v.getName()) : [];
|
||||
|
||||
setTreeData(getTreeData(item?.settings?.scene.root, selection, selectedBgColor));
|
||||
}, [item?.settings?.scene.root, selectedBgColor, settings?.selected]);
|
||||
|
||||
if (!settings) {
|
||||
return <div>No settings</div>;
|
||||
}
|
||||
|
||||
const onSelect = (selectedKeys: Key[], info: { node: { dataRef: ElementState } }) => {
|
||||
if (item.settings?.scene) {
|
||||
doSelect(item.settings.scene, info.node.dataRef);
|
||||
}
|
||||
};
|
||||
|
||||
const allowDrop = () => {
|
||||
return true;
|
||||
};
|
||||
|
||||
const onDrop = (info: { node: DropNode; dragNode: DragNode; dropPosition: number; dropToGap: boolean }) => {
|
||||
const destPos = info.node.pos.split('-');
|
||||
const destPosition = info.dropPosition - Number(destPos[destPos.length - 1]);
|
||||
|
||||
const srcEl = info.dragNode.dataRef;
|
||||
const destEl = info.node.dataRef;
|
||||
|
||||
const data = onNodeDrop(info, treeData);
|
||||
|
||||
setTreeData(data);
|
||||
destEl.parent?.scene.reorderElements(srcEl, destEl, info.dropToGap, destPosition);
|
||||
};
|
||||
|
||||
const onExpand = (expandedKeys: Key[]) => {
|
||||
setExpandedKeys(expandedKeys);
|
||||
setAutoExpandParent(false);
|
||||
};
|
||||
|
||||
const getSvgIcon = (path = '', style = {}) => <SVG src={path} title={'Node Icon'} style={{ ...style }} />;
|
||||
|
||||
const switcherIcon = (obj: { isLeaf: boolean; expanded: boolean }) => {
|
||||
if (obj.isLeaf) {
|
||||
// TODO: Implement element specific icons
|
||||
return getSvgIcon('');
|
||||
}
|
||||
|
||||
return getSvgIcon('public/img/icons/unicons/angle-right.svg', {
|
||||
transform: `rotate(${obj.expanded ? 90 : 0}deg)`,
|
||||
fill: theme.colors.text.primary,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Global styles={globalCSS} />
|
||||
<Tree
|
||||
selectable={true}
|
||||
onSelect={onSelect}
|
||||
draggable={true}
|
||||
defaultExpandAll={true}
|
||||
autoExpandParent={autoExpandParent}
|
||||
showIcon={false}
|
||||
allowDrop={allowDrop}
|
||||
onDrop={onDrop}
|
||||
expandedKeys={expandedKeys}
|
||||
onExpand={onExpand}
|
||||
treeData={treeData}
|
||||
switcherIcon={switcherIcon}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
47
public/app/plugins/panel/canvas/editor/treeViewEditor.tsx
Normal file
47
public/app/plugins/panel/canvas/editor/treeViewEditor.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import { NestedPanelOptions } from '@grafana/data/src/utils/OptionsUIBuilders';
|
||||
import { ElementState } from 'app/features/canvas/runtime/element';
|
||||
import { FrameState } from 'app/features/canvas/runtime/frame';
|
||||
import { Scene } from 'app/features/canvas/runtime/scene';
|
||||
|
||||
import { InstanceState } from '../CanvasPanel';
|
||||
|
||||
import { TreeNavigationEditor } from './TreeNavigationEditor';
|
||||
|
||||
export interface TreeViewEditorProps {
|
||||
scene: Scene;
|
||||
layer: FrameState;
|
||||
selected: ElementState[];
|
||||
}
|
||||
|
||||
export function getTreeViewEditor(opts: InstanceState): NestedPanelOptions<TreeViewEditorProps> {
|
||||
const { selected, scene } = opts;
|
||||
|
||||
if (selected) {
|
||||
for (const element of selected) {
|
||||
if (element instanceof FrameState) {
|
||||
scene.currentLayer = element;
|
||||
break;
|
||||
}
|
||||
|
||||
if (element.parent) {
|
||||
scene.currentLayer = element.parent;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
category: ['Tree View'],
|
||||
path: '--',
|
||||
build: (builder, context) => {
|
||||
builder.addCustomEditor({
|
||||
category: [],
|
||||
id: 'treeView',
|
||||
path: '__', // not used
|
||||
name: '',
|
||||
editor: TreeNavigationEditor,
|
||||
settings: { scene, layer: scene.currentLayer, selected },
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
@ -7,5 +7,237 @@ export function getGlobalStyles(theme: GrafanaTheme2) {
|
||||
.moveable-control-box {
|
||||
z-index: 999;
|
||||
}
|
||||
,
|
||||
.rc-tree {
|
||||
margin: 0;
|
||||
border: 1px solid transparent;
|
||||
|
||||
&-focused:not(&-active-focused) {
|
||||
border-color: cyan;
|
||||
}
|
||||
|
||||
.rc-tree-treenode {
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
line-height: 24px;
|
||||
white-space: nowrap;
|
||||
list-style: none;
|
||||
outline: 0;
|
||||
.draggable {
|
||||
color: #333;
|
||||
-moz-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
/* Required to make elements draggable in old WebKit */
|
||||
// -khtml-user-drag: element;
|
||||
// -webkit-user-drag: element;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: ${theme.colors.background.secondary};
|
||||
}
|
||||
|
||||
&.dragging {
|
||||
background: rgba(100, 100, 255, 0.1);
|
||||
}
|
||||
|
||||
&.drop-container {
|
||||
> .draggable::after {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
box-shadow: inset 0 0 0 2px red;
|
||||
content: '';
|
||||
}
|
||||
& ~ .rc-tree-treenode {
|
||||
border-left: 2px solid chocolate;
|
||||
}
|
||||
}
|
||||
&.drop-target {
|
||||
border: 1px solid ${theme.colors.border.strong};
|
||||
& ~ .rc-tree-treenode {
|
||||
border-left: none;
|
||||
}
|
||||
}
|
||||
&.filter-node {
|
||||
> .rc-tree-node-content-wrapper {
|
||||
color: #a60000 !important;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
}
|
||||
ul {
|
||||
margin: 0;
|
||||
padding: 0 0 0 18px;
|
||||
}
|
||||
.rc-tree-node-content-wrapper {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
height: 24px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
text-decoration: none;
|
||||
vertical-align: top;
|
||||
cursor: pointer;
|
||||
}
|
||||
span {
|
||||
&.rc-tree-switcher,
|
||||
&.rc-tree-checkbox,
|
||||
&.rc-tree-iconEle {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 2px;
|
||||
line-height: 16px;
|
||||
vertical-align: -0.125em;
|
||||
background-color: transparent;
|
||||
background-repeat: no-repeat;
|
||||
background-attachment: scroll;
|
||||
border: 0 none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
|
||||
&.rc-tree-icon__customize {
|
||||
background-image: none;
|
||||
}
|
||||
}
|
||||
&.rc-tree-icon_loading {
|
||||
margin-right: 2px;
|
||||
vertical-align: top;
|
||||
background: url('data:image/gif;base64,R0lGODlhEAAQAKIGAMLY8YSx5HOm4Mjc88/g9Ofw+v///wAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQFCgAGACwAAAAAEAAQAAADMGi6RbUwGjKIXCAA016PgRBElAVlG/RdLOO0X9nK61W39qvqiwz5Ls/rRqrggsdkAgAh+QQFCgAGACwCAAAABwAFAAADD2hqELAmiFBIYY4MAutdCQAh+QQFCgAGACwGAAAABwAFAAADD1hU1kaDOKMYCGAGEeYFCQAh+QQFCgAGACwKAAIABQAHAAADEFhUZjSkKdZqBQG0IELDQAIAIfkEBQoABgAsCgAGAAUABwAAAxBoVlRKgyjmlAIBqCDCzUoCACH5BAUKAAYALAYACgAHAAUAAAMPaGpFtYYMAgJgLogA610JACH5BAUKAAYALAIACgAHAAUAAAMPCAHWFiI4o1ghZZJB5i0JACH5BAUKAAYALAAABgAFAAcAAAMQCAFmIaEp1motpDQySMNFAgA7')
|
||||
no-repeat scroll 0 0 transparent;
|
||||
}
|
||||
&.rc-tree-switcher {
|
||||
&.rc-tree-switcher-noop {
|
||||
cursor: auto;
|
||||
}
|
||||
&.rc-tree-switcher_open {
|
||||
background-position: -93px -56px;
|
||||
}
|
||||
&.rc-tree-switcher_close {
|
||||
background-position: -75px -56px;
|
||||
}
|
||||
}
|
||||
&.rc-tree-checkbox {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
margin: 0 3px;
|
||||
background-position: 0 0;
|
||||
&-checked {
|
||||
background-position: -14px 0;
|
||||
}
|
||||
&-indeterminate {
|
||||
background-position: -14px -28px;
|
||||
}
|
||||
&-disabled {
|
||||
background-position: 0 -56px;
|
||||
}
|
||||
&.rc-tree-checkbox-checked.rc-tree-checkbox-disabled {
|
||||
background-position: -14px -56px;
|
||||
}
|
||||
&.rc-tree-checkbox-indeterminate.rc-tree-checkbox-disabled {
|
||||
position: relative;
|
||||
background: #ccc;
|
||||
border-radius: 3px;
|
||||
&::after {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
left: 3px;
|
||||
width: 5px;
|
||||
height: 0;
|
||||
border: 2px solid #fff;
|
||||
border-top: 0;
|
||||
border-left: 0;
|
||||
-webkit-transform: scale(1);
|
||||
transform: scale(1);
|
||||
content: ' ';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&:not(.rc-tree-show-line) {
|
||||
.rc-tree-treenode {
|
||||
.rc-tree-switcher-noop {
|
||||
background: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.rc-tree-show-line {
|
||||
.rc-tree-treenode:not(:last-child) {
|
||||
> ul {
|
||||
background: url('data:image/gif;base64,R0lGODlhCQACAIAAAMzMzP///yH5BAEAAAEALAAAAAAJAAIAAAIEjI9pUAA7') 0 0
|
||||
repeat-y;
|
||||
}
|
||||
> .rc-tree-switcher-noop {
|
||||
background-position: -56px -18px;
|
||||
}
|
||||
}
|
||||
.rc-tree-treenode:last-child {
|
||||
> .rc-tree-switcher-noop {
|
||||
background-position: -56px -36px;
|
||||
}
|
||||
}
|
||||
}
|
||||
&-child-tree {
|
||||
display: none;
|
||||
&-open {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
&-treenode-disabled {
|
||||
> span:not(.rc-tree-switcher),
|
||||
> a,
|
||||
> a span {
|
||||
color: #767676;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
&-treenode-active {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
&-node-selected {
|
||||
opacity: 0.8;
|
||||
}
|
||||
&-icon__open {
|
||||
margin-right: 2px;
|
||||
vertical-align: top;
|
||||
background-position: -110px -16px;
|
||||
}
|
||||
&-icon__close {
|
||||
margin-right: 2px;
|
||||
vertical-align: top;
|
||||
background-position: -110px 0;
|
||||
}
|
||||
&-icon__docu {
|
||||
margin-right: 2px;
|
||||
vertical-align: top;
|
||||
background-position: -110px -32px;
|
||||
}
|
||||
&-icon__customize {
|
||||
margin-right: 2px;
|
||||
vertical-align: top;
|
||||
}
|
||||
&-title {
|
||||
display: inline-block;
|
||||
}
|
||||
&-indent {
|
||||
display: inline-block;
|
||||
height: 0;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
&-indent-unit {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
&-draggable-icon {
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import { FrameState } from 'app/features/canvas/runtime/frame';
|
||||
import { CanvasPanel, InstanceState } from './CanvasPanel';
|
||||
import { getElementEditor } from './editor/elementEditor';
|
||||
import { getLayerEditor } from './editor/layerEditor';
|
||||
import { getTreeViewEditor } from './editor/treeViewEditor';
|
||||
import { PanelOptions } from './models.gen';
|
||||
|
||||
export const plugin = new PanelPlugin<PanelOptions>(CanvasPanel)
|
||||
@ -20,6 +21,7 @@ export const plugin = new PanelPlugin<PanelOptions>(CanvasPanel)
|
||||
});
|
||||
|
||||
if (state) {
|
||||
builder.addNestedOptions(getTreeViewEditor(state));
|
||||
builder.addNestedOptions(getLayerEditor(state));
|
||||
|
||||
const selection = state.selected;
|
||||
|
110
public/app/plugins/panel/canvas/tree.ts
Normal file
110
public/app/plugins/panel/canvas/tree.ts
Normal file
@ -0,0 +1,110 @@
|
||||
import { CSSProperties } from 'react';
|
||||
|
||||
import { ElementState } from 'app/features/canvas/runtime/element';
|
||||
import { FrameState } from 'app/features/canvas/runtime/frame';
|
||||
import { RootElement } from 'app/features/canvas/runtime/root';
|
||||
|
||||
import { DragNode, DropNode } from './types';
|
||||
|
||||
export interface TreeElement {
|
||||
key: number;
|
||||
title: string;
|
||||
selectable?: boolean;
|
||||
children?: TreeElement[];
|
||||
dataRef: ElementState | FrameState;
|
||||
style?: CSSProperties;
|
||||
}
|
||||
|
||||
export function getTreeData(root?: RootElement | FrameState, selection?: string[], selectedColor?: string) {
|
||||
let elements: TreeElement[] = [];
|
||||
if (root) {
|
||||
for (let i = root.elements.length; i--; i >= 0) {
|
||||
const item = root.elements[i];
|
||||
const element: TreeElement = {
|
||||
key: item.UID,
|
||||
title: item.getName(),
|
||||
selectable: true,
|
||||
dataRef: item,
|
||||
};
|
||||
|
||||
const isSelected = isItemSelected(item, selection);
|
||||
if (isSelected) {
|
||||
element.style = { backgroundColor: selectedColor };
|
||||
}
|
||||
|
||||
if (item instanceof FrameState) {
|
||||
element.children = getTreeData(item, selection, selectedColor);
|
||||
if (isSelected) {
|
||||
element.children.map((child) => {
|
||||
child.style = { backgroundColor: selectedColor };
|
||||
});
|
||||
}
|
||||
}
|
||||
elements.push(element);
|
||||
}
|
||||
}
|
||||
|
||||
return elements;
|
||||
}
|
||||
|
||||
function isItemSelected(item: ElementState, selection: string[] | undefined) {
|
||||
return Boolean(selection?.includes(item.getName()));
|
||||
}
|
||||
|
||||
export function onNodeDrop(
|
||||
info: { node: DropNode; dragNode: DragNode; dropPosition: number; dropToGap: boolean },
|
||||
treeData: TreeElement[]
|
||||
) {
|
||||
const destKey = info.node.key;
|
||||
const srcKey = info.dragNode.key;
|
||||
const destPos = info.node.pos.split('-');
|
||||
const destPosition = info.dropPosition - Number(destPos[destPos.length - 1]);
|
||||
|
||||
const loop = (
|
||||
data: TreeElement[],
|
||||
key: number,
|
||||
callback: { (item: TreeElement, index: number, arr: TreeElement[]): void }
|
||||
) => {
|
||||
data.forEach((item, index, arr) => {
|
||||
if (item.key === key) {
|
||||
callback(item, index, arr);
|
||||
return;
|
||||
}
|
||||
if (item.children) {
|
||||
loop(item.children, key, callback);
|
||||
}
|
||||
});
|
||||
};
|
||||
const data = [...treeData];
|
||||
|
||||
// Find dragObject
|
||||
let srcElement: TreeElement | undefined = undefined;
|
||||
loop(data, srcKey, (item: TreeElement, index: number, arr: TreeElement[]) => {
|
||||
arr.splice(index, 1);
|
||||
srcElement = item;
|
||||
});
|
||||
|
||||
if (destPosition === 0) {
|
||||
// Drop on the content
|
||||
loop(data, destKey, (item: TreeElement) => {
|
||||
item.children = item.children || [];
|
||||
item.children.unshift(srcElement!);
|
||||
});
|
||||
} else {
|
||||
// Drop on the gap (insert before or insert after)
|
||||
let ar: TreeElement[] = [];
|
||||
let i = 0;
|
||||
loop(data, destKey, (item: TreeElement, index: number, arr: TreeElement[]) => {
|
||||
ar = arr;
|
||||
i = index;
|
||||
});
|
||||
|
||||
if (destPosition === -1) {
|
||||
ar.splice(i, 0, srcElement!);
|
||||
} else {
|
||||
ar.splice(i + 1, 0, srcElement!);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
@ -1,6 +1,17 @@
|
||||
import { ElementState } from '../../../features/canvas/runtime/element';
|
||||
|
||||
export enum LayerActionID {
|
||||
Delete = 'delete',
|
||||
Duplicate = 'duplicate',
|
||||
MoveTop = 'move-top',
|
||||
MoveBottom = 'move-bottom',
|
||||
}
|
||||
|
||||
export interface DragNode {
|
||||
key: number;
|
||||
dataRef: ElementState;
|
||||
}
|
||||
|
||||
export interface DropNode extends DragNode {
|
||||
pos: string;
|
||||
}
|
||||
|
25
public/app/plugins/panel/canvas/utils.ts
Normal file
25
public/app/plugins/panel/canvas/utils.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import { AppEvents } from '@grafana/data/src';
|
||||
|
||||
import appEvents from '../../../core/app_events';
|
||||
import { ElementState } from '../../../features/canvas/runtime/element';
|
||||
import { FrameState } from '../../../features/canvas/runtime/frame';
|
||||
import { Scene, SelectionParams } from '../../../features/canvas/runtime/scene';
|
||||
|
||||
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']);
|
||||
}
|
||||
}
|
13
yarn.lock
13
yarn.lock
@ -11386,6 +11386,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/rc-tree@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "@types/rc-tree@npm:3.0.0"
|
||||
dependencies:
|
||||
rc-tree: "*"
|
||||
checksum: bf8e599c5a62c8f8d91fbecdc210119c4c2ad07bdfb8adecb4d1cce9cb967fd6856ad2940431b8fdc9993cd261f127c33588f2988bfc7bc6438993de1c6eeff3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/react-beautiful-dnd@npm:13.1.2":
|
||||
version: 13.1.2
|
||||
resolution: "@types/react-beautiful-dnd@npm:13.1.2"
|
||||
@ -21051,6 +21060,7 @@ __metadata:
|
||||
"@types/pluralize": ^0.0.29
|
||||
"@types/prismjs": 1.26.0
|
||||
"@types/rc-time-picker": 3.4.1
|
||||
"@types/rc-tree": ^3.0.0
|
||||
"@types/react": 17.0.42
|
||||
"@types/react-beautiful-dnd": 13.1.2
|
||||
"@types/react-dom": 17.0.14
|
||||
@ -21192,6 +21202,7 @@ __metadata:
|
||||
rc-drawer: 4.4.3
|
||||
rc-slider: 9.7.5
|
||||
rc-time-picker: 3.7.3
|
||||
rc-tree: ^5.6.5
|
||||
re-resizable: 6.9.9
|
||||
react: 17.0.2
|
||||
react-awesome-query-builder: ^5.1.2
|
||||
@ -30488,7 +30499,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rc-tree@npm:~5.6.3":
|
||||
"rc-tree@npm:*, rc-tree@npm:^5.6.5, rc-tree@npm:~5.6.3":
|
||||
version: 5.6.5
|
||||
resolution: "rc-tree@npm:5.6.5"
|
||||
dependencies:
|
||||
|
Loading…
Reference in New Issue
Block a user