Canvas: show layer elements above element details (#41447)

This commit is contained in:
Ryan McKinley 2021-11-16 10:10:09 -08:00 committed by GitHub
parent 5f5298ad25
commit fbd8dc59d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 84 additions and 94 deletions

View File

@ -1,6 +1,7 @@
import React, { CSSProperties } from 'react'; import React, { CSSProperties } from 'react';
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { ReplaySubject, Subject } from 'rxjs'; import { ReplaySubject, Subject } from 'rxjs';
import { first } from 'rxjs/operators';
import Moveable from 'moveable'; import Moveable from 'moveable';
import Selecto from 'selecto'; import Selecto from 'selecto';
@ -24,6 +25,7 @@ import {
import { ElementState } from './element'; import { ElementState } from './element';
import { RootElement } from './root'; import { RootElement } from './root';
import { GroupState } from './group'; import { GroupState } from './group';
import { LayerActionID } from 'app/plugins/panel/canvas/types';
export interface SelectionParams { export interface SelectionParams {
targets: Array<HTMLElement | SVGElement>; targets: Array<HTMLElement | SVGElement>;
@ -94,6 +96,30 @@ export class Scene {
} }
} }
groupSelection() {
this.selection.pipe(first()).subscribe((currentSelectedElements) => {
const currentLayer = currentSelectedElements[0].parent!;
const newLayer = new GroupState(
{
type: 'group',
elements: [],
},
this,
currentSelectedElements[0].parent
);
currentSelectedElements.forEach((element: ElementState) => {
newLayer.doAction(LayerActionID.Duplicate, element);
currentLayer.doAction(LayerActionID.Delete, element);
});
currentLayer.elements.push(newLayer);
this.save();
});
}
clearCurrentSelection() { clearCurrentSelection() {
let event: MouseEvent = new MouseEvent('click'); let event: MouseEvent = new MouseEvent('click');
this.selecto?.clickTarget(event, this.div); this.selecto?.clickTarget(event, this.div);

View File

@ -1,6 +1,6 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { css, cx } from '@emotion/css'; import { css, cx } from '@emotion/css';
import { Button, Container, Icon, IconButton, stylesFactory, ValuePicker } from '@grafana/ui'; import { Button, HorizontalGroup, Icon, IconButton, stylesFactory, ValuePicker } from '@grafana/ui';
import { AppEvents, GrafanaTheme, SelectableValue, StandardEditorProps } from '@grafana/data'; import { AppEvents, GrafanaTheme, SelectableValue, StandardEditorProps } from '@grafana/data';
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd'; import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
@ -21,6 +21,14 @@ type Props = StandardEditorProps<any, LayerEditorProps, PanelOptions>;
export class LayerElementListEditor extends PureComponent<Props> { export class LayerElementListEditor extends PureComponent<Props> {
style = getLayerDragStyles(config.theme); style = getLayerDragStyles(config.theme);
getScene = () => {
const { settings } = this.props.item;
if (!settings?.layer) {
return;
}
return settings.layer.scene;
};
onAddItem = (sel: SelectableValue<string>) => { onAddItem = (sel: SelectableValue<string>) => {
const { settings } = this.props.item; const { settings } = this.props.item;
if (!settings?.layer) { if (!settings?.layer) {
@ -158,6 +166,15 @@ export class LayerElementListEditor extends PureComponent<Props> {
this.goUpLayer(); this.goUpLayer();
}; };
private onGroupSelection = () => {
const scene = this.getScene();
if (scene) {
scene.groupSelection();
} else {
console.warn('no scene!');
}
};
private onDeleteGroup = () => { private onDeleteGroup = () => {
appEvents.publish( appEvents.publish(
new ShowConfirmModalEvent({ new ShowConfirmModalEvent({
@ -268,7 +285,7 @@ export class LayerElementListEditor extends PureComponent<Props> {
</DragDropContext> </DragDropContext>
<br /> <br />
<Container> <HorizontalGroup>
<ValuePicker <ValuePicker
icon="plus" icon="plus"
label="Add item" label="Add item"
@ -282,7 +299,12 @@ export class LayerElementListEditor extends PureComponent<Props> {
Clear Selection Clear Selection
</Button> </Button>
)} )}
</Container> {selection.length > 1 && (
<Button size="sm" variant="secondary" onClick={this.onGroupSelection}>
Group items
</Button>
)}
</HorizontalGroup>
</> </>
); );
} }

View File

@ -1,42 +0,0 @@
import React, { FC } from 'react';
import { Button } from '@grafana/ui';
import { StandardEditorProps } from '@grafana/data';
import { InstanceState } from '../CanvasPanel';
import { PanelOptions } from '../models.gen';
import { GroupState } from 'app/features/canvas/runtime/group';
import { ElementState } from 'app/features/canvas/runtime/element';
import { LayerActionID } from '../types';
export const MultiSelectionEditor: FC<StandardEditorProps<any, InstanceState, PanelOptions>> = ({ context }) => {
const createNewLayer = () => {
const currentSelectedElements = context?.instanceState.selected;
const currentLayer = currentSelectedElements[0].parent;
const newLayer = new GroupState(
{
type: 'group',
elements: [],
},
context.instanceState.scene,
currentSelectedElements[0].parent
);
currentSelectedElements.forEach((element: ElementState) => {
newLayer.doAction(LayerActionID.Duplicate, element);
currentLayer.doAction(LayerActionID.Delete, element);
});
currentLayer.elements.push(newLayer);
context.instanceState.scene.save();
};
return (
<div>
<Button icon="plus" size="sm" variant="secondary" onClick={createNewLayer}>
Group items
</Button>
</div>
);
};

View File

@ -1,24 +0,0 @@
import { NestedPanelOptions } from '@grafana/data/src/utils/OptionsUIBuilders';
import { Scene } from 'app/features/canvas/runtime/scene';
import { MultiSelectionEditor } from './MultiSelectionEditor';
export interface CanvasEditorGroupOptions {
scene: Scene;
category?: string[];
}
export const getElementsEditor = (opts: CanvasEditorGroupOptions): NestedPanelOptions<any> => {
return {
category: opts.category,
path: '--',
build: (builder, context) => {
builder.addCustomEditor({
id: 'content',
path: '__', // not used
name: 'Options',
editor: MultiSelectionEditor,
settings: opts,
});
},
};
};

View File

@ -7,6 +7,7 @@ import { LayerElementListEditor } from './LayerElementListEditor';
import { GroupState } from 'app/features/canvas/runtime/group'; import { GroupState } from 'app/features/canvas/runtime/group';
import { Scene } from 'app/features/canvas/runtime/scene'; import { Scene } from 'app/features/canvas/runtime/scene';
import { ElementState } from 'app/features/canvas/runtime/element'; import { ElementState } from 'app/features/canvas/runtime/element';
import { PlacementEditor } from './PlacementEditor';
export interface LayerEditorProps { export interface LayerEditorProps {
scene: Scene; scene: Scene;
@ -58,6 +59,11 @@ export function getLayerEditor(opts: InstanceState): NestedPanelOptions<LayerEdi
// Dynamically fill the selected element // Dynamically fill the selected element
build: (builder, context) => { build: (builder, context) => {
const currentLayer = scene.currentLayer;
if (currentLayer && !currentLayer.isRoot()) {
// TODO: the non-root nav option
}
builder.addCustomEditor({ builder.addCustomEditor({
id: 'content', id: 'content',
path: 'root', path: 'root',
@ -66,17 +72,23 @@ export function getLayerEditor(opts: InstanceState): NestedPanelOptions<LayerEdi
settings: { scene, layer: scene.currentLayer, selected }, settings: { scene, layer: scene.currentLayer, selected },
}); });
// // force clean layer configuration
// const layer = canvasElementRegistry.getIfExists(options?.type ?? DEFAULT_CANVAS_ELEMENT_CONFIG.type)!;
//const currentOptions = { ...options, type: layer.id, config: { ...layer.defaultConfig, ...options?.config } };
const ctx = { ...context, options }; const ctx = { ...context, options };
// if (layer.registerOptionsUI) {
// layer.registerOptionsUI(builder, ctx);
// }
optionBuilder.addBackground(builder as any, ctx); optionBuilder.addBackground(builder as any, ctx);
optionBuilder.addBorder(builder as any, ctx); optionBuilder.addBorder(builder as any, ctx);
if (currentLayer && !currentLayer.isRoot()) {
builder.addCustomEditor({
category: ['Layout'],
id: 'content',
path: '__', // not used
name: 'Anchor',
editor: PlacementEditor,
settings: {
scene: opts.scene,
element: currentLayer,
},
});
}
}, },
}; };
} }

View File

@ -4,7 +4,7 @@ import { CanvasPanel, InstanceState } from './CanvasPanel';
import { PanelOptions } from './models.gen'; import { PanelOptions } from './models.gen';
import { getElementEditor } from './editor/elementEditor'; import { getElementEditor } from './editor/elementEditor';
import { getLayerEditor } from './editor/layerEditor'; import { getLayerEditor } from './editor/layerEditor';
import { getElementsEditor } from './editor/elementsEditor'; import { GroupState } from 'app/features/canvas/runtime/group';
export const plugin = new PanelPlugin<PanelOptions>(CanvasPanel) export const plugin = new PanelPlugin<PanelOptions>(CanvasPanel)
.setNoPadding() // extend to panel edges .setNoPadding() // extend to panel edges
@ -20,24 +20,20 @@ export const plugin = new PanelPlugin<PanelOptions>(CanvasPanel)
}); });
if (state) { if (state) {
builder.addNestedOptions(getLayerEditor(state));
const selection = state.selected; const selection = state.selected;
if (selection?.length === 1) { if (selection?.length === 1) {
builder.addNestedOptions( const element = selection[0];
getElementEditor({ if (!(element instanceof GroupState)) {
category: [`Selected element (id: ${selection[0].UID})`], // changing the ID forces are reload builder.addNestedOptions(
element: selection[0], getElementEditor({
scene: state.scene, category: [`Selected element (id: ${element.UID})`], // changing the ID forces reload
}) element,
); scene: state.scene,
} else if (selection?.length > 1) { })
builder.addNestedOptions( );
getElementsEditor({ }
category: [`Current selection`],
scene: state.scene,
})
);
} }
builder.addNestedOptions(getLayerEditor(state));
} }
}); });