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: