Dashboard: Auto focus title fields in edit pane (#101668)

* Dashboard: Auto focus title fields in edit pane

* Fix scroll issue

* Update

* No auto focus in panel edit

* Update to implementation

* alt skip fix

* fixed playwright test

* Update

* Fix
pull/101545/head^2
Torkel Ödegaard 4 months ago committed by GitHub
parent b56db69b32
commit 2f104739d7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 23
      public/app/features/dashboard-scene/edit-pane/DashboardEditPane.tsx
  2. 5
      public/app/features/dashboard-scene/edit-pane/shared.ts
  3. 4
      public/app/features/dashboard-scene/panel-edit/getPanelFrameOptions.tsx
  4. 6
      public/app/features/dashboard-scene/scene/DashboardScene.test.tsx
  5. 10
      public/app/features/dashboard-scene/scene/DashboardScene.tsx
  6. 10
      public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx
  7. 13
      public/app/features/dashboard-scene/scene/layout-responsive-grid/ResponsiveGridLayoutManager.tsx
  8. 3
      public/app/features/dashboard-scene/scene/layout-rows/RowItemEditor.tsx
  9. 22
      public/app/features/dashboard-scene/scene/layout-rows/RowsLayoutManager.tsx
  10. 4
      public/app/features/dashboard-scene/scene/layout-tabs/TabItemEditor.tsx
  11. 44
      public/app/features/dashboard-scene/scene/layouts-shared/addNew.ts
  12. 19
      public/app/features/dashboard-scene/scene/layouts-shared/utils.ts
  13. 4
      public/app/features/dashboard-scene/scene/setDashboardPanelContext.ts
  14. 2
      public/app/features/dashboard-scene/utils/utils.ts

@ -15,11 +15,13 @@ 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';
import { ElementEditPane } from './ElementEditPane';
import { ElementSelection } from './ElementSelection';
import { NewObjectAddedToCanvasEvent } from './shared';
import { useEditableElement } from './useEditableElement';
export interface DashboardEditPaneState extends SceneObjectState {
@ -39,6 +41,18 @@ export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
onSelect: (item, multi) => this.selectElement(item, multi),
},
});
this.addActivationHandler(this.onActivate.bind(this));
}
private onActivate() {
const dashboard = getDashboardSceneFor(this);
this._subs.add(
dashboard.subscribeToEvent(NewObjectAddedToCanvasEvent, ({ payload }) => {
this.newObjectAddedToCanvas(payload);
})
);
}
public enableSelection() {
@ -134,7 +148,7 @@ export class DashboardEditPane extends SceneObjectBase<DashboardEditPaneState> {
this.setState({ tab });
};
public newObjectAddedToCanvas(obj: SceneObject) {
private newObjectAddedToCanvas(obj: SceneObject) {
this.selectObject(obj, obj.state.key!, false);
if (this.state.tab !== 'configure') {
@ -173,13 +187,12 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla
const styles = useStyles2(getStyles);
const paneRef = useRef<HTMLDivElement>(null);
const editableElement = useEditableElement(selection, editPane);
const selectedObject = selection?.getFirstObject();
if (!editableElement) {
return null;
}
const { typeId } = editableElement.getEditableElementInfo();
if (isCollapsed) {
return (
<>
@ -196,7 +209,7 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla
{openOverlay && (
<Resizable className={cx(styles.fixed, styles.container)} defaultSize={{ height: '100%', width: '20vw' }}>
<ElementEditPane element={editableElement} key={typeId} />
<ElementEditPane element={editableElement} key={selectedObject?.state.key} />
</Resizable>
)}
</>
@ -224,7 +237,7 @@ export function DashboardEditPaneRenderer({ editPane, isCollapsed, onToggleColla
</TabsBar>
<div className={styles.tabContent}>
{tab === 'add' && <DashboardAddPane editPane={editPane} />}
{tab === 'configure' && <ElementEditPane element={editableElement} key={typeId} />}
{tab === 'configure' && <ElementEditPane element={editableElement} key={selectedObject?.state.key} />}
{tab === 'outline' && <DashboardOutline editPane={editPane} />}
</div>
</div>

@ -1,5 +1,6 @@
import { useSessionStorage } from 'react-use';
import { BusEventWithPayload } from '@grafana/data';
import { SceneGridRow, SceneObject, VizPanel } from '@grafana/scenes';
import { DashboardScene } from '../scene/DashboardScene';
@ -53,3 +54,7 @@ export function hasEditableElement(sceneObj: SceneObject | undefined): boolean {
return false;
}
export class NewObjectAddedToCanvasEvent extends BusEventWithPayload<SceneObject> {
static type = 'new-object-added-to-canvas';
}

@ -1,3 +1,4 @@
import { CoreApp } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { config } from '@grafana/runtime';
import { SceneTimeRangeLike, VizPanel } from '@grafana/scenes';
@ -10,6 +11,7 @@ import { getPanelLinksVariableSuggestions } from 'app/features/panel/panellinks/
import { VizPanelLinks } from '../scene/PanelLinks';
import { PanelTimeRange } from '../scene/PanelTimeRange';
import { useEditPaneInputAutoFocus } from '../scene/layouts-shared/utils';
import { isDashboardLayoutItem } from '../scene/types/DashboardLayoutItem';
import { vizPanelToPanel, transformSceneToSaveModel } from '../serialization/transformSceneToSaveModel';
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
@ -108,9 +110,11 @@ function ScenePanelLinksEditor({ panelLinks }: ScenePanelLinksEditorProps) {
export function PanelFrameTitleInput({ panel }: { panel: VizPanel }) {
const { title } = panel.useState();
let ref = useEditPaneInputAutoFocus({ noAutoFocus: panel.getPanelContext().app === CoreApp.PanelEditor });
return (
<Input
ref={ref}
data-testid={selectors.components.PanelEditor.OptionsPane.fieldInput('Title')}
value={title}
onChange={(e) => setPanelTitle(panel, e.currentTarget.value)}

@ -453,16 +453,22 @@ describe('DashboardScene', () => {
});
it('Should select new panel', () => {
scene.state.editPane.activate();
const panel = scene.onCreateNewPanel();
expect(scene.state.editPane.state.selection?.getFirstObject()).toBe(panel);
});
it('Should select new row', () => {
scene.state.editPane.activate();
const row = scene.onCreateNewRow();
expect(scene.state.editPane.state.selection?.getFirstObject()).toBe(row);
});
it('Should select new tab', () => {
scene.state.editPane.activate();
const tab = scene.onCreateNewTab();
expect(scene.state.editPane.state.selection?.getFirstObject()).toBe(tab);
});

@ -490,8 +490,6 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
// Add panel to layout
this.state.body.addPanel(vizPanel);
// Select panel
this.state.editPane.newObjectAddedToCanvas(vizPanel);
}
public createLibraryPanel(panelToReplace: VizPanel, libPanel: LibraryPanel) {
@ -599,15 +597,11 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> impleme
}
public onCreateNewRow() {
const newRow = addNewRowTo(this.state.body);
this.state.editPane.newObjectAddedToCanvas(newRow);
return newRow;
return addNewRowTo(this.state.body);
}
public onCreateNewTab() {
const tab = addNewTabTo(this.state.body);
this.state.editPane.newObjectAddedToCanvas(tab);
return tab;
return addNewTabTo(this.state.body);
}
public onCreateNewPanel(): VizPanel {

@ -15,6 +15,7 @@ import { GRID_COLUMN_COUNT } from 'app/core/constants';
import { t } from 'app/core/internationalization';
import DashboardEmpty from 'app/features/dashboard/dashgrid/DashboardEmpty';
import { NewObjectAddedToCanvasEvent } from '../../edit-pane/shared';
import { isClonedKey, joinCloneKeys } from '../../utils/clone';
import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph';
import {
@ -77,6 +78,8 @@ export class DefaultGridLayoutManager
this.state.grid.setState({
children: [newGridItem, ...this.state.grid.state.children],
});
this.publishEvent(new NewObjectAddedToCanvasEvent(vizPanel), true);
}
public removePanel(panel: VizPanel) {
@ -132,6 +135,8 @@ export class DefaultGridLayoutManager
// when we duplicate a panel we don't want to clone the alert state
delete panelData.state.data?.alertState;
const newPanel = new VizPanel({ ...panelState, $data: panelData, key: getVizPanelKeyForPanelId(newPanelId) });
newGridItem = new DashboardGridItem({
x: gridItem.state.x,
y: gridItem.state.y,
@ -142,19 +147,20 @@ export class DefaultGridLayoutManager
repeatDirection: gridItem.state.repeatDirection,
maxPerRow: gridItem.state.maxPerRow,
key: getGridItemKeyForPanelId(newPanelId),
body: new VizPanel({ ...panelState, $data: panelData, key: getVizPanelKeyForPanelId(newPanelId) }),
body: newPanel,
});
if (gridItem.parent instanceof SceneGridRow) {
const row = gridItem.parent;
row.setState({ children: [...row.state.children, newGridItem] });
grid.forceRender();
return;
}
grid.setState({ children: [...grid.state.children, newGridItem] });
this.publishEvent(new NewObjectAddedToCanvasEvent(newPanel), true);
}
public getVizPanels(): VizPanel[] {

@ -2,6 +2,7 @@ import { SceneComponentProps, SceneCSSGridLayout, SceneObjectBase, SceneObjectSt
import { t } from 'app/core/internationalization';
import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/PanelEditor/OptionsPaneItemDescriptor';
import { NewObjectAddedToCanvasEvent } from '../../edit-pane/shared';
import { joinCloneKeys } from '../../utils/clone';
import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph';
import { getGridItemKeyForPanelId, getPanelIdForVizPanel, getVizPanelKeyForPanelId } from '../../utils/utils';
@ -60,6 +61,8 @@ export class ResponsiveGridLayoutManager
this.state.layout.setState({
children: [new ResponsiveGridItem({ body: vizPanel }), ...this.state.layout.state.children],
});
this.publishEvent(new NewObjectAddedToCanvasEvent(vizPanel), true);
}
public removePanel(panel: VizPanel) {
@ -77,11 +80,13 @@ export class ResponsiveGridLayoutManager
const newPanelId = dashboardSceneGraph.getNextPanelId(this);
const grid = this.state.layout;
const newPanel = panel.clone({
key: getVizPanelKeyForPanelId(newPanelId),
});
const newGridItem = gridItem.clone({
key: getGridItemKeyForPanelId(newPanelId),
body: panel.clone({
key: getVizPanelKeyForPanelId(newPanelId),
}),
body: newPanel,
});
const sourceIndex = grid.state.children.indexOf(gridItem);
@ -91,6 +96,8 @@ export class ResponsiveGridLayoutManager
newChildren.splice(sourceIndex + 1, 0, newGridItem);
grid.setState({ children: newChildren });
this.publishEvent(new NewObjectAddedToCanvasEvent(newPanel), true);
}
public getVizPanels(): VizPanel[] {

@ -14,6 +14,7 @@ import { EditPaneHeader } from '../../edit-pane/EditPaneHeader';
import { getDashboardSceneFor, getQueryRunnerFor } from '../../utils/utils';
import { DashboardScene } from '../DashboardScene';
import { DashboardLayoutSelector } from '../layouts-shared/DashboardLayoutSelector';
import { useEditPaneInputAutoFocus } from '../layouts-shared/utils';
import { RowItem } from './RowItem';
@ -77,9 +78,11 @@ export function getEditOptions(model: RowItem): OptionsPaneCategoryDescriptor[]
function RowTitleInput({ row }: { row: RowItem }) {
const { title } = row.useState();
const ref = useEditPaneInputAutoFocus();
return (
<Input
ref={ref}
title={t('dashboard.rows-layout.row-options.title-option', 'Title')}
value={title}
onChange={(e) => row.onChangeTitle(e.currentTarget.value)}

@ -1,6 +1,7 @@
import { SceneGridItemLike, SceneGridRow, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes';
import { t } from 'app/core/internationalization';
import { NewObjectAddedToCanvasEvent } from '../../edit-pane/shared';
import { isClonedKey } from '../../utils/clone';
import { dashboardSceneGraph } from '../../utils/dashboardSceneGraph';
import { DashboardGridItem } from '../layout-default/DashboardGridItem';
@ -96,10 +97,14 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> i
}
public addRowAbove(row: RowItem) {
const rows = this.state.rows;
const index = rows.indexOf(row);
rows.splice(index, 0, new RowItem());
this.setState({ rows });
const index = this.state.rows.indexOf(row);
const newRow = new RowItem();
const newRows = [...this.state.rows];
newRows.splice(index, 0, newRow);
this.setState({ rows: newRows });
this.publishEvent(new NewObjectAddedToCanvasEvent(newRow), true);
}
public addRowBelow(row: RowItem) {
@ -111,8 +116,13 @@ export class RowsLayoutManager extends SceneObjectBase<RowsLayoutManagerState> i
index = index + 1;
}
rows.splice(index + 1, 0, new RowItem());
this.setState({ rows });
const newRow = new RowItem();
const newRows = [...this.state.rows];
newRows.splice(index + 1, 0, newRow);
this.setState({ rows: newRows });
this.publishEvent(new NewObjectAddedToCanvasEvent(newRow), true);
}
public removeRow(row: RowItem) {

@ -7,6 +7,7 @@ import { OptionsPaneItemDescriptor } from 'app/features/dashboard/components/Pan
import { EditPaneHeader } from '../../edit-pane/EditPaneHeader';
import { useLayoutCategory } from '../layouts-shared/DashboardLayoutSelector';
import { useEditPaneInputAutoFocus } from '../layouts-shared/utils';
import { TabItem } from './TabItem';
@ -35,6 +36,7 @@ export function getEditOptions(model: TabItem): OptionsPaneCategoryDescriptor[]
function TabTitleInput({ tab }: { tab: TabItem }) {
const { title } = tab.useState();
const ref = useEditPaneInputAutoFocus();
return <Input value={title} onChange={(e) => tab.onChangeTitle(e.currentTarget.value)} />;
return <Input ref={ref} value={title} onChange={(e) => tab.onChangeTitle(e.currentTarget.value)} />;
}

@ -1,18 +1,22 @@
import { SceneGridRow, SceneObject } from '@grafana/scenes';
import { SceneGridRow } from '@grafana/scenes';
import { NewObjectAddedToCanvasEvent } from '../../edit-pane/shared';
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 { DashboardLayoutManager } from '../types/DashboardLayoutManager';
import { isLayoutParent } from '../types/LayoutParent';
export function addNewTabTo(sceneObject: SceneObject): TabItem {
if (sceneObject instanceof TabsLayoutManager) {
return sceneObject.addNewTab();
export function addNewTabTo(layout: DashboardLayoutManager): TabItem {
if (layout instanceof TabsLayoutManager) {
const tab = layout.addNewTab();
layout.publishEvent(new NewObjectAddedToCanvasEvent(tab), true);
return tab;
}
const layoutParent = sceneObject.parent!;
const layoutParent = layout.parent!;
if (!isLayoutParent(layoutParent)) {
throw new Error('Parent layout is not a LayoutParent');
}
@ -20,24 +24,31 @@ export function addNewTabTo(sceneObject: SceneObject): TabItem {
const tabsLayout = TabsLayoutManager.createFromLayout(layoutParent.getLayout());
layoutParent.switchLayout(tabsLayout);
return tabsLayout.state.tabs[0];
const tab = tabsLayout.state.tabs[0];
layout.publishEvent(new NewObjectAddedToCanvasEvent(tab), true);
return tab;
}
export function addNewRowTo(sceneObject: SceneObject): RowItem | SceneGridRow {
if (sceneObject instanceof RowsLayoutManager) {
return sceneObject.addNewRow();
export function addNewRowTo(layout: DashboardLayoutManager): RowItem | SceneGridRow {
if (layout instanceof RowsLayoutManager) {
const row = layout.addNewRow();
layout.publishEvent(new NewObjectAddedToCanvasEvent(row), true);
return row;
}
if (sceneObject instanceof DefaultGridLayoutManager) {
return sceneObject.addNewRow();
if (layout instanceof DefaultGridLayoutManager) {
const row = layout.addNewRow();
layout.publishEvent(new NewObjectAddedToCanvasEvent(row), true);
return row;
}
if (sceneObject instanceof TabsLayoutManager) {
const currentTab = sceneObject.getCurrentTab();
if (layout instanceof TabsLayoutManager) {
const currentTab = layout.getCurrentTab();
return addNewRowTo(currentTab.state.layout);
}
const layoutParent = sceneObject.parent!;
const layoutParent = layout.parent!;
if (!isLayoutParent(layoutParent)) {
throw new Error('Parent layout is not a LayoutParent');
}
@ -45,5 +56,8 @@ export function addNewRowTo(sceneObject: SceneObject): RowItem | SceneGridRow {
const rowsLayout = RowsLayoutManager.createFromLayout(layoutParent.getLayout());
layoutParent.switchLayout(rowsLayout);
return rowsLayout.state.rows[0];
const row = rowsLayout.state.rows[0];
layout.publishEvent(new NewObjectAddedToCanvasEvent(row), true);
return row;
}

@ -1,3 +1,5 @@
import { useEffect, useRef } from 'react';
import { SceneObject } from '@grafana/scenes';
import { DashboardLayoutManager, isDashboardLayoutManager } from '../types/DashboardLayoutManager';
@ -15,3 +17,20 @@ export function findParentLayout(sceneObject: SceneObject): DashboardLayoutManag
return null;
}
export interface EditPaneInputAutoFocusProps {
noAutoFocus?: boolean;
}
export function useEditPaneInputAutoFocus({ noAutoFocus }: EditPaneInputAutoFocusProps = {}) {
const ref = useRef<HTMLInputElement>(null);
useEffect(() => {
if (ref.current && !noAutoFocus) {
// Need the setTimeout here for some reason
setTimeout(() => ref.current?.focus(), 200);
}
}, [noAutoFocus]);
return ref;
}

@ -12,10 +12,10 @@ import { DashboardScene } from './DashboardScene';
export function setDashboardPanelContext(vizPanel: VizPanel, context: PanelContext) {
const dashboard = getDashboardSceneFor(vizPanel);
context.app = dashboard.state.isEditing ? CoreApp.PanelEditor : CoreApp.Dashboard;
context.app = dashboard.state.editPanel ? CoreApp.PanelEditor : CoreApp.Dashboard;
dashboard.subscribeToState((state) => {
if (state.isEditing) {
if (state.editPanel) {
context.app = CoreApp.PanelEditor;
} else {
context.app = CoreApp.Dashboard;

@ -19,6 +19,7 @@ import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
import { VizPanelLinks, VizPanelLinksMenu } from '../scene/PanelLinks';
import { panelMenuBehavior } from '../scene/PanelMenuBehavior';
import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem';
import { setDashboardPanelContext } from '../scene/setDashboardPanelContext';
import { DashboardLayoutManager, isDashboardLayoutManager } from '../scene/types/DashboardLayoutManager';
import { containsCloneKey, getLastKeyFromClone, getOriginalKey, isInCloneChain } from './clone';
@ -323,6 +324,7 @@ export function getDefaultVizPanel(): VizPanel {
titleItems: [new VizPanelLinks({ menu: new VizPanelLinksMenu({}) })],
hoverHeaderOffset: 0,
$behaviors: [],
extendPanelContext: setDashboardPanelContext,
menu: new VizPanelMenu({
$behaviors: [panelMenuBehavior],
}),

Loading…
Cancel
Save