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

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

@ -452,6 +452,21 @@ describe('DashboardScene', () => {
expect(panel.state.key).toBe('panel-7'); 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', () => { it('Should fail to copy a panel if it does not have a grid item parent', () => {
const vizPanel = new VizPanel({ const vizPanel = new VizPanel({
title: 'Panel Title', title: 'Panel Title',

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

@ -1,7 +1,8 @@
import { useMemo } from 'react'; import { useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; 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 { t, Trans } from 'app/core/internationalization';
import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor'; import { OptionsPaneCategoryDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneCategoryDescriptor';
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor'; import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
@ -22,19 +23,25 @@ export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[]
const dashboard = getDashboardSceneFor(model); const dashboard = getDashboardSceneFor(model);
const editPaneHeaderOptions = new OptionsPaneCategoryDescriptor({ const editPaneHeaderOptions = new OptionsPaneCategoryDescriptor({
title: t('dashboard.rows-layout.row-options.title', 'Row'), title: t('dashboard.rows-layout.item-name', 'Row'),
id: 'row-options', id: 'row-options',
isOpenable: false, isOpenable: false,
renderTitle: () => ( 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( .addItem(
new OptionsPaneItemDescriptor({ new OptionsPaneItemDescriptor({
title: t('dashboard.rows-layout.row-options.title-option', 'Title'), title: t('dashboard.rows-layout.option.title', 'Title'),
render: () => <RowTitleInput row={model} />, render: () => <RowTitleInput row={model} />,
}) })
) )
.addItem(
new OptionsPaneItemDescriptor({
title: t('dashboard.rows-layout.option.height', 'Height'),
render: () => <RowHeightSelect row={model} />,
})
)
.addItem( .addItem(
new OptionsPaneItemDescriptor({ new OptionsPaneItemDescriptor({
title: t('dashboard.layout.common.layout', 'Layout'), title: t('dashboard.layout.common.layout', 'Layout'),
@ -51,13 +58,13 @@ export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[]
editPaneHeaderOptions editPaneHeaderOptions
.addItem( .addItem(
new OptionsPaneItemDescriptor({ 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} />, render: () => <RowRepeatSelect row={model} dashboard={dashboard} />,
}) })
) )
.addItem( .addItem(
new OptionsPaneItemDescriptor({ 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} />, render: () => <RowHeaderSwitch row={model} />,
}) })
); );
@ -86,6 +93,17 @@ function RowHeaderSwitch({ row }: { row: RowItem }) {
return <Switch value={isHeaderHidden} onChange={() => row.onHeaderHiddenToggle()} />; 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 }) { function RowRepeatSelect({ row, dashboard }: { row: RowItem; dashboard: DashboardScene }) {
const { layout } = row.useState(); const { layout } = row.useState();

@ -14,7 +14,7 @@ import { RowItem } from './RowItem';
import { RowItemMenu } from './RowItemMenu'; import { RowItemMenu } from './RowItemMenu';
export function RowItemRenderer({ model }: SceneComponentProps<RowItem>) { 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 isClone = useMemo(() => isClonedKey(key!), [key]);
const dashboard = getDashboardSceneFor(model); const dashboard = getDashboardSceneFor(model);
const { isEditing, showHiddenElements } = dashboard.useState(); const { isEditing, showHiddenElements } = dashboard.useState();

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

@ -86,8 +86,9 @@ export class TabsLayoutManager extends SceneObjectBase<TabsLayoutManagerState> i
} }
public addNewTab() { public addNewTab() {
const currentTab = new TabItem(); const newTab = new TabItem();
this.setState({ tabs: [...this.state.tabs, currentTab], currentTabIndex: this.state.tabs.length }); this.setState({ tabs: [...this.state.tabs, newTab], currentTabIndex: this.state.tabs.length });
return newTab;
} }
public editModeChanged(isEditing: boolean) { 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 { DefaultGridLayoutManager } from '../layout-default/DefaultGridLayoutManager';
import { RowItem } from '../layout-rows/RowItem';
import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager'; import { RowsLayoutManager } from '../layout-rows/RowsLayoutManager';
import { TabItem } from '../layout-tabs/TabItem';
import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager'; import { TabsLayoutManager } from '../layout-tabs/TabsLayoutManager';
import { isLayoutParent } from '../types/LayoutParent'; import { isLayoutParent } from '../types/LayoutParent';
export function addNewTabTo(sceneObject: SceneObject) { export function addNewTabTo(sceneObject: SceneObject): TabItem {
if (sceneObject instanceof TabsLayoutManager) { if (sceneObject instanceof TabsLayoutManager) {
sceneObject.addNewTab(); return sceneObject.addNewTab();
return;
} }
const layoutParent = sceneObject.parent!; const layoutParent = sceneObject.parent!;
@ -16,24 +17,24 @@ export function addNewTabTo(sceneObject: SceneObject) {
throw new Error('Parent layout is not a LayoutParent'); 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) { if (sceneObject instanceof RowsLayoutManager) {
sceneObject.addNewRow(); return sceneObject.addNewRow();
return;
} }
if (sceneObject instanceof DefaultGridLayoutManager) { if (sceneObject instanceof DefaultGridLayoutManager) {
sceneObject.addNewRow(); return sceneObject.addNewRow();
return;
} }
if (sceneObject instanceof TabsLayoutManager) { if (sceneObject instanceof TabsLayoutManager) {
const currentTab = sceneObject.getCurrentTab(); const currentTab = sceneObject.getCurrentTab();
addNewRowTo(currentTab.state.layout); return addNewRowTo(currentTab.state.layout);
return;
} }
const layoutParent = sceneObject.parent!; const layoutParent = sceneObject.parent!;
@ -41,5 +42,8 @@ export function addNewRowTo(sceneObject: SceneObject) {
throw new Error('Parent layout is not a LayoutParent'); 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": { "rows-layout": {
"description": "Rows layout", "description": "Rows layout",
"item-name": "Row",
"name": "Rows", "name": "Rows",
"option": {
"height": "Height",
"hide-header": "Hide row header",
"repeat": "Repeat for",
"title": "Title"
},
"options": {
"height-expand": "Expand",
"height-min": "Min"
},
"row": { "row": {
"collapse": "Collapse row", "collapse": "Collapse row",
"expand": "Expand row", "expand": "Expand row",
@ -1234,15 +1245,6 @@
} }
}, },
"row-options": { "row-options": {
"height": {
"hide-row-header": "Hide row header"
},
"repeat": {
"variable": {
"title": "Repeat for"
}
},
"title": "Row",
"title-option": "Title" "title-option": "Title"
} }
}, },

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

Loading…
Cancel
Save