DashboardScene: Panel edit use new splitter and new conditional data and options pane logic (#82377)

Rebase
This commit is contained in:
Torkel Ödegaard
2024-02-14 13:37:52 +01:00
committed by GitHub
parent 70aa8fe6b3
commit 526916e10f
8 changed files with 112 additions and 128 deletions

View File

@@ -144,7 +144,7 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
const currentTab = tabs.find((t) => t.tabId === tab); const currentTab = tabs.find((t) => t.tabId === tab);
return ( return (
<> <div className={styles.dataPane}>
<TabsBar hideBorder={true} className={styles.tabsBar}> <TabsBar hideBorder={true} className={styles.tabsBar}>
{tabs.map((t, index) => { {tabs.map((t, index) => {
return ( return (
@@ -161,12 +161,19 @@ function PanelDataPaneRendered({ model }: SceneComponentProps<PanelDataPane>) {
<Container>{currentTab && <currentTab.Component model={currentTab} />}</Container> <Container>{currentTab && <currentTab.Component model={currentTab} />}</Container>
</TabContent> </TabContent>
</CustomScrollbar> </CustomScrollbar>
</> </div>
); );
} }
function getStyles(theme: GrafanaTheme2) { function getStyles(theme: GrafanaTheme2) {
return { return {
dataPane: css({
display: 'flex',
flexDirection: 'column',
flexGrow: 1,
minHeight: 0,
height: '100%',
}),
tabContent: css({ tabContent: css({
padding: theme.spacing(2), padding: theme.spacing(2),
border: `1px solid ${theme.colors.border.weak}`, border: `1px solid ${theme.colors.border.weak}`,

View File

@@ -1,10 +1,9 @@
import { PanelPlugin, PanelPluginMeta, PluginType } from '@grafana/data'; import { PanelPlugin, PanelPluginMeta, PluginType } from '@grafana/data';
import { SceneFlexItem, SceneGridItem, SceneGridLayout, SplitLayout, VizPanel } from '@grafana/scenes'; import { SceneGridItem, SceneGridLayout, VizPanel } from '@grafana/scenes';
import { DashboardScene } from '../scene/DashboardScene'; import { DashboardScene } from '../scene/DashboardScene';
import { activateFullSceneTree } from '../utils/test-utils'; import { activateFullSceneTree } from '../utils/test-utils';
import { PanelDataPane } from './PanelDataPane/PanelDataPane';
import { buildPanelEditScene } from './PanelEditor'; import { buildPanelEditScene } from './PanelEditor';
let pluginToLoad: PanelPlugin | undefined; let pluginToLoad: PanelPlugin | undefined;
@@ -47,8 +46,7 @@ describe('PanelEditor', () => {
const deactivate = activateFullSceneTree(scene); const deactivate = activateFullSceneTree(scene);
const vizManager = editScene.state.panelRef.resolve(); editScene.state.vizManager.state.panel.setState({ title: 'changed title' });
vizManager.state.panel.setState({ title: 'changed title' });
deactivate(); deactivate();
@@ -72,7 +70,7 @@ describe('PanelEditor', () => {
activateFullSceneTree(scene); activateFullSceneTree(scene);
expect(((editScene.state.body as SplitLayout).state.primary as SplitLayout).state.secondary).toBeUndefined(); expect(editScene.state.dataPane).toBeUndefined();
}); });
it('should exist if panel is supporting querying', () => { it('should exist if panel is supporting querying', () => {
@@ -88,10 +86,7 @@ describe('PanelEditor', () => {
}); });
activateFullSceneTree(scene); activateFullSceneTree(scene);
const secondaryPane = ((editScene.state.body as SplitLayout).state.primary as SplitLayout).state.secondary; expect(editScene.state.dataPane).toBeDefined();
expect(secondaryPane).toBeInstanceOf(SceneFlexItem);
expect((secondaryPane as SceneFlexItem).state.body).toBeInstanceOf(PanelDataPane);
}); });
}); });
}); });

View File

@@ -2,17 +2,7 @@ import * as H from 'history';
import { NavIndex } from '@grafana/data'; import { NavIndex } from '@grafana/data';
import { config, locationService } from '@grafana/runtime'; import { config, locationService } from '@grafana/runtime';
import { import { SceneGridItem, SceneObject, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes';
SceneFlexItem,
SceneFlexLayout,
SceneGridItem,
SceneObject,
SceneObjectBase,
SceneObjectRef,
SceneObjectState,
SplitLayout,
VizPanel,
} from '@grafana/scenes';
import { import {
findVizPanelByKey, findVizPanelByKey,
@@ -27,11 +17,12 @@ import { PanelOptionsPane } from './PanelOptionsPane';
import { VizPanelManager } from './VizPanelManager'; import { VizPanelManager } from './VizPanelManager';
export interface PanelEditorState extends SceneObjectState { export interface PanelEditorState extends SceneObjectState {
body: SceneObject;
controls?: SceneObject[]; controls?: SceneObject[];
isDirty?: boolean; isDirty?: boolean;
panelId: number; panelId: number;
panelRef: SceneObjectRef<VizPanelManager>; optionsPane?: PanelOptionsPane;
dataPane?: PanelDataPane;
vizManager: VizPanelManager;
} }
export class PanelEditor extends SceneObjectBase<PanelEditorState> { export class PanelEditor extends SceneObjectBase<PanelEditorState> {
@@ -42,14 +33,41 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
public constructor(state: PanelEditorState) { public constructor(state: PanelEditorState) {
super(state); super(state);
this.addActivationHandler(() => { this.addActivationHandler(this._activationHandler.bind(this));
return () => {
if (!this._discardChanges) {
this.commitChanges();
}
};
});
} }
private _activationHandler() {
const panelManager = this.state.vizManager;
const panel = panelManager.state.panel;
this._subs.add(
panelManager.subscribeToState((n, p) => {
if (n.panel.state.pluginId !== p.panel.state.pluginId) {
this._initDataPane(n.panel.state.pluginId);
}
})
);
this._initDataPane(panel.state.pluginId);
return () => {
if (!this._discardChanges) {
this.commitChanges();
}
};
}
private _initDataPane(pluginId: string) {
const skipDataQuery = config.panels[pluginId].skipDataQuery;
if (!skipDataQuery && !this.state.dataPane) {
this.setState({ dataPane: new PanelDataPane(this.state.vizManager) });
} else if (this.state.dataPane) {
locationService.partial({ tab: null }, true);
this.setState({ dataPane: undefined });
}
}
public getUrlKey() { public getUrlKey() {
return this.state.panelId.toString(); return this.state.panelId.toString();
} }
@@ -76,10 +94,20 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
dashboard.onEnterEditMode(); dashboard.onEnterEditMode();
} }
const panelMngr = this.state.panelRef.resolve();
if (sourcePanel!.parent instanceof SceneGridItem) { if (sourcePanel!.parent instanceof SceneGridItem) {
sourcePanel!.parent.setState({ body: panelMngr.state.panel.clone() }); sourcePanel!.parent.setState({ body: this.state.vizManager.state.panel.clone() });
}
}
public toggleOptionsPane(withOpenVizPicker?: boolean) {
if (this.state.optionsPane) {
this.setState({ optionsPane: undefined });
} else {
this.setState({
optionsPane: new PanelOptionsPane({
isVizPickerOpen: withOpenVizPicker,
}),
});
} }
} }
} }
@@ -90,78 +118,7 @@ export function buildPanelEditScene(panel: VizPanel): PanelEditor {
return new PanelEditor({ return new PanelEditor({
panelId: getPanelIdForVizPanel(panel), panelId: getPanelIdForVizPanel(panel),
panelRef: vizPanelMgr.getRef(), optionsPane: new PanelOptionsPane({}),
body: new SplitLayout({ vizManager: vizPanelMgr,
direction: 'row',
initialSize: 0.75,
primary: new SplitLayout({
direction: 'column',
$behaviors: [conditionalDataPaneBehavior],
primary: new SceneFlexLayout({
direction: 'column',
minHeight: 200,
children: [vizPanelMgr],
}),
primaryPaneStyles: {
minHeight: 0,
overflow: 'hidden',
},
secondaryPaneStyles: {
minHeight: 0,
},
}),
secondary: new SceneFlexItem({
body: new PanelOptionsPane(vizPanelMgr),
width: '100%',
}),
primaryPaneStyles: {
minWidth: '0',
},
secondaryPaneStyles: {
minWidth: '0',
},
}),
}); });
} }
// This function is used to conditionally add the data pane to the panel editor,
// depending on the type of a panel being edited.
function conditionalDataPaneBehavior(scene: SplitLayout) {
const dashboard = getDashboardSceneFor(scene);
const editor = dashboard.state.editPanel;
if (!editor) {
return;
}
const panelManager = editor.state.panelRef.resolve();
const panel = panelManager.state.panel;
const getDataPane = () =>
new SceneFlexItem({
body: new PanelDataPane(panelManager),
});
if (!config.panels[panel.state.pluginId].skipDataQuery) {
scene.setState({
secondary: getDataPane(),
});
}
const sub = panelManager.subscribeToState((n, p) => {
const hadDataSupport = !config.panels[p.panel.state.pluginId].skipDataQuery;
const willHaveDataSupport = !config.panels[n.panel.state.pluginId].skipDataQuery;
if (hadDataSupport && !willHaveDataSupport) {
locationService.partial({ tab: null }, true);
scene.setState({ secondary: undefined });
} else if (!hadDataSupport && willHaveDataSupport) {
scene.setState({ secondary: getDataPane() });
}
});
return () => {
sub.unsubscribe();
};
}

View File

@@ -3,16 +3,17 @@ import React from 'react';
import { GrafanaTheme2 } from '@grafana/data'; import { GrafanaTheme2 } from '@grafana/data';
import { SceneComponentProps } from '@grafana/scenes'; import { SceneComponentProps } from '@grafana/scenes';
import { useStyles2 } from '@grafana/ui'; import { Splitter, useStyles2 } from '@grafana/ui';
import { NavToolbarActions } from '../scene/NavToolbarActions'; import { NavToolbarActions } from '../scene/NavToolbarActions';
import { getDashboardSceneFor } from '../utils/utils'; import { getDashboardSceneFor } from '../utils/utils';
import { PanelEditor } from './PanelEditor'; import { PanelEditor } from './PanelEditor';
import { VisualizationButton } from './PanelOptionsPane';
export function PanelEditorRenderer({ model }: SceneComponentProps<PanelEditor>) { export function PanelEditorRenderer({ model }: SceneComponentProps<PanelEditor>) {
const dashboard = getDashboardSceneFor(model); const dashboard = getDashboardSceneFor(model);
const { body } = model.useState(); const { optionsPane, vizManager, dataPane } = model.useState();
const { controls } = dashboard.useState(); const { controls } = dashboard.useState();
const styles = useStyles2(getStyles); const styles = useStyles2(getStyles);
@@ -25,10 +26,34 @@ export function PanelEditorRenderer({ model }: SceneComponentProps<PanelEditor>)
{controls.map((control) => ( {controls.map((control) => (
<control.Component key={control.state.key} model={control} /> <control.Component key={control.state.key} model={control} />
))} ))}
{!optionsPane && (
<VisualizationButton
pluginId={vizManager.state.panel.state.pluginId}
onOpen={() => model.toggleOptionsPane(true)}
isOpen={false}
onTogglePane={() => model.toggleOptionsPane()}
/>
)}
</div> </div>
)} )}
<div className={styles.body}> <div className={styles.body}>
<body.Component model={body} /> <Splitter
direction="row"
dragPosition="end"
initialSize={0.75}
primaryPaneStyles={{ paddingBottom: !dataPane ? 16 : 0 }}
>
<Splitter
direction="column"
primaryPaneStyles={{ minHeight: 0, paddingRight: !optionsPane ? 16 : 0 }}
secondaryPaneStyles={{ minHeight: 0, overflow: 'hidden' }}
dragPosition="start"
>
<vizManager.Component model={vizManager} />
{dataPane && <dataPane.Component model={dataPane} />}
</Splitter>
{optionsPane && <optionsPane.Component model={optionsPane} />}
</Splitter>
</div> </div>
</div> </div>
</> </>

View File

@@ -11,8 +11,8 @@ import { getPanelFrameCategory2 } from 'app/features/dashboard/components/PanelE
import { getVisualizationOptions2 } from 'app/features/dashboard/components/PanelEditor/getVisualizationOptions'; import { getVisualizationOptions2 } from 'app/features/dashboard/components/PanelEditor/getVisualizationOptions';
import { getAllPanelPluginMeta } from 'app/features/panel/state/util'; import { getAllPanelPluginMeta } from 'app/features/panel/state/util';
import { PanelEditor } from './PanelEditor';
import { PanelVizTypePicker } from './PanelVizTypePicker'; import { PanelVizTypePicker } from './PanelVizTypePicker';
import { VizPanelManager } from './VizPanelManager';
export interface PanelOptionsPaneState extends SceneObjectState { export interface PanelOptionsPaneState extends SceneObjectState {
isVizPickerOpen?: boolean; isVizPickerOpen?: boolean;
@@ -21,15 +21,16 @@ export interface PanelOptionsPaneState extends SceneObjectState {
} }
export class PanelOptionsPane extends SceneObjectBase<PanelOptionsPaneState> { export class PanelOptionsPane extends SceneObjectBase<PanelOptionsPaneState> {
public panelManager: VizPanelManager; public constructor(state: Partial<PanelOptionsPaneState>) {
public constructor(panelMgr: VizPanelManager) {
super({ super({
searchQuery: '', searchQuery: '',
listMode: OptionFilter.All, listMode: OptionFilter.All,
...state,
}); });
}
this.panelManager = panelMgr; public getVizManager() {
return sceneGraph.getAncestor(this, PanelEditor).state.vizManager;
} }
onToggleVizPicker = () => { onToggleVizPicker = () => {
@@ -44,11 +45,14 @@ export class PanelOptionsPane extends SceneObjectBase<PanelOptionsPaneState> {
this.setState({ listMode }); this.setState({ listMode });
}; };
onCollapsePane = () => {}; onCollapsePane = () => {
const editor = sceneGraph.getAncestor(this, PanelEditor);
editor.toggleOptionsPane();
};
static Component = ({ model }: SceneComponentProps<PanelOptionsPane>) => { static Component = ({ model }: SceneComponentProps<PanelOptionsPane>) => {
const { isVizPickerOpen, searchQuery, listMode } = model.useState(); const { isVizPickerOpen, searchQuery, listMode } = model.useState();
const { panelManager } = model; const panelManager = model.getVizManager();
const { panel } = panelManager.state; const { panel } = panelManager.state;
const dataObject = sceneGraph.getData(panel); const dataObject = sceneGraph.getData(panel);
const { data } = dataObject.useState(); const { data } = dataObject.useState();

View File

@@ -506,11 +506,9 @@ describe('VizPanelManager', () => {
describe('change transformations', () => { describe('change transformations', () => {
it('should update and reprocess transformations', () => { it('should update and reprocess transformations', () => {
const { scene, panel } = setupTest('panel-3'); const { scene, panel } = setupTest('panel-3');
scene.setState({ scene.setState({ editPanel: buildPanelEditScene(panel) });
editPanel: buildPanelEditScene(panel),
});
const vizPanelManager = scene.state.editPanel!.state.panelRef.resolve(); const vizPanelManager = scene.state.editPanel!.state.vizManager;
vizPanelManager.activate(); vizPanelManager.activate();
vizPanelManager.state.panel.state.$data?.activate(); vizPanelManager.state.panel.state.$data?.activate();
@@ -560,11 +558,9 @@ describe('VizPanelManager', () => {
describe('dashboard queries', () => { describe('dashboard queries', () => {
it('should update queries', () => { it('should update queries', () => {
const { scene, panel } = setupTest('panel-3'); const { scene, panel } = setupTest('panel-3');
scene.setState({ scene.setState({ editPanel: buildPanelEditScene(panel) });
editPanel: buildPanelEditScene(panel),
});
const vizPanelManager = scene.state.editPanel!.state.panelRef.resolve(); const vizPanelManager = scene.state.editPanel!.state.vizManager;
vizPanelManager.activate(); vizPanelManager.activate();
vizPanelManager.state.panel.state.$data?.activate(); vizPanelManager.state.panel.state.$data?.activate();
@@ -578,6 +574,7 @@ describe('VizPanelManager', () => {
panelId: panelWithTransformations.id, panelId: panelWithTransformations.id,
}, },
]); ]);
expect(vizPanelManager.panelData).toBeInstanceOf(SceneDataTransformer); expect(vizPanelManager.panelData).toBeInstanceOf(SceneDataTransformer);
expect(vizPanelManager.queryRunner.state.queries[0].panelId).toEqual(panelWithTransformations.id); expect(vizPanelManager.queryRunner.state.queries[0].panelId).toEqual(panelWithTransformations.id);

View File

@@ -71,8 +71,7 @@ describe('getSaveDashboardChange', () => {
dashboard.onEnterEditMode(); dashboard.onEnterEditMode();
dashboard.setState({ editPanel: editScene }); dashboard.setState({ editPanel: editScene });
const vizManager = editScene.state.panelRef.resolve(); editScene.state.vizManager.state.panel.setState({ title: 'changed title' });
vizManager.state.panel.setState({ title: 'changed title' });
const result = getSaveDashboardChange(dashboard, false, true); const result = getSaveDashboardChange(dashboard, false, true);
const panelSaveModel = result.changedSaveModel.panels![0]; const panelSaveModel = result.changedSaveModel.panels![0];

View File

@@ -147,7 +147,7 @@ class ResolveInspectPanelByKey extends SceneObjectBase<ResolveInspectPanelByKeyS
let panel = findVizPanelByKey(dashboard, panelId); let panel = findVizPanelByKey(dashboard, panelId);
if (dashboard.state.editPanel) { if (dashboard.state.editPanel) {
panel = dashboard.state.editPanel.state.panelRef.resolve().state.panel; panel = dashboard.state.editPanel.state.vizManager.state.panel;
} }
if (dashboard.state.viewPanelScene && dashboard.state.viewPanelScene.state.body) { if (dashboard.state.viewPanelScene && dashboard.state.viewPanelScene.state.body) {