DashboardLayouts: Multi-select elements (#99257)

* wip

* refactor to map

Co-authored-by: Sergej-Vlasov <sergej.vlasov@grafana.com>

* refactor + allow selecting any kind of elements

* rename class

* refactor + tests

* cr changes

* fix deselection on shift clicking multiselected objects

* i18n

* fix

* move logic to elementSelection

* lint fix

* unselecting last multiselected item should reopen dashboard options

---------

Co-authored-by: Sergej-Vlasov <sergej.vlasov@grafana.com>
pull/99994/head
Victor Marin 4 months ago committed by GitHub
parent 62aaec14b6
commit d96c1169c2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 66
      public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx
  2. 8
      public/app/features/dashboard-scene/edit-pane/DashboardEditPaneSplitter.tsx
  3. 6
      public/app/features/dashboard-scene/edit-pane/ElementEditPane.tsx
  4. 178
      public/app/features/dashboard-scene/edit-pane/ElementSelection.test.ts
  5. 184
      public/app/features/dashboard-scene/edit-pane/ElementSelection.ts
  6. 40
      public/app/features/dashboard-scene/edit-pane/MultiSelectedObjectsEditableElement.tsx
  7. 55
      public/app/features/dashboard-scene/edit-pane/MultiSelectedVizPanelsEditableElement.tsx
  8. 4
      public/app/features/dashboard-scene/edit-pane/VizPanelEditableElement.tsx
  9. 30
      public/app/features/dashboard-scene/edit-pane/useEditableElement.ts
  10. 92
      public/app/features/dashboard-scene/scene/layout-rows/MultiSelectedRowItemsElement.tsx
  11. 13
      public/app/features/dashboard-scene/scene/layout-rows/RowItem.tsx
  12. 2
      public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx
  13. 38
      public/app/features/dashboard-scene/scene/types.ts
  14. 31
      public/locales/en-US/grafana.json
  15. 31
      public/locales/pseudo-LOCALE/grafana.json

@ -2,24 +2,18 @@ import { css } from '@emotion/css';
import { useEffect, useRef } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import {
SceneObjectState,
SceneObjectBase,
SceneObject,
SceneObjectRef,
sceneGraph,
useSceneObjectState,
} from '@grafana/scenes';
import { SceneObjectState, SceneObjectBase, SceneObject, sceneGraph, useSceneObjectState } from '@grafana/scenes';
import { ElementSelectionContextItem, ElementSelectionContextState, ToolbarButton, useStyles2 } from '@grafana/ui';
import { isInCloneChain } from '../utils/clone';
import { getDashboardSceneFor } from '../utils/utils';
import { ElementEditPane } from './ElementEditPane';
import { ElementSelection } from './ElementSelection';
import { useEditableElement } from './useEditableElement';
export interface DashboardEditPaneState extends SceneObjectState {
selectedObject?: SceneObjectRef<SceneObject>;
selection?: ElementSelection;
selectionContext: ElementSelectionContextState;
}
@ -42,7 +36,7 @@ export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
public disableSelection() {
this.setState({
selectionContext: { ...this.state.selectionContext, selected: [], enabled: false },
selectedObject: undefined,
selection: undefined,
});
}
@ -64,17 +58,49 @@ export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
}
public selectObject(obj: SceneObject, id: string, multi?: boolean) {
const currentSelection = this.state.selectedObject?.resolve();
if (currentSelection === obj) {
if (!this.state.selection) {
return;
}
const prevItem = this.state.selection.getFirstObject();
if (prevItem === obj && !multi) {
this.clearSelection();
return;
}
if (multi && this.state.selection.hasValue(id)) {
this.removeMultiSelectedObject(id);
return;
}
const { selection, contextItems: selected } = this.state.selection.getStateWithValue(id, obj, !!multi);
this.setState({
selection: new ElementSelection(selection),
selectionContext: {
...this.state.selectionContext,
selected,
},
});
}
private removeMultiSelectedObject(id: string) {
if (!this.state.selection) {
return;
}
const { entries, contextItems: selected } = this.state.selection.getStateWithoutValueAt(id);
if (entries.length === 0) {
this.clearSelection();
return;
}
this.setState({
selectedObject: obj.getRef(),
selection: new ElementSelection([...entries]),
selectionContext: {
...this.state.selectionContext,
selected: [{ id }],
selected,
},
});
}
@ -82,7 +108,7 @@ export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
public clearSelection() {
const dashboard = getDashboardSceneFor(this);
this.setState({
selectedObject: dashboard.getRef(),
selection: new ElementSelection([[dashboard.state.uid!, dashboard.getRef()]]),
selectionContext: {
...this.state.selectionContext,
selected: [],
@ -103,9 +129,11 @@ export interface Props {
export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleCollapse }: Props) {
// Activate the edit pane
useEffect(() => {
if (!editPane.state.selectedObject) {
if (!editPane.state.selection) {
const dashboard = getDashboardSceneFor(editPane);
editPane.setState({ selectedObject: dashboard.getRef() });
editPane.setState({
selection: new ElementSelection([[dashboard.state.uid!, dashboard.getRef()]]),
});
}
editPane.enableSelection();
@ -115,10 +143,10 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla
};
}, [editPane]);
const { selectedObject } = useSceneObjectState(editPane, { shouldActivateOrKeepAlive: true });
const { selection } = useSceneObjectState(editPane, { shouldActivateOrKeepAlive: true });
const styles = useStyles2(getStyles);
const paneRef = useRef<HTMLDivElement>(null);
const editableElement = useEditableElement(selectedObject?.resolve());
const editableElement = useEditableElement(selection);
if (!editableElement) {
return null;

@ -76,7 +76,13 @@ export function DashboardEditPaneSplitter({ dashboard, isEditing, body, controls
<div
{...primaryProps}
className={cx(primaryProps.className, styles.canvasWithSplitter)}
onPointerDown={() => editPane.clearSelection()}
onPointerDown={(evt) => {
if (evt.shiftKey) {
return;
}
editPane.clearSelection();
}}
>
<NavToolbarActions dashboard={dashboard} />
<div className={cx(!isEditing && styles.controlsWrapperSticky)}>{controls}</div>

@ -4,14 +4,14 @@ import { GrafanaTheme2 } from '@grafana/data';
import { Stack, useStyles2 } from '@grafana/ui';
import { OptionsPaneCategory } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategory';
import { EditableDashboardElement } from '../scene/types';
import { EditableDashboardElement, MultiSelectedEditableDashboardElement } from '../scene/types';
export interface Props {
element: EditableDashboardElement;
element: EditableDashboardElement | MultiSelectedEditableDashboardElement;
}
export function ElementEditPane({ element }: Props) {
const categories = element.useEditPaneOptions();
const categories = element.useEditPaneOptions ? element.useEditPaneOptions() : [];
const styles = useStyles2(getStyles);
return (

@ -0,0 +1,178 @@
import { SceneTimeRange, VizPanel } from '@grafana/scenes';
import { DashboardScene } from '../scene/DashboardScene';
import { DefaultGridLayoutManager } from '../scene/layout-default/DefaultGridLayoutManager';
import { DashboardEditableElement } from './DashboardEditableElement';
import { ElementSelection } from './ElementSelection';
import { MultiSelectedObjectsEditableElement } from './MultiSelectedObjectsEditableElement';
import { MultiSelectedVizPanelsEditableElement } from './MultiSelectedVizPanelsEditableElement';
import { VizPanelEditableElement } from './VizPanelEditableElement';
let panel1: VizPanel, panel2: VizPanel, scene: DashboardScene;
describe('ElementSelection', () => {
beforeAll(() => {
const testScene = buildScene();
panel1 = testScene.panel1;
panel2 = testScene.panel2;
scene = testScene.scene;
});
it('returns a single object when only one is selected', () => {
const selection = new ElementSelection([['id1', panel1.getRef()]]);
expect(selection.isMultiSelection).toBe(false);
expect(selection.getSelection()).toBe(panel1);
});
it('returns multiple objects when multiple are selected', () => {
const selection = new ElementSelection([
['id1', panel1.getRef()],
['id2', panel2.getRef()],
]);
expect(selection.isMultiSelection).toBe(true);
expect(selection.getSelection()).toEqual([panel1, panel2]);
});
it('delete element', () => {
const selection = new ElementSelection([
['id1', panel1.getRef()],
['id2', panel2.getRef()],
]);
selection.removeValue('id1');
expect(selection.isMultiSelection).toBe(false);
expect(selection.getSelection()).toEqual(panel2);
});
it('returns entries', () => {
const ref1 = panel1.getRef();
const ref2 = panel2.getRef();
const selection = new ElementSelection([
['id1', ref1],
['id2', ref2],
]);
expect(selection.isMultiSelection).toBe(true);
expect(selection.getSelectionEntries()).toEqual([
['id1', ref1],
['id2', ref2],
]);
});
it('returns the first selected object through getFirstObject', () => {
const selection = new ElementSelection([
['id1', panel1.getRef()],
['id2', panel2.getRef()],
]);
expect(selection.isMultiSelection).toBe(true);
expect(selection.getFirstObject()).toBe(panel1);
});
it('creates correct element type for single selection', () => {
const vizSelection = new ElementSelection([['id1', panel1.getRef()]]);
expect(vizSelection.createSelectionElement()).toBeInstanceOf(VizPanelEditableElement);
const dashboardSelection = new ElementSelection([['id1', scene.getRef()]]);
expect(dashboardSelection.createSelectionElement()).toBeInstanceOf(DashboardEditableElement);
});
it('creates correct element type for multi-selection of same type', () => {
const selection = new ElementSelection([
['id1', panel1.getRef()],
['id2', panel2.getRef()],
]);
expect(selection.createSelectionElement()).toBeInstanceOf(MultiSelectedVizPanelsEditableElement);
});
it('creates MultiSelectedObjectsEditableElement for selection of different object types', () => {
const selection = new ElementSelection([
['id1', panel1.getRef()],
['id2', scene.getRef()],
]);
expect(selection.createSelectionElement()).toBeInstanceOf(MultiSelectedObjectsEditableElement);
});
it('handles empty selection correctly', () => {
const selection = new ElementSelection([]);
expect(selection.getSelection()).toBeUndefined();
expect(selection.getFirstObject()).toBeUndefined();
expect(selection.createSelectionElement()).toBeUndefined();
});
it('returns the entries with the specified value removed', () => {
const selection = new ElementSelection([
['id1', panel1.getRef()],
['id2', panel2.getRef()],
['id3', scene.getRef()],
]);
const { entries, contextItems } = selection.getStateWithoutValueAt('id2');
expect(entries).toEqual([
['id1', panel1.getRef()],
['id3', scene.getRef()],
]);
expect(contextItems).toEqual([{ id: 'id1' }, { id: 'id3' }]);
});
it('returns the entries with the specified value added in a multi-select scenario', () => {
const selection = new ElementSelection([
['id1', panel1.getRef()],
['id2', panel2.getRef()],
]);
const { selection: entries, contextItems } = selection.getStateWithValue('id3', scene, true);
expect(entries).toEqual([
['id3', panel1.getRef()],
['id1', panel2.getRef()],
['id2', scene.getRef()],
]);
expect(contextItems).toEqual([{ id: 'id3' }, { id: 'id1' }, { id: 'id2' }]);
});
it('returns the entries with just the specified value added in a non multi-select scenario', () => {
const selection = new ElementSelection([
['id1', panel1.getRef()],
['id2', panel2.getRef()],
]);
const { selection: entries, contextItems } = selection.getStateWithValue('id3', scene, false);
expect(entries).toEqual([['id3', scene.getRef()]]);
expect(contextItems).toEqual([{ id: 'id3' }]);
});
});
function buildScene() {
const panel1 = new VizPanel({
title: 'Panel A',
// pluginId: 'text',
key: 'panel-12',
});
const panel2 = new VizPanel({
title: 'Panel B',
// pluginId: 'text',
key: 'panel-13',
});
const scene = new DashboardScene({
title: 'hello',
uid: 'dash-1',
meta: {
canEdit: true,
},
$timeRange: new SceneTimeRange({}),
body: DefaultGridLayoutManager.fromVizPanels([panel1, panel2]),
});
return { panel1, panel2, scene };
}

@ -0,0 +1,184 @@
import { SceneObject, SceneObjectRef, VizPanel } from '@grafana/scenes';
import { ElementSelectionContextItem } from '@grafana/ui';
import { DashboardScene } from '../scene/DashboardScene';
import {
EditableDashboardElement,
isBulkActionElement,
isEditableDashboardElement,
MultiSelectedEditableDashboardElement,
} from '../scene/types';
import { DashboardEditableElement } from './DashboardEditableElement';
import { MultiSelectedObjectsEditableElement } from './MultiSelectedObjectsEditableElement';
import { MultiSelectedVizPanelsEditableElement } from './MultiSelectedVizPanelsEditableElement';
import { VizPanelEditableElement } from './VizPanelEditableElement';
export class ElementSelection {
private selectedObjects?: Map<string, SceneObjectRef<SceneObject>>;
private sameType?: boolean;
private _isMultiSelection: boolean;
constructor(values: Array<[string, SceneObjectRef<SceneObject>]>) {
this.selectedObjects = new Map(values);
this._isMultiSelection = values.length > 1;
if (this.isMultiSelection) {
this.sameType = this.checkSameType();
}
}
private checkSameType() {
const values = this.selectedObjects?.values();
const firstType = values?.next().value?.resolve()?.constructor.name;
if (!firstType) {
return false;
}
for (let obj of values ?? []) {
if (obj.resolve()?.constructor.name !== firstType) {
return false;
}
}
return true;
}
public hasValue(id: string) {
return this.selectedObjects?.has(id);
}
public removeValue(id: string) {
this.selectedObjects?.delete(id);
if (this.selectedObjects && this.selectedObjects.size < 2) {
this.sameType = undefined;
this._isMultiSelection = false;
}
}
public getStateWithValue(
id: string,
obj: SceneObject,
isMulti: boolean
): { selection: Array<[string, SceneObjectRef<SceneObject>]>; contextItems: ElementSelectionContextItem[] } {
const ref = obj.getRef();
let contextItems = [{ id }];
let selection: Array<[string, SceneObjectRef<SceneObject>]> = [[id, ref]];
const entries = this.getSelectionEntries() ?? [];
const items = entries.map(([key]) => ({ id: key }));
if (isMulti) {
selection = [[id, ref], ...entries];
contextItems = [{ id }, ...items];
}
return { selection, contextItems };
}
public getStateWithoutValueAt(id: string): {
entries: Array<[string, SceneObjectRef<SceneObject>]>;
contextItems: ElementSelectionContextItem[];
} {
this.removeValue(id);
const entries = this.getSelectionEntries() ?? [];
const contextItems = entries.map(([key]) => ({ id: key }));
return { entries, contextItems };
}
public getSelection(): SceneObject | SceneObject[] | undefined {
if (this.isMultiSelection) {
return this.getSceneObjects();
}
return this.getFirstObject();
}
public getSelectionEntries(): Array<[string, SceneObjectRef<SceneObject>]> {
return Array.from(this.selectedObjects?.entries() ?? []);
}
public getFirstObject(): SceneObject | undefined {
return this.selectedObjects?.values().next().value?.resolve();
}
public get isMultiSelection(): boolean {
return this._isMultiSelection;
}
private getSceneObjects(): SceneObject[] {
return Array.from(this.selectedObjects?.values() ?? []).map((obj) => obj.resolve());
}
public createSelectionElement() {
if (this.isMultiSelection) {
return this.createMultiSelectedElement();
}
return this.createSingleSelectedElement();
}
private createSingleSelectedElement(): EditableDashboardElement | undefined {
const sceneObj = this.selectedObjects?.values().next().value?.resolve();
if (!sceneObj) {
return undefined;
}
if (isEditableDashboardElement(sceneObj)) {
return sceneObj;
}
if (sceneObj instanceof VizPanel) {
return new VizPanelEditableElement(sceneObj);
}
if (sceneObj instanceof DashboardScene) {
return new DashboardEditableElement(sceneObj);
}
return undefined;
}
private createMultiSelectedElement(): MultiSelectedEditableDashboardElement | undefined {
if (!this.isMultiSelection) {
return;
}
const sceneObjects = this.getSceneObjects();
if (this.sameType) {
const firstObj = this.selectedObjects?.values().next().value?.resolve();
if (firstObj instanceof VizPanel) {
return new MultiSelectedVizPanelsEditableElement(sceneObjects);
}
if (isEditableDashboardElement(firstObj!)) {
return firstObj.createMultiSelectedElement?.(sceneObjects);
}
}
const bulkActionElements = [];
for (const sceneObject of sceneObjects) {
if (sceneObject instanceof VizPanel) {
const editableElement = new VizPanelEditableElement(sceneObject);
bulkActionElements.push(editableElement);
}
if (isBulkActionElement(sceneObject)) {
bulkActionElements.push(sceneObject);
}
}
if (bulkActionElements.length) {
return new MultiSelectedObjectsEditableElement(bulkActionElements);
}
return undefined;
}
}

@ -0,0 +1,40 @@
import { ReactNode } from 'react';
import { Stack, Text, Button } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { BulkActionElement, MultiSelectedEditableDashboardElement } from '../scene/types';
export class MultiSelectedObjectsEditableElement implements MultiSelectedEditableDashboardElement {
public isMultiSelectedEditableDashboardElement: true = true;
private items?: BulkActionElement[];
constructor(items: BulkActionElement[]) {
this.items = items;
}
public onDelete = () => {
for (const item of this.items || []) {
item.onDelete();
}
};
public getTypeName(): string {
return 'Objects';
}
renderActions(): ReactNode {
return (
<Stack direction="column">
<Text>
<Trans i18nKey="dashboard.edit-pane.objects.multi-select.selection-number">No. of objects selected: </Trans>
{this.items?.length}
</Text>
<Stack direction="row">
<Button size="sm" variant="secondary" icon="copy" />
<Button size="sm" variant="destructive" fill="outline" onClick={this.onDelete} icon="trash-alt" />
</Stack>
</Stack>
);
}
}

@ -0,0 +1,55 @@
import { ReactNode } from 'react';
import { SceneObject, VizPanel } from '@grafana/scenes';
import { Button, Stack, Text } from '@grafana/ui';
import { Trans } from 'app/core/internationalization';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
import { MultiSelectedEditableDashboardElement } from '../scene/types';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
export class MultiSelectedVizPanelsEditableElement implements MultiSelectedEditableDashboardElement {
public isMultiSelectedEditableDashboardElement: true = true;
private items?: VizPanel[];
constructor(items: SceneObject[]) {
this.items = [];
for (const item of items) {
if (item instanceof VizPanel) {
this.items.push(item);
}
}
}
useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
return [];
}
public onDelete = () => {
for (const panel of this.items || []) {
const layout = dashboardSceneGraph.getLayoutManagerFor(panel);
layout.removePanel(panel);
}
};
public getTypeName(): string {
return 'Panels';
}
renderActions(): ReactNode {
return (
<Stack direction="column">
<Text>
<Trans i18nKey="dashboard.edit-pane.panels.multi-select.selection-number">No. of panels selected: </Trans>
{this.items?.length}
</Text>
<Stack direction="row">
<Button size="sm" variant="secondary" icon="copy" />
<Button size="sm" variant="destructive" fill="outline" onClick={this.onDelete} icon="trash-alt" />
</Stack>
</Stack>
);
}
}

@ -12,10 +12,10 @@ import {
PanelDescriptionTextArea,
PanelFrameTitleInput,
} from '../panel-edit/getPanelFrameOptions';
import { EditableDashboardElement, isDashboardLayoutItem } from '../scene/types';
import { BulkActionElement, EditableDashboardElement, isDashboardLayoutItem } from '../scene/types';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
export class VizPanelEditableElement implements EditableDashboardElement {
export class VizPanelEditableElement implements EditableDashboardElement, BulkActionElement {
public isEditableDashboardElement: true = true;
public constructor(private panel: VizPanel) {}

@ -1,31 +1,17 @@
import { useMemo } from 'react';
import { SceneObject, VizPanel } from '@grafana/scenes';
import { EditableDashboardElement, MultiSelectedEditableDashboardElement } from '../scene/types';
import { DashboardScene } from '../scene/DashboardScene';
import { EditableDashboardElement, isEditableDashboardElement } from '../scene/types';
import { ElementSelection } from './ElementSelection';
import { DashboardEditableElement } from './DashboardEditableElement';
import { VizPanelEditableElement } from './VizPanelEditableElement';
export function useEditableElement(sceneObj: SceneObject | undefined): EditableDashboardElement | undefined {
export function useEditableElement(
selection: ElementSelection | undefined
): EditableDashboardElement | MultiSelectedEditableDashboardElement | undefined {
return useMemo(() => {
if (!sceneObj) {
if (!selection) {
return undefined;
}
if (isEditableDashboardElement(sceneObj)) {
return sceneObj;
}
if (sceneObj instanceof VizPanel) {
return new VizPanelEditableElement(sceneObj);
}
if (sceneObj instanceof DashboardScene) {
return new DashboardEditableElement(sceneObj);
}
return undefined;
}, [sceneObj]);
return selection.createSelectionElement();
}, [selection]);
}

@ -0,0 +1,92 @@
import { ReactNode, useMemo } from 'react';
import { SceneObject } from '@grafana/scenes';
import { Button, Stack, Switch, Text } from '@grafana/ui';
import { t, Trans } from 'app/core/internationalization';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
import { MultiSelectedEditableDashboardElement } from '../types';
import { RowItem } from './RowItem';
export class MultiSelectedRowItemsElement implements MultiSelectedEditableDashboardElement {
public isMultiSelectedEditableDashboardElement: true = true;
private items?: RowItem[];
constructor(items: SceneObject[]) {
this.items = [];
for (const item of items) {
if (item instanceof RowItem) {
this.items.push(item);
}
}
}
useEditPaneOptions(): OptionsPaneCategoryDescriptor[] {
const rows = this.items;
const rowOptions = useMemo(() => {
return new OptionsPaneCategoryDescriptor({
title: t('dashboard.edit-pane.row.multi-select.options-header', 'Multi-selected Row options'),
id: 'ms-row-options',
isOpenDefault: true,
}).addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.edit-pane.row.hide', 'Hide row header'),
render: () => <RowHeaderSwitch rows={rows} />,
})
);
}, [rows]);
return [rowOptions];
}
public getTypeName(): string {
return 'Rows';
}
public onDelete = () => {
for (const item of this.items || []) {
item.onDelete();
}
};
renderActions(): ReactNode {
return (
<Stack direction="column">
<Text>
<Trans i18nKey="dashboard.edit-pane.row.multi-select.selection-number">No. of rows selected: </Trans>
{this.items?.length}
</Text>
<Stack direction="row">
<Button size="sm" variant="secondary" icon="copy" />
<Button size="sm" variant="destructive" fill="outline" onClick={this.onDelete} icon="trash-alt" />
</Stack>
</Stack>
);
}
}
export function RowHeaderSwitch({ rows }: { rows: RowItem[] | undefined }) {
if (!rows) {
return null;
}
const { isHeaderHidden = false } = rows[0].useState();
return (
<Switch
value={isHeaderHidden}
onChange={() => {
for (const row of rows) {
row.setState({
isHeaderHidden: !isHeaderHidden,
});
}
}}
/>
);
}

@ -9,6 +9,7 @@ import {
SceneComponentProps,
sceneGraph,
VariableDependencyConfig,
SceneObject,
} from '@grafana/scenes';
import {
Alert,
@ -32,8 +33,9 @@ import { isClonedKey } from '../../utils/clone';
import { getDashboardSceneFor, getDefaultVizPanel, getQueryRunnerFor } from '../../utils/utils';
import { DashboardScene } from '../DashboardScene';
import { useLayoutCategory } from '../layouts-shared/DashboardLayoutSelector';
import { DashboardLayoutManager, EditableDashboardElement, LayoutParent } from '../types';
import { BulkActionElement, DashboardLayoutManager, EditableDashboardElement, LayoutParent } from '../types';
import { MultiSelectedRowItemsElement } from './MultiSelectedRowItemsElement';
import { RowItemRepeaterBehavior } from './RowItemRepeaterBehavior';
import { RowsLayoutManager } from './RowsLayoutManager';
@ -45,7 +47,10 @@ export interface RowItemState extends SceneObjectState {
height?: 'expand' | 'min';
}
export class RowItem extends SceneObjectBase<RowItemState> implements LayoutParent, EditableDashboardElement {
export class RowItem
extends SceneObjectBase<RowItemState>
implements LayoutParent, BulkActionElement, EditableDashboardElement
{
protected _variableDependency = new VariableDependencyConfig(this, {
statePaths: ['title'],
});
@ -106,6 +111,10 @@ export class RowItem extends SceneObjectBase<RowItemState> implements LayoutPare
return 'Row';
}
public createMultiSelectedElement(items: SceneObject[]) {
return new MultiSelectedRowItemsElement(items);
}
public onDelete = () => {
const layout = sceneGraph.getAncestor(this, RowsLayoutManager);
layout.removeRow(this);

@ -118,7 +118,7 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> i
}
public getSelectedObject() {
return sceneGraph.getAncestor(this, DashboardScene).state.editPane.state.selectedObject?.resolve();
return sceneGraph.getAncestor(this, DashboardScene).state.editPane.state.selection?.getFirstObject();
}
public static getDescriptor(): LayoutRegistryItem {

@ -153,7 +153,7 @@ export interface EditableDashboardElement {
*/
isEditableDashboardElement: true;
/**
* Hook that returns edit pane optionsß
* Hook that returns edit pane options
*/
useEditPaneOptions(): OptionsPaneCategoryDescriptor[];
/**
@ -164,8 +164,44 @@ export interface EditableDashboardElement {
* Panel Actions
**/
renderActions?(): React.ReactNode;
/**
* creates a new multi-selection element from a list of selected items
*/
createMultiSelectedElement?(items: SceneObject[]): MultiSelectedEditableDashboardElement;
}
export function isEditableDashboardElement(obj: object): obj is EditableDashboardElement {
return 'isEditableDashboardElement' in obj;
}
export interface MultiSelectedEditableDashboardElement {
/**
* Marks this object as an element that can be selected and edited directly on the canvas
*/
isMultiSelectedEditableDashboardElement: true;
/**
* Get the type name of the element
*/
getTypeName(): string;
/**
* Hook that returns edit pane options
*/
useEditPaneOptions?(): OptionsPaneCategoryDescriptor[];
/**
* Panel Actions
**/
renderActions?(): React.ReactNode;
}
export function isMultiSelectedEditableDashboardElement(obj: object): obj is MultiSelectedEditableDashboardElement {
return 'isMultiSelectedEditableDashboardElement' in obj;
}
export interface BulkActionElement {
onDelete(): void;
onCopy?(): void;
}
export function isBulkActionElement(obj: object): obj is BulkActionElement {
return 'onDelete' in obj;
}

@ -635,15 +635,15 @@
},
"counts": {
"alertRule_one": "{{count}} alert rule",
"alertRule_other": "{{count}} alert rules",
"alertRule_other": "{{count}} alert rule",
"dashboard_one": "{{count}} dashboard",
"dashboard_other": "{{count}} dashboards",
"dashboard_other": "{{count}} dashboard",
"folder_one": "{{count}} folder",
"folder_other": "{{count}} folders",
"folder_other": "{{count}} folder",
"libraryPanel_one": "{{count}} library panel",
"libraryPanel_other": "{{count}} library panels",
"libraryPanel_other": "{{count}} library panel",
"total_one": "{{count}} item",
"total_other": "{{count}} items"
"total_other": "{{count}} item"
},
"dashboards-tree": {
"collapse-folder-button": "Collapse folder {{title}}",
@ -912,6 +912,25 @@
}
}
},
"edit-pane": {
"objects": {
"multi-select": {
"selection-number": "No. of objects selected: "
}
},
"panels": {
"multi-select": {
"selection-number": "No. of panels selected: "
}
},
"row": {
"hide": "Hide row header",
"multi-select": {
"options-header": "Multi-selected Row options",
"selection-number": "No. of rows selected: "
}
}
},
"empty": {
"add-library-panel-body": "Add visualizations that are shared with other dashboards.",
"add-library-panel-button": "Add library panel",
@ -2826,7 +2845,7 @@
"shared-dashboard-modal-title": "Shared dashboards"
},
"table-body": {
"dashboard-count_one": "{{count}} dashboard",
"dashboard-count_one": "{{count}} dashboards",
"dashboard-count_other": "{{count}} dashboards"
},
"table-header": {

@ -635,15 +635,15 @@
},
"counts": {
"alertRule_one": "{{count}} äľęřŧ řūľę",
"alertRule_other": "{{count}} äľęřŧ řūľęş",
"alertRule_other": "{{count}} äľęřŧ řūľę",
"dashboard_one": "{{count}} đäşĥþőäřđ",
"dashboard_other": "{{count}} đäşĥþőäřđş",
"dashboard_other": "{{count}} đäşĥþőäřđ",
"folder_one": "{{count}} ƒőľđęř",
"folder_other": "{{count}} ƒőľđęřş",
"folder_other": "{{count}} ƒőľđęř",
"libraryPanel_one": "{{count}} ľįþřäřy päʼnęľ",
"libraryPanel_other": "{{count}} ľįþřäřy päʼnęľş",
"libraryPanel_other": "{{count}} ľįþřäřy päʼnęľ",
"total_one": "{{count}} įŧęm",
"total_other": "{{count}} įŧęmş"
"total_other": "{{count}} įŧęm"
},
"dashboards-tree": {
"collapse-folder-button": "Cőľľäpşę ƒőľđęř {{title}}",
@ -912,6 +912,25 @@
}
}
},
"edit-pane": {
"objects": {
"multi-select": {
"selection-number": "Ńő. őƒ őþĵęčŧş şęľęčŧęđ: "
}
},
"panels": {
"multi-select": {
"selection-number": "Ńő. őƒ päʼnęľş şęľęčŧęđ: "
}
},
"row": {
"hide": "Ħįđę řőŵ ĥęäđęř",
"multi-select": {
"options-header": "Mūľŧį-şęľęčŧęđ Ŗőŵ őpŧįőʼnş",
"selection-number": "Ńő. őƒ řőŵş şęľęčŧęđ: "
}
}
},
"empty": {
"add-library-panel-body": "Åđđ vįşūäľįžäŧįőʼnş ŧĥäŧ äřę şĥäřęđ ŵįŧĥ őŧĥęř đäşĥþőäřđş.",
"add-library-panel-button": "Åđđ ľįþřäřy päʼnęľ",
@ -2826,7 +2845,7 @@
"shared-dashboard-modal-title": "Ŝĥäřęđ đäşĥþőäřđş"
},
"table-body": {
"dashboard-count_one": "{{count}} đäşĥþőäřđ",
"dashboard-count_one": "{{count}} đäşĥþőäřđş",
"dashboard-count_other": "{{count}} đäşĥþőäřđş"
},
"table-header": {

Loading…
Cancel
Save