Dashboards: Select objects after adding them (#101609)
* Dashboards: Select newly added object * Add tests * Update
This commit is contained in:
@@ -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": "Ŧįŧľę"
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user