diff --git a/.betterer.results b/.betterer.results
index d1ac5fb17b0..9c625a489e0 100644
--- a/.betterer.results
+++ b/.betterer.results
@@ -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"]
]
}`
-};
\ No newline at end of file
+};
diff --git a/package.json b/package.json
index 7dd8d03cd7e..aad08ba9a43 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/public/app/features/canvas/runtime/frame.tsx b/public/app/features/canvas/runtime/frame.tsx
index cd1e1364d58..d53fd92d682 100644
--- a/public/app/features/canvas/runtime/frame.tsx
+++ b/public/app/features/canvas/runtime/frame.tsx
@@ -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 (
-
+
{this.elements.map((v) => v.render())}
);
diff --git a/public/app/features/canvas/runtime/scene.tsx b/public/app/features/canvas/runtime/scene.tsx
index a167a3992eb..0bec916d8a7 100644
--- a/public/app/features/canvas/runtime/scene.tsx
+++ b/public/app/features/canvas/runtime/scene.tsx
@@ -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);
diff --git a/public/app/plugins/panel/canvas/editor/LayerElementListEditor.tsx b/public/app/plugins/panel/canvas/editor/LayerElementListEditor.tsx
index 2865f83f2a2..8fa784bcf77 100644
--- a/public/app/plugins/panel/canvas/editor/LayerElementListEditor.tsx
+++ b/public/app/plugins/panel/canvas/editor/LayerElementListEditor.tsx
@@ -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
{
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);
}
};
diff --git a/public/app/plugins/panel/canvas/editor/TreeNavigationEditor.tsx b/public/app/plugins/panel/canvas/editor/TreeNavigationEditor.tsx
new file mode 100644
index 00000000000..9e7dc391eaf
--- /dev/null
+++ b/public/app/plugins/panel/canvas/editor/TreeNavigationEditor.tsx
@@ -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) => {
+ const [treeData, setTreeData] = useState(getTreeData(item?.settings?.scene.root));
+ const [autoExpandParent, setAutoExpandParent] = useState(true);
+ const [expandedKeys, setExpandedKeys] = useState([]);
+
+ 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 No settings
;
+ }
+
+ 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 = {}) => ;
+
+ 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 (
+ <>
+
+
+ >
+ );
+};
diff --git a/public/app/plugins/panel/canvas/editor/treeViewEditor.tsx b/public/app/plugins/panel/canvas/editor/treeViewEditor.tsx
new file mode 100644
index 00000000000..e82f2b85814
--- /dev/null
+++ b/public/app/plugins/panel/canvas/editor/treeViewEditor.tsx
@@ -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 {
+ 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 },
+ });
+ },
+ };
+}
diff --git a/public/app/plugins/panel/canvas/globalStyles.ts b/public/app/plugins/panel/canvas/globalStyles.ts
index a90f0db0619..3d87148fea3 100644
--- a/public/app/plugins/panel/canvas/globalStyles.ts
+++ b/public/app/plugins/panel/canvas/globalStyles.ts
@@ -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;
+ }
+ }
`;
}
diff --git a/public/app/plugins/panel/canvas/module.tsx b/public/app/plugins/panel/canvas/module.tsx
index 0499cb8f152..c51e92fe5d8 100644
--- a/public/app/plugins/panel/canvas/module.tsx
+++ b/public/app/plugins/panel/canvas/module.tsx
@@ -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(CanvasPanel)
@@ -20,6 +21,7 @@ export const plugin = new PanelPlugin(CanvasPanel)
});
if (state) {
+ builder.addNestedOptions(getTreeViewEditor(state));
builder.addNestedOptions(getLayerEditor(state));
const selection = state.selected;
diff --git a/public/app/plugins/panel/canvas/tree.ts b/public/app/plugins/panel/canvas/tree.ts
new file mode 100644
index 00000000000..03e7771efbc
--- /dev/null
+++ b/public/app/plugins/panel/canvas/tree.ts
@@ -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;
+}
diff --git a/public/app/plugins/panel/canvas/types.ts b/public/app/plugins/panel/canvas/types.ts
index 20943d4054c..25898071058 100644
--- a/public/app/plugins/panel/canvas/types.ts
+++ b/public/app/plugins/panel/canvas/types.ts
@@ -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;
+}
diff --git a/public/app/plugins/panel/canvas/utils.ts b/public/app/plugins/panel/canvas/utils.ts
new file mode 100644
index 00000000000..e83177f9a13
--- /dev/null
+++ b/public/app/plugins/panel/canvas/utils.ts
@@ -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']);
+ }
+}
diff --git a/yarn.lock b/yarn.lock
index fbed4c0aff6..835f747e93a 100644
--- a/yarn.lock
+++ b/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: