diff --git a/.betterer.results b/.betterer.results index 32305e284f2..fc0766fa8a9 100644 --- a/.betterer.results +++ b/.betterer.results @@ -8630,15 +8630,12 @@ exports[`better eslint`] = { [0, 0, 0, "Unexpected any. Specify a different type.", "3"], [0, 0, 0, "Unexpected any. Specify a different type.", "4"] ], - "public/app/plugins/panel/canvas/editor/LayerElementListEditor.tsx:5381": [ - [0, 0, 0, "Unexpected any. Specify a different type.", "0"], - [0, 0, 0, "Do not use any type assertions.", "1"] - ], "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"] + [0, 0, 0, "Unexpected any. Specify a different type.", "0"], + [0, 0, 0, "Do not use any type assertions.", "1"] ], "public/app/plugins/panel/canvas/editor/elementEditor.tsx:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"], diff --git a/public/app/plugins/panel/canvas/InlineEditBody.tsx b/public/app/plugins/panel/canvas/InlineEditBody.tsx index 66fe736a58d..78df25d03ab 100644 --- a/public/app/plugins/panel/canvas/InlineEditBody.tsx +++ b/public/app/plugins/panel/canvas/InlineEditBody.tsx @@ -15,7 +15,6 @@ import { setOptionImmutably } from 'app/features/dashboard/components/PanelEdito import { activePanelSubject, InstanceState } from './CanvasPanel'; import { getElementEditor } from './editor/elementEditor'; import { getLayerEditor } from './editor/layerEditor'; -import { getTreeViewEditor } from './editor/treeViewEditor'; export const InlineEditBody = () => { const activePanel = useObservable(activePanelSubject); @@ -30,7 +29,6 @@ export const InlineEditBody = () => { } const supplier = (builder: PanelOptionsEditorBuilder, context: StandardEditorContext) => { - builder.addNestedOptions(getTreeViewEditor(state)); builder.addNestedOptions(getLayerEditor(instanceState)); const selection = state.selected; diff --git a/public/app/plugins/panel/canvas/editor/LayerElementListEditor.tsx b/public/app/plugins/panel/canvas/editor/LayerElementListEditor.tsx deleted file mode 100644 index 8fa784bcf77..00000000000 --- a/public/app/plugins/panel/canvas/editor/LayerElementListEditor.tsx +++ /dev/null @@ -1,266 +0,0 @@ -import React, { PureComponent } from 'react'; -import { DropResult } from 'react-beautiful-dnd'; - -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'; -import { LayerDragDropList } from 'app/core/components/Layers/LayerDragDropList'; -import { CanvasElementOptions, canvasElementRegistry } from 'app/features/canvas'; -import { notFoundItem } from 'app/features/canvas/elements/notFound'; -import { ElementState } from 'app/features/canvas/runtime/element'; -import { FrameState } from 'app/features/canvas/runtime/frame'; -import { ShowConfirmModalEvent } from 'app/types/events'; - -import { PanelOptions } from '../models.gen'; -import { LayerActionID } from '../types'; -import { doSelect } from '../utils'; - -import { LayerEditorProps } from './layerEditor'; - -type Props = StandardEditorProps; - -export class LayerElementListEditor extends PureComponent { - getScene = () => { - const { settings } = this.props.item; - if (!settings?.layer) { - return; - } - return settings.layer.scene; - }; - - onAddItem = (sel: SelectableValue) => { - const { settings } = this.props.item; - if (!settings?.layer) { - return; - } - const { layer } = settings; - - const item = canvasElementRegistry.getIfExists(sel.value) ?? notFoundItem; - const newElementOptions = item.getNewOptions() as CanvasElementOptions; - newElementOptions.type = item.id; - const newElement = new ElementState(item, newElementOptions, layer); - newElement.updateData(layer.scene.context); - layer.elements.push(newElement); - layer.scene.save(); - - layer.reinitializeMoveable(); - }; - - onSelect = (item: ElementState) => { - const { settings } = this.props.item; - if (settings?.scene) { - doSelect(settings.scene, item); - } - }; - - onClearSelection = () => { - const { settings } = this.props.item; - - if (!settings?.layer) { - return; - } - - const { layer } = settings; - - layer.scene.clearCurrentSelection(); - }; - - onDragEnd = (result: DropResult) => { - if (!result.destination) { - return; - } - - const { settings } = this.props.item; - if (!settings?.layer) { - return; - } - - const { layer } = settings; - - const count = layer.elements.length - 1; - const src = (result.source.index - count) * -1; - const dst = (result.destination.index - count) * -1; - - layer.reorder(src, dst); - }; - - goUpLayer = () => { - const settings = this.props.item.settings; - - if (!settings?.layer || !settings?.scene) { - return; - } - - const { scene, layer } = settings; - - if (layer.parent) { - scene.updateCurrentLayer(layer.parent); - } - }; - - private decoupleFrame = () => { - const settings = this.props.item.settings; - - if (!settings?.layer) { - return; - } - - const { layer } = settings; - - this.deleteFrame(); - layer.elements.forEach((element: ElementState) => { - const elementContainer = element.div?.getBoundingClientRect(); - element.setPlacementFromConstraint(elementContainer, layer.parent?.div?.getBoundingClientRect()); - layer.parent?.doAction(LayerActionID.Duplicate, element, false, false); - }); - }; - - private onDecoupleFrame = () => { - appEvents.publish( - new ShowConfirmModalEvent({ - title: 'Decouple frame', - text: `Are you sure you want to decouple this frame?`, - text2: 'This will remove the frame and push nested elements in the next level up.', - confirmText: 'Yes', - yesText: 'Decouple', - onConfirm: async () => { - this.decoupleFrame(); - }, - }) - ); - }; - - private deleteFrame = () => { - const settings = this.props.item.settings; - - if (!settings?.layer) { - return; - } - - const { layer } = settings; - - const scene = this.getScene(); - scene?.byName.delete(layer.getName()); - layer.elements.forEach((element) => scene?.byName.delete(element.getName())); - layer.parent?.doAction(LayerActionID.Delete, layer); - - this.goUpLayer(); - }; - - private onFrameSelection = () => { - const scene = this.getScene(); - if (scene) { - scene.frameSelection(); - } else { - console.warn('no scene!'); - } - }; - - private onDeleteFrame = () => { - appEvents.publish( - new ShowConfirmModalEvent({ - title: 'Delete frame', - text: `Are you sure you want to delete this frame?`, - text2: 'This will delete the frame and all nested elements.', - icon: 'trash-alt', - confirmText: 'Delete', - yesText: 'Delete', - onConfirm: async () => { - this.deleteFrame(); - }, - }) - ); - }; - - render() { - const settings = this.props.item.settings; - if (!settings) { - return
No settings
; - } - const layer = settings.layer; - if (!layer) { - return
Missing layer?
; - } - - const onDelete = (element: ElementState) => { - layer.doAction(LayerActionID.Delete, element); - }; - - const onDuplicate = (element: ElementState) => { - layer.doAction(LayerActionID.Duplicate, element); - }; - - const getLayerInfo = (element: ElementState) => { - return element.options.type; - }; - - const onNameChange = (element: ElementState, name: string) => { - element.onChange({ ...element.options, name }); - }; - - const showActions = (element: ElementState) => { - return !(element instanceof FrameState); - }; - - const verifyLayerNameUniqueness = (nameToVerify: string) => { - const scene = this.getScene(); - - return Boolean(scene?.canRename(nameToVerify)); - }; - - const selection: string[] = settings.selected ? settings.selected.map((v) => v.getName()) : []; - return ( - <> - {!layer.isRoot() && ( - <> - - - - - - )} - -
- - - - {selection.length > 0 && ( - - )} - {selection.length > 1 && config.featureToggles.canvasPanelNesting && ( - - )} - - - ); - } -} diff --git a/public/app/plugins/panel/canvas/editor/TreeNavigationEditor.tsx b/public/app/plugins/panel/canvas/editor/TreeNavigationEditor.tsx index 9e7dc391eaf..f77e68c816c 100644 --- a/public/app/plugins/panel/canvas/editor/TreeNavigationEditor.tsx +++ b/public/app/plugins/panel/canvas/editor/TreeNavigationEditor.tsx @@ -1,42 +1,65 @@ import { Global } from '@emotion/react'; import Tree from 'rc-tree'; -import React, { Key, useEffect, useState } from 'react'; +import React, { Key, useEffect, useMemo, useState } from 'react'; import SVG from 'react-inlinesvg'; -import { StandardEditorProps } from '@grafana/data'; -import { useTheme2 } from '@grafana/ui'; +import { SelectableValue, StandardEditorProps } from '@grafana/data'; +import { config } from '@grafana/runtime'; +import { Button, HorizontalGroup, useTheme2 } from '@grafana/ui'; import { ElementState } from 'app/features/canvas/runtime/element'; +import { AddLayerButton } from '../../../../core/components/Layers/AddLayerButton'; +import { CanvasElementOptions, canvasElementRegistry } from '../../../../features/canvas'; +import { notFoundItem } from '../../../../features/canvas/elements/notFound'; import { getGlobalStyles } from '../globalStyles'; import { PanelOptions } from '../models.gen'; -import { getTreeData, onNodeDrop } from '../tree'; +import { getTreeData, onNodeDrop, TreeElement } from '../tree'; import { DragNode, DropNode } from '../types'; import { doSelect } from '../utils'; -import { TreeViewEditorProps } from './treeViewEditor'; +import { TreeNodeTitle } from './TreeNodeTitle'; +import { TreeViewEditorProps } from './elementEditor'; + +let allowSelection = true; export const TreeNavigationEditor = ({ item }: StandardEditorProps) => { const [treeData, setTreeData] = useState(getTreeData(item?.settings?.scene.root)); const [autoExpandParent, setAutoExpandParent] = useState(true); const [expandedKeys, setExpandedKeys] = useState([]); + const [selectedKeys, setSelectedKeys] = useState([]); const theme = useTheme2(); const globalCSS = getGlobalStyles(theme); - const selectedBgColor = theme.colors.background.secondary; + + const selectedBgColor = theme.v1.colors.formInputBorderActive; const { settings } = item; + const selection = useMemo( + () => (settings?.selected ? settings.selected.map((v) => v.getName()) : []), + [settings?.selected] + ); + + const selectionByUID = useMemo( + () => (settings?.selected ? settings.selected.map((v) => v.UID) : []), + [settings?.selected] + ); 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]); + setSelectedKeys(selectionByUID); + setAllowSelection(); + }, [item?.settings?.scene.root, selectedBgColor, selection, selectionByUID]); if (!settings) { return
No settings
; } + const layer = settings.layer; + if (!layer) { + return
Missing layer?
; + } + const onSelect = (selectedKeys: Key[], info: { node: { dataRef: ElementState } }) => { - if (item.settings?.scene) { + if (allowSelection && item.settings?.scene) { doSelect(item.settings.scene, info.node.dataRef); } }; @@ -77,6 +100,39 @@ export const TreeNavigationEditor = ({ item }: StandardEditorProps { + allowSelection = allow; + }; + + const onAddItem = (sel: SelectableValue) => { + const newItem = canvasElementRegistry.getIfExists(sel.value) ?? notFoundItem; + const newElementOptions = newItem.getNewOptions() as CanvasElementOptions; + newElementOptions.type = newItem.id; + const newElement = new ElementState(newItem, newElementOptions, layer); + newElement.updateData(layer.scene.context); + layer.elements.push(newElement); + layer.scene.save(); + + layer.reinitializeMoveable(); + }; + + const onClearSelection = () => { + layer.scene.clearCurrentSelection(); + }; + + const onTitleRender = (nodeData: TreeElement) => { + return ; + }; + + // TODO: This functionality is currently kinda broken / no way to decouple / delete created frames at this time + const onFrameSelection = () => { + if (layer.scene) { + layer.scene.frameSelection(); + } else { + console.warn('no scene!'); + } + }; + return ( <> @@ -92,8 +148,31 @@ export const TreeNavigationEditor = ({ item }: StandardEditorProps + + +
+ +
+ {selection.length > 0 && ( + + )} + {selection.length > 1 && config.featureToggles.canvasPanelNesting && ( + + )} +
); }; diff --git a/public/app/plugins/panel/canvas/editor/TreeNodeTitle.tsx b/public/app/plugins/panel/canvas/editor/TreeNodeTitle.tsx new file mode 100644 index 00000000000..0c2df7f9d9b --- /dev/null +++ b/public/app/plugins/panel/canvas/editor/TreeNodeTitle.tsx @@ -0,0 +1,118 @@ +import { css } from '@emotion/css'; +import React from 'react'; + +import { GrafanaTheme2 } from '@grafana/data'; +import { IconButton, useStyles2 } from '@grafana/ui'; +import { ElementState } from 'app/features/canvas/runtime/element'; + +import { LayerName } from '../../../../core/components/Layers/LayerName'; +import { TreeElement } from '../tree'; +import { LayerActionID } from '../types'; + +import { TreeViewEditorProps } from './elementEditor'; + +interface Props { + settings: TreeViewEditorProps; + nodeData: TreeElement; + setAllowSelection: (allow: boolean) => void; +} + +export const TreeNodeTitle = ({ settings, nodeData, setAllowSelection }: Props) => { + const element = nodeData.dataRef; + const name = nodeData.dataRef.getName(); + + const styles = useStyles2(getStyles); + + const layer = settings.layer; + + const getScene = () => { + if (!settings?.layer) { + return; + } + + return settings.layer.scene; + }; + + const onDelete = (element: ElementState) => { + const elLayer = element.parent ?? layer; + elLayer.doAction(LayerActionID.Delete, element); + setAllowSelection(false); + }; + + const onDuplicate = (element: ElementState) => { + const elLayer = element.parent ?? layer; + elLayer.doAction(LayerActionID.Duplicate, element); + setAllowSelection(false); + }; + + const onNameChange = (element: ElementState, name: string) => { + element.onChange({ ...element.options, name }); + }; + + const verifyLayerNameUniqueness = (nameToVerify: string) => { + const scene = getScene(); + + return Boolean(scene?.canRename(nameToVerify)); + }; + + const getLayerInfo = (element: ElementState) => { + return element.options.type; + }; + + return ( + <> + onNameChange(element, v)} + verifyLayerNameUniqueness={verifyLayerNameUniqueness ?? undefined} + /> + +
  {getLayerInfo(element)}
+ + {!nodeData.children && ( +
+ onDuplicate(element)} + /> + onDelete(element)} + /> +
+ )} + + ); +}; + +const getStyles = (theme: GrafanaTheme2) => ({ + actionButtonsWrapper: css` + display: flex; + align-items: flex-end; + `, + actionIcon: css` + color: ${theme.colors.text.secondary}; + cursor: pointer; + &:hover { + color: ${theme.colors.text.primary}; + } + `, + textWrapper: css` + display: flex; + align-items: center; + flex-grow: 1; + overflow: hidden; + margin-right: ${theme.v1.spacing.sm}; + `, + layerName: css` + font-weight: ${theme.v1.typography.weight.semibold}; + color: ${theme.v1.colors.textBlue}; + cursor: pointer; + overflow: hidden; + margin-left: ${theme.v1.spacing.xs}; + `, +}); diff --git a/public/app/plugins/panel/canvas/editor/elementEditor.tsx b/public/app/plugins/panel/canvas/editor/elementEditor.tsx index 3403bf463a8..0a3c3e51915 100644 --- a/public/app/plugins/panel/canvas/editor/elementEditor.tsx +++ b/public/app/plugins/panel/canvas/editor/elementEditor.tsx @@ -6,6 +6,8 @@ import { ElementState } from 'app/features/canvas/runtime/element'; import { Scene } from 'app/features/canvas/runtime/scene'; import { setOptionImmutably } from 'app/features/dashboard/components/PanelEditor/utils'; +import { FrameState } from '../../../../features/canvas/runtime/frame'; + import { PlacementEditor } from './PlacementEditor'; import { optionBuilder } from './options'; @@ -15,6 +17,12 @@ export interface CanvasEditorOptions { category?: string[]; } +export interface TreeViewEditorProps { + scene: Scene; + layer: FrameState; + selected: ElementState[]; +} + export function getElementEditor(opts: CanvasEditorOptions): NestedPanelOptions { return { category: opts.category, diff --git a/public/app/plugins/panel/canvas/editor/layerEditor.tsx b/public/app/plugins/panel/canvas/editor/layerEditor.tsx index cb5891f2fd3..32858b59511 100644 --- a/public/app/plugins/panel/canvas/editor/layerEditor.tsx +++ b/public/app/plugins/panel/canvas/editor/layerEditor.tsx @@ -8,8 +8,8 @@ import { setOptionImmutably } from 'app/features/dashboard/components/PanelEdito import { InstanceState } from '../CanvasPanel'; -import { LayerElementListEditor } from './LayerElementListEditor'; import { PlacementEditor } from './PlacementEditor'; +import { TreeNavigationEditor } from './TreeNavigationEditor'; import { optionBuilder } from './options'; export interface LayerEditorProps { @@ -72,7 +72,7 @@ export function getLayerEditor(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 3d87148fea3..3f57f3c348b 100644 --- a/public/app/plugins/panel/canvas/globalStyles.ts +++ b/public/app/plugins/panel/canvas/globalStyles.ts @@ -10,19 +10,32 @@ export function getGlobalStyles(theme: GrafanaTheme2) { , .rc-tree { margin: 0; + margin-bottom: 15px; border: 1px solid transparent; &-focused:not(&-active-focused) { border-color: cyan; } + .rc-tree-title { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + } + .rc-tree-treenode { margin: 0; - padding: 5px; + padding: 1px; line-height: 24px; white-space: nowrap; list-style: none; outline: 0; + + display: flex; + margin-bottom: 3px; + cursor: pointer; + .draggable { color: #333; -moz-user-select: none; @@ -34,14 +47,6 @@ export function getGlobalStyles(theme: GrafanaTheme2) { // -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; @@ -49,15 +54,14 @@ export function getGlobalStyles(theme: GrafanaTheme2) { right: 0; bottom: 0; left: 0; - box-shadow: inset 0 0 0 2px red; + box-shadow: inset 0 0 0 2px blue; content: ''; } & ~ .rc-tree-treenode { - border-left: 2px solid chocolate; + border-left: 2px solid ${theme.v1.colors.formInputBorder}; } } &.drop-target { - border: 1px solid ${theme.colors.border.strong}; & ~ .rc-tree-treenode { border-left: none; } @@ -80,10 +84,26 @@ export function getGlobalStyles(theme: GrafanaTheme2) { padding: 0; text-decoration: none; vertical-align: top; - cursor: pointer; + cursor: grab; + flex-grow: 1; + display: flex; + + border: 1px solid ${theme.v1.colors.formInputBorder}; + border-radius: ${theme.v1.border.radius.sm}; + background: ${theme.v1.colors.bg2}; + min-height: ${theme.v1.spacing.formInputHeight}px; + + &:hover { + border: 1px solid ${theme.v1.colors.formInputBorderHover}; + } + + &.rc-tree-node-selected { + border: 1px solid ${theme.v1.colors.formInputBorderActive}; + opacity: 1; + } } + span { - &.rc-tree-switcher, &.rc-tree-checkbox, &.rc-tree-iconEle { display: inline-block; @@ -103,6 +123,21 @@ export function getGlobalStyles(theme: GrafanaTheme2) { background-image: none; } } + &.rc-tree-switcher { + display: flex; + align-items: center; + width: 16px; + 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; diff --git a/public/app/plugins/panel/canvas/module.tsx b/public/app/plugins/panel/canvas/module.tsx index c51e92fe5d8..0499cb8f152 100644 --- a/public/app/plugins/panel/canvas/module.tsx +++ b/public/app/plugins/panel/canvas/module.tsx @@ -4,7 +4,6 @@ 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) @@ -21,7 +20,6 @@ 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 index 03e7771efbc..0460c6b84eb 100644 --- a/public/app/plugins/panel/canvas/tree.ts +++ b/public/app/plugins/panel/canvas/tree.ts @@ -27,18 +27,8 @@ export function getTreeData(root?: RootElement | FrameState, selection?: string[ 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); } @@ -47,10 +37,6 @@ export function getTreeData(root?: RootElement | FrameState, selection?: string[ 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[]