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.", "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"],
|
||||
|
@ -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<any>, context: StandardEditorContext<any>) => {
|
||||
builder.addNestedOptions(getTreeViewEditor(state));
|
||||
builder.addNestedOptions(getLayerEditor(instanceState));
|
||||
|
||||
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 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<any, TreeViewEditorProps, PanelOptions>) => {
|
||||
const [treeData, setTreeData] = useState(getTreeData(item?.settings?.scene.root));
|
||||
const [autoExpandParent, setAutoExpandParent] = useState(true);
|
||||
const [expandedKeys, setExpandedKeys] = useState<Key[]>([]);
|
||||
const [selectedKeys, setSelectedKeys] = useState<Key[]>([]);
|
||||
|
||||
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 <div>No settings</div>;
|
||||
}
|
||||
|
||||
const layer = settings.layer;
|
||||
if (!layer) {
|
||||
return <div>Missing layer?</div>;
|
||||
}
|
||||
|
||||
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<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 (
|
||||
<>
|
||||
<Global styles={globalCSS} />
|
||||
@ -92,8 +148,31 @@ export const TreeNavigationEditor = ({ item }: StandardEditorProps<any, TreeView
|
||||
expandedKeys={expandedKeys}
|
||||
onExpand={onExpand}
|
||||
treeData={treeData}
|
||||
titleRender={onTitleRender}
|
||||
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 { 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<CanvasElementOptions> {
|
||||
return {
|
||||
category: opts.category,
|
||||
|
@ -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<LayerEdi
|
||||
id: 'content',
|
||||
path: 'root',
|
||||
name: 'Elements',
|
||||
editor: LayerElementListEditor,
|
||||
editor: TreeNavigationEditor,
|
||||
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 {
|
||||
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;
|
||||
|
@ -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<PanelOptions>(CanvasPanel)
|
||||
@ -21,7 +20,6 @@ export const plugin = new PanelPlugin<PanelOptions>(CanvasPanel)
|
||||
});
|
||||
|
||||
if (state) {
|
||||
builder.addNestedOptions(getTreeViewEditor(state));
|
||||
builder.addNestedOptions(getLayerEditor(state));
|
||||
|
||||
const selection = state.selected;
|
||||
|
@ -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[]
|
||||
|
Loading…
Reference in New Issue
Block a user