Dashboards: Select objects after adding them (#101609)

* Dashboards: Select newly added object

* Add tests

* Update
pull/101673/head
Torkel Ödegaard 4 months ago committed by GitHub
parent a1d5e5dad1
commit 1576b69f65
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 38
      public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx
  2. 10
      public/app/features/dashboard-scene/edit-pane/useEditableElement.ts
  3. 15
      public/app/features/dashboard-scene/scene/DashboardScene.test.tsx
  4. 13
      public/app/features/dashboard-scene/scene/DashboardScene.tsx
  5. 30
      public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx
  6. 2
      public/app/features/dashboard-scene/scene/layout-rows/RowItemRenderer.tsx
  7. 6
      public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx
  8. 5
      public/app/features/dashboard-scene/scene/layout-tabs/TabsLayoutManager.tsx
  9. 30
      public/app/features/dashboard-scene/scene/layouts-shared/addNew.ts
  10. 20
      public/locales/en-US/grafana.json
  11. 20
      public/locales/pseudo-LOCALE/grafana.json

@ -15,7 +15,6 @@ import {
import { t } from 'app/core/internationalization';
import { isInCloneChain } from '../utils/clone';
import { getDashboardSceneFor } from '../utils/utils';
import { DashboardAddPane } from './DashboardAddPane';
import { DashboardOutline } from './DashboardOutline';
@ -72,22 +71,20 @@ export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
}
public selectObject(obj: SceneObject, id: string, multi?: boolean) {
if (!this.state.selection) {
return;
}
const prevItem = this.state.selection.getFirstObject();
const prevItem = this.state.selection?.getFirstObject();
if (prevItem === obj && !multi) {
this.clearSelection();
return;
}
if (multi && this.state.selection.hasValue(id)) {
if (multi && this.state.selection?.hasValue(id)) {
this.removeMultiSelectedObject(id);
return;
}
const { selection, contextItems: selected } = this.state.selection.getStateWithValue(id, obj, !!multi);
const elementSelection = this.state.selection ?? new ElementSelection([[id, obj.getRef()]]);
const { selection, contextItems: selected } = elementSelection.getStateWithValue(id, obj, !!multi);
this.setState({
selection: new ElementSelection(selection),
@ -120,14 +117,12 @@ export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
}
public clearSelection() {
const dashboard = getDashboardSceneFor(this);
if (this.state.selection?.getFirstObject() === dashboard) {
if (!this.state.selection) {
return;
}
this.setState({
selection: new ElementSelection([[dashboard.state.uid!, dashboard.getRef()]]),
selection: undefined,
selectionContext: {
...this.state.selectionContext,
selected: [],
@ -138,6 +133,14 @@ export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
public onChangeTab = (tab: EditPaneTab) => {
this.setState({ tab });
};
public newObjectAddedToCanvas(obj: SceneObject) {
this.selectObject(obj, obj.state.key!, false);
if (this.state.tab !== 'configure') {
this.onChangeTab('configure');
}
}
}
export interface Props {
@ -153,13 +156,6 @@ export interface Props {
export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleCollapse, openOverlay }: Props) {
// Activate the edit pane
useEffect(() => {
if (!editPane.state.selection) {
const dashboard = getDashboardSceneFor(editPane);
editPane.setState({
selection: new ElementSelection([[dashboard.state.uid!, dashboard.getRef()]]),
});
}
editPane.enableSelection();
return () => {
@ -168,7 +164,7 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla
}, [editPane]);
useEffect(() => {
if (isCollapsed && editPane.state.selection?.getSelectionEntries().length) {
if (isCollapsed) {
editPane.clearSelection();
}
}, [editPane, isCollapsed]);
@ -176,7 +172,7 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla
const { selection, tab = 'configure' } = useSceneObjectState(editPane, { shouldActivateOrKeepAlive: true });
const styles = useStyles2(getStyles);
const paneRef = useRef<HTMLDivElement>(null);
const editableElement = useEditableElement(selection);
const editableElement = useEditableElement(selection, editPane);
if (!editableElement) {
return null;

@ -2,17 +2,21 @@ import { useMemo } from 'react';
import { EditableDashboardElement } from '../scene/types/EditableDashboardElement';
import { MultiSelectedEditableDashboardElement } from '../scene/types/MultiSelectedEditableDashboardElement';
import { getDashboardSceneFor } from '../utils/utils';
import { DashboardEditPane } from './DashboardEditPane';
import { ElementSelection } from './ElementSelection';
export function useEditableElement(
selection: ElementSelection | undefined
selection: ElementSelection | undefined,
editPane: DashboardEditPane
): EditableDashboardElement | MultiSelectedEditableDashboardElement | undefined {
return useMemo(() => {
if (!selection) {
return undefined;
const dashboard = getDashboardSceneFor(editPane);
return new ElementSelection([[dashboard.state.uid!, dashboard.getRef()]]).createSelectionElement();
}
return selection.createSelectionElement();
}, [selection]);
}, [selection, editPane]);
}

@ -452,6 +452,21 @@ describe('DashboardScene', () => {
expect(panel.state.key).toBe('panel-7');
});
it('Should select new panel', () => {
const panel = scene.onCreateNewPanel();
expect(scene.state.editPane.state.selection?.getFirstObject()).toBe(panel);
});
it('Should select new row', () => {
const row = scene.onCreateNewRow();
expect(scene.state.editPane.state.selection?.getFirstObject()).toBe(row);
});
it('Should select new tab', () => {
const tab = scene.onCreateNewTab();
expect(scene.state.editPane.state.selection?.getFirstObject()).toBe(tab);
});
it('Should fail to copy a panel if it does not have a grid item parent', () => {
const vizPanel = new VizPanel({
title: 'Panel Title',

@ -488,7 +488,10 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
this.onEnterEditMode();
}
// Add panel to layout
this.state.body.addPanel(vizPanel);
// Select panel
this.state.editPane.newObjectAddedToCanvas(vizPanel);
}
public createLibraryPanel(panelToReplace: VizPanel, libPanel: LibraryPanel) {
@ -596,18 +599,20 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
}
public onCreateNewRow() {
addNewRowTo(this.state.body);
const newRow = addNewRowTo(this.state.body);
this.state.editPane.newObjectAddedToCanvas(newRow);
return newRow;
}
public onCreateNewTab() {
addNewTabTo(this.state.body);
const tab = addNewTabTo(this.state.body);
this.state.editPane.newObjectAddedToCanvas(tab);
return tab;
}
public onCreateNewPanel(): VizPanel {
const vizPanel = getDefaultVizPanel();
this.addPanel(vizPanel);
return vizPanel;
}

@ -1,7 +1,8 @@
import { useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Alert, Input, Switch, TextLink } from '@grafana/ui';
import { Alert, Input, RadioButtonGroup, Switch, TextLink } 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';
@ -22,19 +23,25 @@ export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[]
const dashboard = getDashboardSceneFor(model);
const editPaneHeaderOptions = new OptionsPaneCategoryDescriptor({
title: t('dashboard.rows-layout.row-options.title', 'Row'),
title: t('dashboard.rows-layout.item-name', 'Row'),
id: 'row-options',
isOpenable: false,
renderTitle: () => (
<EditPaneHeader title={t('dashboard.rows-layout.row-options.title', 'Row')} onDelete={() => model.onDelete()} />
<EditPaneHeader title={t('dashboard.rows-layout.item-name', 'Row')} onDelete={() => model.onDelete()} />
),
})
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.rows-layout.row-options.title-option', 'Title'),
title: t('dashboard.rows-layout.option.title', 'Title'),
render: () => <RowTitleInput row={model} />,
})
)
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.rows-layout.option.height', 'Height'),
render: () => <RowHeightSelect row={model} />,
})
)
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.layout.common.layout', 'Layout'),
@ -51,13 +58,13 @@ export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[]
editPaneHeaderOptions
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.rows-layout.row-options.repeat.variable.title', 'Repeat for'),
title: t('dashboard.rows-layout.option.repeat', 'Repeat for'),
render: () => <RowRepeatSelect row={model} dashboard={dashboard} />,
})
)
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.rows-layout.row-options.height.hide-row-header', 'Hide row header'),
title: t('dashboard.rows-layout.option.hide-header', 'Hide row header'),
render: () => <RowHeaderSwitch row={model} />,
})
);
@ -86,6 +93,17 @@ function RowHeaderSwitch({ row }: { row: RowItem }) {
return <Switch value={isHeaderHidden} onChange={() => row.onHeaderHiddenToggle()} />;
}
function RowHeightSelect({ row }: { row: RowItem }) {
const { height = 'min' } = row.useState();
const options: Array<SelectableValue<'expand' | 'min'>> = [
{ label: t('dashboard.rows-layout.options.height-expand', 'Expand'), value: 'expand' },
{ label: t('dashboard.rows-layout.options.height-min', 'Min'), value: 'min' },
];
return <RadioButtonGroup options={options} value={height} onChange={(option) => row.onChangeHeight(option)} />;
}
function RowRepeatSelect({ row, dashboard }: { row: RowItem; dashboard: DashboardScene }) {
const { layout } = row.useState();

@ -14,7 +14,7 @@ import { RowItem } from './RowItem';
import { RowItemMenu } from './RowItemMenu';
export function RowItemRenderer({ model }: SceneComponentProps<RowItem>) {
const { layout, title, isCollapsed, height = 'expand', isHeaderHidden, key } = model.useState();
const { layout, title, isCollapsed, height = 'min', isHeaderHidden, key } = model.useState();
const isClone = useMemo(() => isClonedKey(key!), [key]);
const dashboard = getDashboardSceneFor(model);
const { isEditing, showHiddenElements } = dashboard.useState();

@ -69,8 +69,10 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> i
throw new Error('Method not implemented.');
}
public addNewRow() {
this.setState({ rows: [...this.state.rows, new RowItem()] });
public addNewRow(): RowItem {
const row = new RowItem();
this.setState({ rows: [...this.state.rows, row] });
return row;
}
public editModeChanged(isEditing: boolean) {

@ -86,8 +86,9 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> i
}
public addNewTab() {
const currentTab = new TabItem();
this.setState({ tabs: [...this.state.tabs, currentTab], currentTabIndex: this.state.tabs.length });
const newTab = new TabItem();
this.setState({ tabs: [...this.state.tabs, newTab], currentTabIndex: this.state.tabs.length });
return newTab;
}
public editModeChanged(isEditing: boolean) {

@ -1,14 +1,15 @@
import { SceneObject } from '@grafana/scenes';
import { SceneGridRow, SceneObject } from '@grafana/scenes';
import { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager';
import { RowItem } from '../layout-rows/RowItem';
import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager';
import { TabItem } from '../layout-tabs/TabItem';
import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager';
import { isLayoutParent } from '../types/LayoutParent';
export function addNewTabTo(sceneObject: SceneObject) {
export function addNewTabTo(sceneObject: SceneObject): TabItem {
if (sceneObject instanceof TabsLayoutManager) {
sceneObject.addNewTab();
return;
return sceneObject.addNewTab();
}
const layoutParent = sceneObject.parent!;
@ -16,24 +17,24 @@ export function addNewTabTo(sceneObject: SceneObject) {
throw new Error('Parent layout is not a LayoutParent');
}
layoutParent.switchLayout(TabsLayoutManager.createFromLayout(layoutParent.getLayout()));
const tabsLayout = TabsLayoutManager.createFromLayout(layoutParent.getLayout());
layoutParent.switchLayout(tabsLayout);
return tabsLayout.state.tabs[0];
}
export function addNewRowTo(sceneObject: SceneObject) {
export function addNewRowTo(sceneObject: SceneObject): RowItem | SceneGridRow {
if (sceneObject instanceof RowsLayoutManager) {
sceneObject.addNewRow();
return;
return sceneObject.addNewRow();
}
if (sceneObject instanceof DefaultGridLayoutManager) {
sceneObject.addNewRow();
return;
return sceneObject.addNewRow();
}
if (sceneObject instanceof TabsLayoutManager) {
const currentTab = sceneObject.getCurrentTab();
addNewRowTo(currentTab.state.layout);
return;
return addNewRowTo(currentTab.state.layout);
}
const layoutParent = sceneObject.parent!;
@ -41,5 +42,8 @@ export function addNewRowTo(sceneObject: SceneObject) {
throw new Error('Parent layout is not a LayoutParent');
}
layoutParent.switchLayout(RowsLayoutManager.createFromLayout(layoutParent.getLayout()));
const rowsLayout = RowsLayoutManager.createFromLayout(layoutParent.getLayout());
layoutParent.switchLayout(rowsLayout);
return rowsLayout.state.rows[0];
}

@ -1214,7 +1214,18 @@
},
"rows-layout": {
"description": "Rows layout",
"item-name": "Row",
"name": "Rows",
"option": {
"height": "Height",
"hide-header": "Hide row header",
"repeat": "Repeat for",
"title": "Title"
},
"options": {
"height-expand": "Expand",
"height-min": "Min"
},
"row": {
"collapse": "Collapse row",
"expand": "Expand row",
@ -1234,15 +1245,6 @@
}
},
"row-options": {
"height": {
"hide-row-header": "Hide row header"
},
"repeat": {
"variable": {
"title": "Repeat for"
}
},
"title": "Row",
"title-option": "Title"
}
},

@ -1214,7 +1214,18 @@
},
"rows-layout": {
"description": "Ŗőŵş ľäyőūŧ",
"item-name": "Ŗőŵ",
"name": "Ŗőŵş",
"option": {
"height": "Ħęįģĥŧ",
"hide-header": "Ħįđę řőŵ ĥęäđęř",
"repeat": "Ŗępęäŧ ƒőř",
"title": "Ŧįŧľę"
},
"options": {
"height-expand": "Ēχpäʼnđ",
"height-min": "Mįʼn"
},
"row": {
"collapse": "Cőľľäpşę řőŵ",
"expand": "Ēχpäʼnđ řőŵ",
@ -1234,15 +1245,6 @@
}
},
"row-options": {
"height": {
"hide-row-header": "Ħįđę řőŵ ĥęäđęř"
},
"repeat": {
"variable": {
"title": "Ŗępęäŧ ƒőř"
}
},
"title": "Ŗőŵ",
"title-option": "Ŧįŧľę"
}
},

Loading…
Cancel
Save