mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -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": [
|
"public/app/plugins/panel/canvas/editor/PlacementEditor.tsx:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[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": [
|
"public/app/plugins/panel/canvas/editor/elementEditor.tsx:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
[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"]
|
[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/browser": "6.19.7",
|
||||||
"@sentry/types": "6.19.7",
|
"@sentry/types": "6.19.7",
|
||||||
"@sentry/utils": "6.19.7",
|
"@sentry/utils": "6.19.7",
|
||||||
|
"@types/rc-tree": "^3.0.0",
|
||||||
"@types/react-resizable": "3.0.0",
|
"@types/react-resizable": "3.0.0",
|
||||||
"@types/webpack-env": "^1.17.0",
|
"@types/webpack-env": "^1.17.0",
|
||||||
"@visx/event": "2.6.0",
|
"@visx/event": "2.6.0",
|
||||||
@ -349,6 +350,7 @@
|
|||||||
"rc-drawer": "4.4.3",
|
"rc-drawer": "4.4.3",
|
||||||
"rc-slider": "9.7.5",
|
"rc-slider": "9.7.5",
|
||||||
"rc-time-picker": "3.7.3",
|
"rc-time-picker": "3.7.3",
|
||||||
|
"rc-tree": "^5.6.5",
|
||||||
"re-resizable": "6.9.9",
|
"re-resizable": "6.9.9",
|
||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-awesome-query-builder": "^5.1.2",
|
"react-awesome-query-builder": "^5.1.2",
|
||||||
|
@ -74,6 +74,19 @@ export class FrameState extends ElementState {
|
|||||||
this.reinitializeMoveable();
|
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) {
|
doMove(child: ElementState, action: LayerActionID) {
|
||||||
const vals = this.elements.filter((v) => v !== child);
|
const vals = this.elements.filter((v) => v !== child);
|
||||||
if (action === LayerActionID.MoveBottom) {
|
if (action === LayerActionID.MoveBottom) {
|
||||||
@ -176,7 +189,7 @@ export class FrameState extends ElementState {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div key={this.UID} ref={this.initElement} style={{ overflow: 'hidden' }}>
|
<div key={this.UID} ref={this.initElement}>
|
||||||
{this.elements.map((v) => v.render())}
|
{this.elements.map((v) => v.render())}
|
||||||
</div>
|
</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() {
|
render() {
|
||||||
const canShowContextMenu = this.isPanelEditing || (!this.isPanelEditing && this.isEditingEnabled);
|
const canShowContextMenu = this.isPanelEditing || (!this.isPanelEditing && this.isEditingEnabled);
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { DropResult } from 'react-beautiful-dnd';
|
import { DropResult } from 'react-beautiful-dnd';
|
||||||
|
|
||||||
import { AppEvents, SelectableValue, StandardEditorProps } from '@grafana/data';
|
import { SelectableValue, StandardEditorProps } from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime/src';
|
import { config } from '@grafana/runtime';
|
||||||
import { Button, HorizontalGroup } from '@grafana/ui';
|
import { Button, HorizontalGroup } from '@grafana/ui';
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
import { AddLayerButton } from 'app/core/components/Layers/AddLayerButton';
|
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 { notFoundItem } from 'app/features/canvas/elements/notFound';
|
||||||
import { ElementState } from 'app/features/canvas/runtime/element';
|
import { ElementState } from 'app/features/canvas/runtime/element';
|
||||||
import { FrameState } from 'app/features/canvas/runtime/frame';
|
import { FrameState } from 'app/features/canvas/runtime/frame';
|
||||||
import { SelectionParams } from 'app/features/canvas/runtime/scene';
|
|
||||||
import { ShowConfirmModalEvent } from 'app/types/events';
|
import { ShowConfirmModalEvent } from 'app/types/events';
|
||||||
|
|
||||||
import { PanelOptions } from '../models.gen';
|
import { PanelOptions } from '../models.gen';
|
||||||
import { LayerActionID } from '../types';
|
import { LayerActionID } from '../types';
|
||||||
|
import { doSelect } from '../utils';
|
||||||
|
|
||||||
import { LayerEditorProps } from './layerEditor';
|
import { LayerEditorProps } from './layerEditor';
|
||||||
|
|
||||||
@ -50,24 +50,8 @@ export class LayerElementListEditor extends PureComponent<Props> {
|
|||||||
|
|
||||||
onSelect = (item: ElementState) => {
|
onSelect = (item: ElementState) => {
|
||||||
const { settings } = this.props.item;
|
const { settings } = this.props.item;
|
||||||
|
|
||||||
if (settings?.scene) {
|
if (settings?.scene) {
|
||||||
try {
|
doSelect(settings.scene, item);
|
||||||
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']);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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 {
|
.moveable-control-box {
|
||||||
z-index: 999;
|
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 { CanvasPanel, InstanceState } from './CanvasPanel';
|
||||||
import { getElementEditor } from './editor/elementEditor';
|
import { getElementEditor } from './editor/elementEditor';
|
||||||
import { getLayerEditor } from './editor/layerEditor';
|
import { getLayerEditor } from './editor/layerEditor';
|
||||||
|
import { getTreeViewEditor } from './editor/treeViewEditor';
|
||||||
import { PanelOptions } from './models.gen';
|
import { PanelOptions } from './models.gen';
|
||||||
|
|
||||||
export const plugin = new PanelPlugin<PanelOptions>(CanvasPanel)
|
export const plugin = new PanelPlugin<PanelOptions>(CanvasPanel)
|
||||||
@ -20,6 +21,7 @@ export const plugin = new PanelPlugin<PanelOptions>(CanvasPanel)
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (state) {
|
if (state) {
|
||||||
|
builder.addNestedOptions(getTreeViewEditor(state));
|
||||||
builder.addNestedOptions(getLayerEditor(state));
|
builder.addNestedOptions(getLayerEditor(state));
|
||||||
|
|
||||||
const selection = state.selected;
|
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 {
|
export enum LayerActionID {
|
||||||
Delete = 'delete',
|
Delete = 'delete',
|
||||||
Duplicate = 'duplicate',
|
Duplicate = 'duplicate',
|
||||||
MoveTop = 'move-top',
|
MoveTop = 'move-top',
|
||||||
MoveBottom = 'move-bottom',
|
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
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@types/react-beautiful-dnd@npm:13.1.2":
|
||||||
version: 13.1.2
|
version: 13.1.2
|
||||||
resolution: "@types/react-beautiful-dnd@npm:13.1.2"
|
resolution: "@types/react-beautiful-dnd@npm:13.1.2"
|
||||||
@ -21051,6 +21060,7 @@ __metadata:
|
|||||||
"@types/pluralize": ^0.0.29
|
"@types/pluralize": ^0.0.29
|
||||||
"@types/prismjs": 1.26.0
|
"@types/prismjs": 1.26.0
|
||||||
"@types/rc-time-picker": 3.4.1
|
"@types/rc-time-picker": 3.4.1
|
||||||
|
"@types/rc-tree": ^3.0.0
|
||||||
"@types/react": 17.0.42
|
"@types/react": 17.0.42
|
||||||
"@types/react-beautiful-dnd": 13.1.2
|
"@types/react-beautiful-dnd": 13.1.2
|
||||||
"@types/react-dom": 17.0.14
|
"@types/react-dom": 17.0.14
|
||||||
@ -21192,6 +21202,7 @@ __metadata:
|
|||||||
rc-drawer: 4.4.3
|
rc-drawer: 4.4.3
|
||||||
rc-slider: 9.7.5
|
rc-slider: 9.7.5
|
||||||
rc-time-picker: 3.7.3
|
rc-time-picker: 3.7.3
|
||||||
|
rc-tree: ^5.6.5
|
||||||
re-resizable: 6.9.9
|
re-resizable: 6.9.9
|
||||||
react: 17.0.2
|
react: 17.0.2
|
||||||
react-awesome-query-builder: ^5.1.2
|
react-awesome-query-builder: ^5.1.2
|
||||||
@ -30488,7 +30499,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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
|
version: 5.6.5
|
||||||
resolution: "rc-tree@npm:5.6.5"
|
resolution: "rc-tree@npm:5.6.5"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
Loading…
Reference in New Issue
Block a user