mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Canvas: Merge tree view and layer element list UX (#52701)
Co-authored-by: nmarrs <nathanielmarrs@gmail.com>
This commit is contained in:
parent
e5d89ddb95
commit
a5410063c6
@ -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.", "3"],
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
|
[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": [
|
"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": [
|
"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": [
|
"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"],
|
||||||
|
@ -15,7 +15,6 @@ import { setOptionImmutably } from 'app/features/dashboard/components/PanelEdito
|
|||||||
import { activePanelSubject, InstanceState } from './CanvasPanel';
|
import { activePanelSubject, 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';
|
|
||||||
|
|
||||||
export const InlineEditBody = () => {
|
export const InlineEditBody = () => {
|
||||||
const activePanel = useObservable(activePanelSubject);
|
const activePanel = useObservable(activePanelSubject);
|
||||||
@ -30,7 +29,6 @@ export const InlineEditBody = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const supplier = (builder: PanelOptionsEditorBuilder<any>, context: StandardEditorContext<any>) => {
|
const supplier = (builder: PanelOptionsEditorBuilder<any>, context: StandardEditorContext<any>) => {
|
||||||
builder.addNestedOptions(getTreeViewEditor(state));
|
|
||||||
builder.addNestedOptions(getLayerEditor(instanceState));
|
builder.addNestedOptions(getLayerEditor(instanceState));
|
||||||
|
|
||||||
const selection = state.selected;
|
const selection = state.selected;
|
||||||
|
@ -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<any, LayerEditorProps, PanelOptions>;
|
|
||||||
|
|
||||||
export class LayerElementListEditor extends PureComponent<Props> {
|
|
||||||
getScene = () => {
|
|
||||||
const { settings } = this.props.item;
|
|
||||||
if (!settings?.layer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
return settings.layer.scene;
|
|
||||||
};
|
|
||||||
|
|
||||||
onAddItem = (sel: SelectableValue<string>) => {
|
|
||||||
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 <div>No settings</div>;
|
|
||||||
}
|
|
||||||
const layer = settings.layer;
|
|
||||||
if (!layer) {
|
|
||||||
return <div>Missing layer?</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
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() && (
|
|
||||||
<>
|
|
||||||
<Button icon="angle-up" size="sm" variant="secondary" onClick={this.goUpLayer}>
|
|
||||||
Go up level
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="secondary" onClick={() => this.onSelect(layer)}>
|
|
||||||
Select frame
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="secondary" onClick={() => this.onDecoupleFrame()}>
|
|
||||||
Decouple frame
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="secondary" onClick={() => this.onDeleteFrame()}>
|
|
||||||
Delete frame
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<LayerDragDropList
|
|
||||||
onDragEnd={this.onDragEnd}
|
|
||||||
onSelect={this.onSelect}
|
|
||||||
onDelete={onDelete}
|
|
||||||
onDuplicate={onDuplicate}
|
|
||||||
getLayerInfo={getLayerInfo}
|
|
||||||
onNameChange={onNameChange}
|
|
||||||
verifyLayerNameUniqueness={verifyLayerNameUniqueness}
|
|
||||||
showActions={showActions}
|
|
||||||
layers={layer.elements}
|
|
||||||
selection={selection}
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
|
|
||||||
<HorizontalGroup>
|
|
||||||
<AddLayerButton
|
|
||||||
onChange={this.onAddItem}
|
|
||||||
options={canvasElementRegistry.selectOptions().options}
|
|
||||||
label={'Add item'}
|
|
||||||
/>
|
|
||||||
{selection.length > 0 && (
|
|
||||||
<Button size="sm" variant="secondary" onClick={this.onClearSelection}>
|
|
||||||
Clear selection
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{selection.length > 1 && config.featureToggles.canvasPanelNesting && (
|
|
||||||
<Button size="sm" variant="secondary" onClick={this.onFrameSelection}>
|
|
||||||
Frame selection
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</HorizontalGroup>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,42 +1,65 @@
|
|||||||
import { Global } from '@emotion/react';
|
import { Global } from '@emotion/react';
|
||||||
import Tree from 'rc-tree';
|
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 SVG from 'react-inlinesvg';
|
||||||
|
|
||||||
import { StandardEditorProps } from '@grafana/data';
|
import { SelectableValue, StandardEditorProps } from '@grafana/data';
|
||||||
import { useTheme2 } from '@grafana/ui';
|
import { config } from '@grafana/runtime';
|
||||||
|
import { Button, HorizontalGroup, useTheme2 } from '@grafana/ui';
|
||||||
import { ElementState } from 'app/features/canvas/runtime/element';
|
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 { getGlobalStyles } from '../globalStyles';
|
||||||
import { PanelOptions } from '../models.gen';
|
import { PanelOptions } from '../models.gen';
|
||||||
import { getTreeData, onNodeDrop } from '../tree';
|
import { getTreeData, onNodeDrop, TreeElement } from '../tree';
|
||||||
import { DragNode, DropNode } from '../types';
|
import { DragNode, DropNode } from '../types';
|
||||||
import { doSelect } from '../utils';
|
import { doSelect } from '../utils';
|
||||||
|
|
||||||
import { TreeViewEditorProps } from './treeViewEditor';
|
import { TreeNodeTitle } from './TreeNodeTitle';
|
||||||
|
import { TreeViewEditorProps } from './elementEditor';
|
||||||
|
|
||||||
|
let allowSelection = true;
|
||||||
|
|
||||||
export const TreeNavigationEditor = ({ item }: StandardEditorProps<any, TreeViewEditorProps, PanelOptions>) => {
|
export const TreeNavigationEditor = ({ item }: StandardEditorProps<any, TreeViewEditorProps, PanelOptions>) => {
|
||||||
const [treeData, setTreeData] = useState(getTreeData(item?.settings?.scene.root));
|
const [treeData, setTreeData] = useState(getTreeData(item?.settings?.scene.root));
|
||||||
const [autoExpandParent, setAutoExpandParent] = useState(true);
|
const [autoExpandParent, setAutoExpandParent] = useState(true);
|
||||||
const [expandedKeys, setExpandedKeys] = useState<Key[]>([]);
|
const [expandedKeys, setExpandedKeys] = useState<Key[]>([]);
|
||||||
|
const [selectedKeys, setSelectedKeys] = useState<Key[]>([]);
|
||||||
|
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const globalCSS = getGlobalStyles(theme);
|
const globalCSS = getGlobalStyles(theme);
|
||||||
const selectedBgColor = theme.colors.background.secondary;
|
|
||||||
|
const selectedBgColor = theme.v1.colors.formInputBorderActive;
|
||||||
const { settings } = item;
|
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(() => {
|
useEffect(() => {
|
||||||
const selection: string[] = settings?.selected ? settings.selected.map((v) => v.getName()) : [];
|
|
||||||
|
|
||||||
setTreeData(getTreeData(item?.settings?.scene.root, selection, selectedBgColor));
|
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) {
|
if (!settings) {
|
||||||
return <div>No settings</div>;
|
return <div>No settings</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const layer = settings.layer;
|
||||||
|
if (!layer) {
|
||||||
|
return <div>Missing layer?</div>;
|
||||||
|
}
|
||||||
|
|
||||||
const onSelect = (selectedKeys: Key[], info: { node: { dataRef: ElementState } }) => {
|
const onSelect = (selectedKeys: Key[], info: { node: { dataRef: ElementState } }) => {
|
||||||
if (item.settings?.scene) {
|
if (allowSelection && item.settings?.scene) {
|
||||||
doSelect(item.settings.scene, info.node.dataRef);
|
doSelect(item.settings.scene, info.node.dataRef);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -77,6 +100,39 @@ export const TreeNavigationEditor = ({ item }: StandardEditorProps<any, TreeView
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setAllowSelection = (allow = true) => {
|
||||||
|
allowSelection = allow;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onAddItem = (sel: SelectableValue<string>) => {
|
||||||
|
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 <TreeNodeTitle nodeData={nodeData} setAllowSelection={setAllowSelection} settings={settings} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Global styles={globalCSS} />
|
<Global styles={globalCSS} />
|
||||||
@ -92,8 +148,31 @@ export const TreeNavigationEditor = ({ item }: StandardEditorProps<any, TreeView
|
|||||||
expandedKeys={expandedKeys}
|
expandedKeys={expandedKeys}
|
||||||
onExpand={onExpand}
|
onExpand={onExpand}
|
||||||
treeData={treeData}
|
treeData={treeData}
|
||||||
|
titleRender={onTitleRender}
|
||||||
switcherIcon={switcherIcon}
|
switcherIcon={switcherIcon}
|
||||||
|
selectedKeys={selectedKeys}
|
||||||
|
multiple={true}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<HorizontalGroup>
|
||||||
|
<div style={{ marginLeft: '18px' }}>
|
||||||
|
<AddLayerButton
|
||||||
|
onChange={onAddItem}
|
||||||
|
options={canvasElementRegistry.selectOptions().options}
|
||||||
|
label={'Add item'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{selection.length > 0 && (
|
||||||
|
<Button size="sm" variant="secondary" onClick={onClearSelection}>
|
||||||
|
Clear selection
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{selection.length > 1 && config.featureToggles.canvasPanelNesting && (
|
||||||
|
<Button size="sm" variant="secondary" onClick={onFrameSelection}>
|
||||||
|
Frame selection
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</HorizontalGroup>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
118
public/app/plugins/panel/canvas/editor/TreeNodeTitle.tsx
Normal file
118
public/app/plugins/panel/canvas/editor/TreeNodeTitle.tsx
Normal file
@ -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 (
|
||||||
|
<>
|
||||||
|
<LayerName
|
||||||
|
name={name}
|
||||||
|
onChange={(v) => onNameChange(element, v)}
|
||||||
|
verifyLayerNameUniqueness={verifyLayerNameUniqueness ?? undefined}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.textWrapper}> {getLayerInfo(element)}</div>
|
||||||
|
|
||||||
|
{!nodeData.children && (
|
||||||
|
<div className={styles.actionButtonsWrapper}>
|
||||||
|
<IconButton
|
||||||
|
name="copy"
|
||||||
|
title={'Duplicate'}
|
||||||
|
className={styles.actionIcon}
|
||||||
|
onClick={() => onDuplicate(element)}
|
||||||
|
/>
|
||||||
|
<IconButton
|
||||||
|
name="trash-alt"
|
||||||
|
title={'remove'}
|
||||||
|
className={styles.actionIcon}
|
||||||
|
onClick={() => onDelete(element)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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};
|
||||||
|
`,
|
||||||
|
});
|
@ -6,6 +6,8 @@ import { ElementState } from 'app/features/canvas/runtime/element';
|
|||||||
import { Scene } from 'app/features/canvas/runtime/scene';
|
import { Scene } from 'app/features/canvas/runtime/scene';
|
||||||
import { setOptionImmutably } from 'app/features/dashboard/components/PanelEditor/utils';
|
import { setOptionImmutably } from 'app/features/dashboard/components/PanelEditor/utils';
|
||||||
|
|
||||||
|
import { FrameState } from '../../../../features/canvas/runtime/frame';
|
||||||
|
|
||||||
import { PlacementEditor } from './PlacementEditor';
|
import { PlacementEditor } from './PlacementEditor';
|
||||||
import { optionBuilder } from './options';
|
import { optionBuilder } from './options';
|
||||||
|
|
||||||
@ -15,6 +17,12 @@ export interface CanvasEditorOptions {
|
|||||||
category?: string[];
|
category?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TreeViewEditorProps {
|
||||||
|
scene: Scene;
|
||||||
|
layer: FrameState;
|
||||||
|
selected: ElementState[];
|
||||||
|
}
|
||||||
|
|
||||||
export function getElementEditor(opts: CanvasEditorOptions): NestedPanelOptions<CanvasElementOptions> {
|
export function getElementEditor(opts: CanvasEditorOptions): NestedPanelOptions<CanvasElementOptions> {
|
||||||
return {
|
return {
|
||||||
category: opts.category,
|
category: opts.category,
|
||||||
|
@ -8,8 +8,8 @@ import { setOptionImmutably } from 'app/features/dashboard/components/PanelEdito
|
|||||||
|
|
||||||
import { InstanceState } from '../CanvasPanel';
|
import { InstanceState } from '../CanvasPanel';
|
||||||
|
|
||||||
import { LayerElementListEditor } from './LayerElementListEditor';
|
|
||||||
import { PlacementEditor } from './PlacementEditor';
|
import { PlacementEditor } from './PlacementEditor';
|
||||||
|
import { TreeNavigationEditor } from './TreeNavigationEditor';
|
||||||
import { optionBuilder } from './options';
|
import { optionBuilder } from './options';
|
||||||
|
|
||||||
export interface LayerEditorProps {
|
export interface LayerEditorProps {
|
||||||
@ -72,7 +72,7 @@ export function getLayerEditor(opts: InstanceState): NestedPanelOptions<LayerEdi
|
|||||||
id: 'content',
|
id: 'content',
|
||||||
path: 'root',
|
path: 'root',
|
||||||
name: 'Elements',
|
name: 'Elements',
|
||||||
editor: LayerElementListEditor,
|
editor: TreeNavigationEditor,
|
||||||
settings: { scene, layer: scene.currentLayer, selected },
|
settings: { scene, layer: scene.currentLayer, selected },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,47 +0,0 @@
|
|||||||
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 },
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
@ -10,19 +10,32 @@ export function getGlobalStyles(theme: GrafanaTheme2) {
|
|||||||
,
|
,
|
||||||
.rc-tree {
|
.rc-tree {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
margin-bottom: 15px;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
|
|
||||||
&-focused:not(&-active-focused) {
|
&-focused:not(&-active-focused) {
|
||||||
border-color: cyan;
|
border-color: cyan;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rc-tree-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.rc-tree-treenode {
|
.rc-tree-treenode {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 5px;
|
padding: 1px;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
list-style: none;
|
list-style: none;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
.draggable {
|
.draggable {
|
||||||
color: #333;
|
color: #333;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
@ -34,14 +47,6 @@ export function getGlobalStyles(theme: GrafanaTheme2) {
|
|||||||
// -webkit-user-drag: element;
|
// -webkit-user-drag: element;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: ${theme.colors.background.secondary};
|
|
||||||
}
|
|
||||||
|
|
||||||
&.dragging {
|
|
||||||
background: rgba(100, 100, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.drop-container {
|
&.drop-container {
|
||||||
> .draggable::after {
|
> .draggable::after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -49,15 +54,14 @@ export function getGlobalStyles(theme: GrafanaTheme2) {
|
|||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
box-shadow: inset 0 0 0 2px red;
|
box-shadow: inset 0 0 0 2px blue;
|
||||||
content: '';
|
content: '';
|
||||||
}
|
}
|
||||||
& ~ .rc-tree-treenode {
|
& ~ .rc-tree-treenode {
|
||||||
border-left: 2px solid chocolate;
|
border-left: 2px solid ${theme.v1.colors.formInputBorder};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.drop-target {
|
&.drop-target {
|
||||||
border: 1px solid ${theme.colors.border.strong};
|
|
||||||
& ~ .rc-tree-treenode {
|
& ~ .rc-tree-treenode {
|
||||||
border-left: none;
|
border-left: none;
|
||||||
}
|
}
|
||||||
@ -80,10 +84,26 @@ export function getGlobalStyles(theme: GrafanaTheme2) {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
vertical-align: top;
|
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 {
|
span {
|
||||||
&.rc-tree-switcher,
|
|
||||||
&.rc-tree-checkbox,
|
&.rc-tree-checkbox,
|
||||||
&.rc-tree-iconEle {
|
&.rc-tree-iconEle {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@ -103,6 +123,21 @@ export function getGlobalStyles(theme: GrafanaTheme2) {
|
|||||||
background-image: none;
|
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 {
|
&.rc-tree-icon_loading {
|
||||||
margin-right: 2px;
|
margin-right: 2px;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
|
@ -4,7 +4,6 @@ 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)
|
||||||
@ -21,7 +20,6 @@ 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;
|
||||||
|
@ -27,18 +27,8 @@ export function getTreeData(root?: RootElement | FrameState, selection?: string[
|
|||||||
dataRef: item,
|
dataRef: item,
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSelected = isItemSelected(item, selection);
|
|
||||||
if (isSelected) {
|
|
||||||
element.style = { backgroundColor: selectedColor };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item instanceof FrameState) {
|
if (item instanceof FrameState) {
|
||||||
element.children = getTreeData(item, selection, selectedColor);
|
element.children = getTreeData(item, selection, selectedColor);
|
||||||
if (isSelected) {
|
|
||||||
element.children.map((child) => {
|
|
||||||
child.style = { backgroundColor: selectedColor };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
elements.push(element);
|
elements.push(element);
|
||||||
}
|
}
|
||||||
@ -47,10 +37,6 @@ export function getTreeData(root?: RootElement | FrameState, selection?: string[
|
|||||||
return elements;
|
return elements;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isItemSelected(item: ElementState, selection: string[] | undefined) {
|
|
||||||
return Boolean(selection?.includes(item.getName()));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function onNodeDrop(
|
export function onNodeDrop(
|
||||||
info: { node: DropNode; dragNode: DragNode; dropPosition: number; dropToGap: boolean },
|
info: { node: DropNode; dragNode: DragNode; dropPosition: number; dropToGap: boolean },
|
||||||
treeData: TreeElement[]
|
treeData: TreeElement[]
|
||||||
|
Loading…
Reference in New Issue
Block a user