mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
DashboardScene: Panel edit route basics (#74081)
* DashboardScene: Panel edit route basics * remove unused file * Removed some comments * Minor fix * Update * example of apply changes implementation * SceneObjectRef: Testing scene object ref * Rename to ref suffix * Update * Fix url sync in panel edit * Update * Update * simplify logic when committing change * remove import * Another fix for committing change
This commit is contained in:
parent
b9c681e1a7
commit
499b02b3c6
@ -10,6 +10,7 @@ import {
|
|||||||
SceneObject,
|
SceneObject,
|
||||||
sceneGraph,
|
sceneGraph,
|
||||||
VizPanel,
|
VizPanel,
|
||||||
|
SceneObjectRef,
|
||||||
} from '@grafana/scenes';
|
} from '@grafana/scenes';
|
||||||
import { Drawer, Tab, TabsBar } from '@grafana/ui';
|
import { Drawer, Tab, TabsBar } from '@grafana/ui';
|
||||||
import { supportsDataQuery } from 'app/features/dashboard/components/PanelEditor/utils';
|
import { supportsDataQuery } from 'app/features/dashboard/components/PanelEditor/utils';
|
||||||
@ -21,39 +22,38 @@ import { InspectTabState } from './types';
|
|||||||
|
|
||||||
interface PanelInspectDrawerState extends SceneObjectState {
|
interface PanelInspectDrawerState extends SceneObjectState {
|
||||||
tabs?: Array<SceneObject<InspectTabState>>;
|
tabs?: Array<SceneObject<InspectTabState>>;
|
||||||
|
panelRef: SceneObjectRef<VizPanel>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PanelInspectDrawer extends SceneObjectBase<PanelInspectDrawerState> {
|
export class PanelInspectDrawer extends SceneObjectBase<PanelInspectDrawerState> {
|
||||||
static Component = PanelInspectRenderer;
|
static Component = PanelInspectRenderer;
|
||||||
|
|
||||||
// Not stored in state as this is just a reference and it never changes
|
constructor(state: PanelInspectDrawerState) {
|
||||||
private _panel: VizPanel;
|
super(state);
|
||||||
|
|
||||||
constructor(panel: VizPanel) {
|
|
||||||
super({});
|
|
||||||
|
|
||||||
this._panel = panel;
|
|
||||||
this.buildTabs();
|
this.buildTabs();
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTabs() {
|
buildTabs() {
|
||||||
const plugin = this._panel.getPlugin();
|
const panel = this.state.panelRef.resolve();
|
||||||
|
const plugin = panel.getPlugin();
|
||||||
const tabs: Array<SceneObject<InspectTabState>> = [];
|
const tabs: Array<SceneObject<InspectTabState>> = [];
|
||||||
|
|
||||||
if (plugin) {
|
if (plugin) {
|
||||||
if (supportsDataQuery(plugin)) {
|
if (supportsDataQuery(plugin)) {
|
||||||
tabs.push(new InspectDataTab(this._panel));
|
tabs.push(new InspectDataTab(panel));
|
||||||
tabs.push(new InspectStatsTab(this._panel));
|
tabs.push(new InspectStatsTab(panel));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
tabs.push(new InspectJsonTab(this._panel));
|
tabs.push(new InspectJsonTab(panel));
|
||||||
|
|
||||||
this.setState({ tabs });
|
this.setState({ tabs });
|
||||||
}
|
}
|
||||||
|
|
||||||
getDrawerTitle() {
|
getDrawerTitle() {
|
||||||
return sceneGraph.interpolate(this._panel, `Inspect: ${this._panel.state.title}`);
|
const panel = this.state.panelRef.resolve();
|
||||||
|
return sceneGraph.interpolate(panel, `Inspect: ${panel.state.title}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
onClose = () => {
|
onClose = () => {
|
||||||
|
@ -12,10 +12,11 @@ export interface Props extends GrafanaRouteComponentProps<{ uid: string }> {}
|
|||||||
|
|
||||||
export function DashboardScenePage({ match }: Props) {
|
export function DashboardScenePage({ match }: Props) {
|
||||||
const stateManager = getDashboardScenePageStateManager();
|
const stateManager = getDashboardScenePageStateManager();
|
||||||
const { dashboard, isLoading } = stateManager.useState();
|
const { dashboard, isLoading, loadError } = stateManager.useState();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
stateManager.loadAndInit(match.params.uid);
|
stateManager.loadDashboard(match.params.uid);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
stateManager.clearState();
|
stateManager.clearState();
|
||||||
};
|
};
|
||||||
@ -25,7 +26,7 @@ export function DashboardScenePage({ match }: Props) {
|
|||||||
return (
|
return (
|
||||||
<Page layout={PageLayoutType.Canvas}>
|
<Page layout={PageLayoutType.Canvas}>
|
||||||
{isLoading && <PageLoader />}
|
{isLoading && <PageLoader />}
|
||||||
{!isLoading && <h2>Dashboard not found</h2>}
|
{loadError && <h2>{loadError}</h2>}
|
||||||
</Page>
|
</Page>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -12,12 +12,12 @@ describe('DashboardScenePageStateManager', () => {
|
|||||||
const loadDashboardMock = setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} });
|
const loadDashboardMock = setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} });
|
||||||
|
|
||||||
const loader = new DashboardScenePageStateManager({});
|
const loader = new DashboardScenePageStateManager({});
|
||||||
await loader.loadAndInit('fake-dash');
|
await loader.loadDashboard('fake-dash');
|
||||||
|
|
||||||
expect(loadDashboardMock).toHaveBeenCalledWith('db', '', 'fake-dash');
|
expect(loadDashboardMock).toHaveBeenCalledWith('db', '', 'fake-dash');
|
||||||
|
|
||||||
// should use cache second time
|
// should use cache second time
|
||||||
await loader.loadAndInit('fake-dash');
|
await loader.loadDashboard('fake-dash');
|
||||||
expect(loadDashboardMock.mock.calls.length).toBe(1);
|
expect(loadDashboardMock.mock.calls.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ describe('DashboardScenePageStateManager', () => {
|
|||||||
setupLoadDashboardMock({ dashboard: undefined, meta: {} });
|
setupLoadDashboardMock({ dashboard: undefined, meta: {} });
|
||||||
|
|
||||||
const loader = new DashboardScenePageStateManager({});
|
const loader = new DashboardScenePageStateManager({});
|
||||||
await loader.loadAndInit('fake-dash');
|
await loader.loadDashboard('fake-dash');
|
||||||
|
|
||||||
expect(loader.state.dashboard).toBeUndefined();
|
expect(loader.state.dashboard).toBeUndefined();
|
||||||
expect(loader.state.isLoading).toBe(false);
|
expect(loader.state.isLoading).toBe(false);
|
||||||
@ -36,7 +36,7 @@ describe('DashboardScenePageStateManager', () => {
|
|||||||
setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} });
|
setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} });
|
||||||
|
|
||||||
const loader = new DashboardScenePageStateManager({});
|
const loader = new DashboardScenePageStateManager({});
|
||||||
await loader.loadAndInit('fake-dash');
|
await loader.loadDashboard('fake-dash');
|
||||||
|
|
||||||
expect(loader.state.dashboard?.state.uid).toBe('fake-dash');
|
expect(loader.state.dashboard?.state.uid).toBe('fake-dash');
|
||||||
expect(loader.state.loadError).toBe(undefined);
|
expect(loader.state.loadError).toBe(undefined);
|
||||||
@ -47,7 +47,7 @@ describe('DashboardScenePageStateManager', () => {
|
|||||||
setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} });
|
setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} });
|
||||||
|
|
||||||
const loader = new DashboardScenePageStateManager({});
|
const loader = new DashboardScenePageStateManager({});
|
||||||
await loader.loadAndInit('fake-dash');
|
await loader.loadDashboard('fake-dash');
|
||||||
|
|
||||||
expect(loader.state.dashboard).toBeInstanceOf(DashboardScene);
|
expect(loader.state.dashboard).toBeInstanceOf(DashboardScene);
|
||||||
expect(loader.state.isLoading).toBe(false);
|
expect(loader.state.isLoading).toBe(false);
|
||||||
@ -59,7 +59,7 @@ describe('DashboardScenePageStateManager', () => {
|
|||||||
locationService.partial({ from: 'now-5m', to: 'now' });
|
locationService.partial({ from: 'now-5m', to: 'now' });
|
||||||
|
|
||||||
const loader = new DashboardScenePageStateManager({});
|
const loader = new DashboardScenePageStateManager({});
|
||||||
await loader.loadAndInit('fake-dash');
|
await loader.loadDashboard('fake-dash');
|
||||||
const dash = loader.state.dashboard;
|
const dash = loader.state.dashboard;
|
||||||
|
|
||||||
expect(dash!.state.$timeRange?.state.from).toEqual('now-5m');
|
expect(dash!.state.$timeRange?.state.from).toEqual('now-5m');
|
||||||
@ -69,7 +69,7 @@ describe('DashboardScenePageStateManager', () => {
|
|||||||
// try loading again (and hitting cache)
|
// try loading again (and hitting cache)
|
||||||
locationService.partial({ from: 'now-10m', to: 'now' });
|
locationService.partial({ from: 'now-10m', to: 'now' });
|
||||||
|
|
||||||
await loader.loadAndInit('fake-dash');
|
await loader.loadDashboard('fake-dash');
|
||||||
const dash2 = loader.state.dashboard;
|
const dash2 = loader.state.dashboard;
|
||||||
|
|
||||||
expect(dash2!.state.$timeRange?.state.from).toEqual('now-10m');
|
expect(dash2!.state.$timeRange?.state.from).toEqual('now-10m');
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import { StateManagerBase } from 'app/core/services/StateManagerBase';
|
import { StateManagerBase } from 'app/core/services/StateManagerBase';
|
||||||
import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
|
import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
|
||||||
|
|
||||||
|
import { buildPanelEditScene, PanelEditor } from '../panel-edit/PanelEditor';
|
||||||
import { DashboardScene } from '../scene/DashboardScene';
|
import { DashboardScene } from '../scene/DashboardScene';
|
||||||
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
||||||
|
import { getVizPanelKeyForPanelId, findVizPanelByKey } from '../utils/utils';
|
||||||
|
|
||||||
export interface DashboardScenePageState {
|
export interface DashboardScenePageState {
|
||||||
dashboard?: DashboardScene;
|
dashboard?: DashboardScene;
|
||||||
|
panelEditor?: PanelEditor;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
loadError?: string;
|
loadError?: string;
|
||||||
}
|
}
|
||||||
@ -13,13 +16,31 @@ export interface DashboardScenePageState {
|
|||||||
export class DashboardScenePageStateManager extends StateManagerBase<DashboardScenePageState> {
|
export class DashboardScenePageStateManager extends StateManagerBase<DashboardScenePageState> {
|
||||||
private cache: Record<string, DashboardScene> = {};
|
private cache: Record<string, DashboardScene> = {};
|
||||||
|
|
||||||
async loadAndInit(uid: string) {
|
public async loadDashboard(uid: string) {
|
||||||
try {
|
try {
|
||||||
const scene = await this.loadScene(uid);
|
const dashboard = await this.loadScene(uid);
|
||||||
scene.startUrlSync();
|
dashboard.startUrlSync();
|
||||||
|
|
||||||
this.cache[uid] = scene;
|
this.setState({ dashboard: dashboard, isLoading: false });
|
||||||
this.setState({ dashboard: scene, isLoading: false });
|
} catch (err) {
|
||||||
|
this.setState({ isLoading: false, loadError: String(err) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async loadPanelEdit(uid: string, panelId: string) {
|
||||||
|
try {
|
||||||
|
const dashboard = await this.loadScene(uid);
|
||||||
|
const panel = findVizPanelByKey(dashboard, getVizPanelKeyForPanelId(parseInt(panelId, 10)));
|
||||||
|
|
||||||
|
if (!panel) {
|
||||||
|
this.setState({ isLoading: false, loadError: 'Panel not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const panelEditor = buildPanelEditScene(dashboard, panel);
|
||||||
|
panelEditor.startUrlSync();
|
||||||
|
|
||||||
|
this.setState({ isLoading: false, panelEditor });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
this.setState({ isLoading: false, loadError: String(err) });
|
this.setState({ isLoading: false, loadError: String(err) });
|
||||||
}
|
}
|
||||||
@ -36,14 +57,16 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
|
|||||||
const rsp = await dashboardLoaderSrv.loadDashboard('db', '', uid);
|
const rsp = await dashboardLoaderSrv.loadDashboard('db', '', uid);
|
||||||
|
|
||||||
if (rsp.dashboard) {
|
if (rsp.dashboard) {
|
||||||
return transformSaveModelToScene(rsp);
|
const scene = transformSaveModelToScene(rsp);
|
||||||
|
this.cache[uid] = scene;
|
||||||
|
return scene;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error('Dashboard not found');
|
throw new Error('Dashboard not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
public clearState() {
|
public clearState() {
|
||||||
this.setState({ dashboard: undefined, loadError: undefined, isLoading: false });
|
this.setState({ dashboard: undefined, loadError: undefined, isLoading: false, panelEditor: undefined });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
36
public/app/features/dashboard-scene/pages/PanelEditPage.tsx
Normal file
36
public/app/features/dashboard-scene/pages/PanelEditPage.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
// Libraries
|
||||||
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { PageLayoutType } from '@grafana/data';
|
||||||
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
|
import PageLoader from 'app/core/components/PageLoader/PageLoader';
|
||||||
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||||
|
|
||||||
|
import { getDashboardScenePageStateManager } from './DashboardScenePageStateManager';
|
||||||
|
|
||||||
|
export interface Props extends GrafanaRouteComponentProps<{ uid: string; panelId: string }> {}
|
||||||
|
|
||||||
|
export function PanelEditPage({ match }: Props) {
|
||||||
|
const stateManager = getDashboardScenePageStateManager();
|
||||||
|
const { panelEditor, isLoading, loadError } = stateManager.useState();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
stateManager.loadPanelEdit(match.params.uid, match.params.panelId);
|
||||||
|
return () => {
|
||||||
|
stateManager.clearState();
|
||||||
|
};
|
||||||
|
}, [stateManager, match.params.uid, match.params.panelId]);
|
||||||
|
|
||||||
|
if (!panelEditor) {
|
||||||
|
return (
|
||||||
|
<Page layout={PageLayoutType.Canvas}>
|
||||||
|
{isLoading && <PageLoader />}
|
||||||
|
{loadError && <h2>{loadError}</h2>}
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <panelEditor.Component model={panelEditor} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PanelEditPage;
|
134
public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx
Normal file
134
public/app/features/dashboard-scene/panel-edit/PanelEditor.tsx
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import * as H from 'history';
|
||||||
|
|
||||||
|
import { locationService } from '@grafana/runtime';
|
||||||
|
import {
|
||||||
|
getUrlSyncManager,
|
||||||
|
SceneFlexItem,
|
||||||
|
SceneFlexLayout,
|
||||||
|
SceneObject,
|
||||||
|
SceneObjectBase,
|
||||||
|
SceneObjectRef,
|
||||||
|
SceneObjectState,
|
||||||
|
sceneUtils,
|
||||||
|
SplitLayout,
|
||||||
|
VizPanel,
|
||||||
|
} from '@grafana/scenes';
|
||||||
|
|
||||||
|
import { DashboardScene } from '../scene/DashboardScene';
|
||||||
|
import { getDashboardUrl } from '../utils/utils';
|
||||||
|
|
||||||
|
import { PanelEditorRenderer } from './PanelEditorRenderer';
|
||||||
|
import { PanelOptionsPane } from './PanelOptionsPane';
|
||||||
|
|
||||||
|
export interface PanelEditorState extends SceneObjectState {
|
||||||
|
body: SceneObject;
|
||||||
|
controls?: SceneObject[];
|
||||||
|
isDirty?: boolean;
|
||||||
|
/** Panel to inspect */
|
||||||
|
inspectPanelId?: string;
|
||||||
|
/** Scene object that handles the current drawer */
|
||||||
|
drawer?: SceneObject;
|
||||||
|
|
||||||
|
dashboardRef: SceneObjectRef<DashboardScene>;
|
||||||
|
sourcePanelRef: SceneObjectRef<VizPanel>;
|
||||||
|
panelRef: SceneObjectRef<VizPanel>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
||||||
|
static Component = PanelEditorRenderer;
|
||||||
|
|
||||||
|
public constructor(state: PanelEditorState) {
|
||||||
|
super(state);
|
||||||
|
|
||||||
|
this.addActivationHandler(() => this._activationHandler());
|
||||||
|
}
|
||||||
|
|
||||||
|
private _activationHandler() {
|
||||||
|
// Deactivation logic
|
||||||
|
return () => {
|
||||||
|
getUrlSyncManager().cleanUp(this);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public startUrlSync() {
|
||||||
|
getUrlSyncManager().initSync(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getPageNav(location: H.Location) {
|
||||||
|
return {
|
||||||
|
text: 'Edit panel',
|
||||||
|
parentItem: this.state.dashboardRef.resolve().getPageNav(location),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public onDiscard = () => {
|
||||||
|
// Open question on what to preserve when going back
|
||||||
|
// Preserve time range, and variables state (that might have been changed while in panel edit)
|
||||||
|
// Preserve current panel data? (say if you just changed the time range and have new data)
|
||||||
|
this._navigateBackToDashboard();
|
||||||
|
};
|
||||||
|
|
||||||
|
public onApply = () => {
|
||||||
|
this._commitChanges();
|
||||||
|
this._navigateBackToDashboard();
|
||||||
|
};
|
||||||
|
|
||||||
|
public onSave = () => {
|
||||||
|
this._commitChanges();
|
||||||
|
// Open dashboard save drawer
|
||||||
|
};
|
||||||
|
|
||||||
|
private _commitChanges() {
|
||||||
|
const dashboard = this.state.dashboardRef.resolve();
|
||||||
|
const sourcePanel = this.state.sourcePanelRef.resolve();
|
||||||
|
const panel = this.state.panelRef.resolve();
|
||||||
|
|
||||||
|
if (!dashboard.state.isEditing) {
|
||||||
|
dashboard.onEnterEditMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
const newState = sceneUtils.cloneSceneObjectState(panel.state);
|
||||||
|
sourcePanel.setState(newState);
|
||||||
|
|
||||||
|
// preserve time range and variables state
|
||||||
|
dashboard.setState({
|
||||||
|
$timeRange: this.state.$timeRange?.clone(),
|
||||||
|
$variables: this.state.$variables?.clone(),
|
||||||
|
isDirty: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _navigateBackToDashboard() {
|
||||||
|
locationService.push(
|
||||||
|
getDashboardUrl({
|
||||||
|
uid: this.state.dashboardRef.resolve().state.uid,
|
||||||
|
currentQueryParams: locationService.getLocation().search,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPanelEditScene(dashboard: DashboardScene, panel: VizPanel): PanelEditor {
|
||||||
|
const panelClone = panel.clone();
|
||||||
|
const dashboardStateCloned = sceneUtils.cloneSceneObjectState(dashboard.state);
|
||||||
|
|
||||||
|
return new PanelEditor({
|
||||||
|
dashboardRef: new SceneObjectRef(dashboard),
|
||||||
|
sourcePanelRef: new SceneObjectRef(panel),
|
||||||
|
panelRef: new SceneObjectRef(panelClone),
|
||||||
|
controls: dashboardStateCloned.controls,
|
||||||
|
$variables: dashboardStateCloned.$variables,
|
||||||
|
$timeRange: dashboardStateCloned.$timeRange,
|
||||||
|
body: new SplitLayout({
|
||||||
|
direction: 'row',
|
||||||
|
primary: new SceneFlexLayout({
|
||||||
|
direction: 'column',
|
||||||
|
children: [panelClone],
|
||||||
|
}),
|
||||||
|
secondary: new SceneFlexItem({
|
||||||
|
width: '300px',
|
||||||
|
body: new PanelOptionsPane(panelClone),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,92 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
||||||
|
import { SceneComponentProps } from '@grafana/scenes';
|
||||||
|
import { Button, useStyles2 } from '@grafana/ui';
|
||||||
|
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
|
||||||
|
import { NavToolbarSeparator } from 'app/core/components/AppChrome/NavToolbar/NavToolbarSeparator';
|
||||||
|
import { Page } from 'app/core/components/Page/Page';
|
||||||
|
|
||||||
|
import { PanelEditor } from './PanelEditor';
|
||||||
|
|
||||||
|
export function PanelEditorRenderer({ model }: SceneComponentProps<PanelEditor>) {
|
||||||
|
const { body, controls, drawer } = model.useState();
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const location = useLocation();
|
||||||
|
const pageNav = model.getPageNav(location);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Page navId="scenes" pageNav={pageNav} layout={PageLayoutType.Custom}>
|
||||||
|
<AppChromeUpdate actions={getToolbarActions(model)} />
|
||||||
|
<div className={styles.canvasContent}>
|
||||||
|
{controls && (
|
||||||
|
<div className={styles.controls}>
|
||||||
|
{controls.map((control) => (
|
||||||
|
<control.Component key={control.state.key} model={control} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.body}>
|
||||||
|
<body.Component model={body} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{drawer && <drawer.Component model={drawer} />}
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getToolbarActions(editor: PanelEditor) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<NavToolbarSeparator leftActionsSeparator key="separator" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
onClick={editor.onDiscard}
|
||||||
|
tooltip=""
|
||||||
|
key="panel-edit-discard"
|
||||||
|
variant="destructive"
|
||||||
|
fill="outline"
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
|
Discard
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button onClick={editor.onApply} tooltip="" key="panel-edit-apply" variant="primary" size="sm">
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStyles(theme: GrafanaTheme2) {
|
||||||
|
return {
|
||||||
|
canvasContent: css({
|
||||||
|
label: 'canvas-content',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
padding: theme.spacing(0, 2),
|
||||||
|
flexBasis: '100%',
|
||||||
|
flexGrow: 1,
|
||||||
|
minHeight: 0,
|
||||||
|
width: '100%',
|
||||||
|
}),
|
||||||
|
body: css({
|
||||||
|
label: 'body',
|
||||||
|
flexGrow: 1,
|
||||||
|
display: 'flex',
|
||||||
|
position: 'relative',
|
||||||
|
minHeight: 0,
|
||||||
|
gap: '8px',
|
||||||
|
marginBottom: theme.spacing(2),
|
||||||
|
}),
|
||||||
|
controls: css({
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: theme.spacing(1),
|
||||||
|
padding: theme.spacing(2, 0),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { SceneComponentProps, SceneObjectBase, SceneObjectState, VizPanel } from '@grafana/scenes';
|
||||||
|
import { Field, Input, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
|
export interface PanelOptionsPaneState extends SceneObjectState {}
|
||||||
|
|
||||||
|
export class PanelOptionsPane extends SceneObjectBase<PanelOptionsPaneState> {
|
||||||
|
public panel: VizPanel;
|
||||||
|
|
||||||
|
public constructor(panel: VizPanel) {
|
||||||
|
super({});
|
||||||
|
|
||||||
|
this.panel = panel;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Component = ({ model }: SceneComponentProps<PanelOptionsPane>) => {
|
||||||
|
const { panel } = model;
|
||||||
|
const { title } = panel.useState();
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.box}>
|
||||||
|
<Field label="Title">
|
||||||
|
<Input value={title} onChange={(evt) => panel.setState({ title: evt.currentTarget.value })} />
|
||||||
|
</Field>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStyles(theme: GrafanaTheme2) {
|
||||||
|
return {
|
||||||
|
box: css({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
flexBasis: '100%',
|
||||||
|
flexGrow: 1,
|
||||||
|
minHeight: 0,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
@ -55,6 +55,7 @@ describe('DashboardScene', () => {
|
|||||||
function buildTestScene() {
|
function buildTestScene() {
|
||||||
const scene = new DashboardScene({
|
const scene = new DashboardScene({
|
||||||
title: 'hello',
|
title: 'hello',
|
||||||
|
uid: 'dash-1',
|
||||||
body: new SceneGridLayout({
|
body: new SceneGridLayout({
|
||||||
children: [
|
children: [
|
||||||
new SceneGridItem({
|
new SceneGridItem({
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import * as H from 'history';
|
import * as H from 'history';
|
||||||
import { Unsubscribable } from 'rxjs';
|
import { Unsubscribable } from 'rxjs';
|
||||||
|
|
||||||
import { locationUtil, NavModelItem, UrlQueryMap } from '@grafana/data';
|
import { NavModelItem, UrlQueryMap } from '@grafana/data';
|
||||||
import { locationService } from '@grafana/runtime';
|
import { locationService } from '@grafana/runtime';
|
||||||
import {
|
import {
|
||||||
getUrlSyncManager,
|
getUrlSyncManager,
|
||||||
@ -9,6 +9,7 @@ import {
|
|||||||
SceneGridLayout,
|
SceneGridLayout,
|
||||||
SceneObject,
|
SceneObject,
|
||||||
SceneObjectBase,
|
SceneObjectBase,
|
||||||
|
SceneObjectRef,
|
||||||
SceneObjectState,
|
SceneObjectState,
|
||||||
SceneObjectStateChangedEvent,
|
SceneObjectStateChangedEvent,
|
||||||
sceneUtils,
|
sceneUtils,
|
||||||
@ -16,7 +17,7 @@ import {
|
|||||||
|
|
||||||
import { DashboardSceneRenderer } from '../scene/DashboardSceneRenderer';
|
import { DashboardSceneRenderer } from '../scene/DashboardSceneRenderer';
|
||||||
import { SaveDashboardDrawer } from '../serialization/SaveDashboardDrawer';
|
import { SaveDashboardDrawer } from '../serialization/SaveDashboardDrawer';
|
||||||
import { findVizPanelByKey, forceRenderChildren } from '../utils/utils';
|
import { findVizPanelByKey, forceRenderChildren, getDashboardUrl } from '../utils/utils';
|
||||||
|
|
||||||
import { DashboardSceneUrlSync } from './DashboardSceneUrlSync';
|
import { DashboardSceneUrlSync } from './DashboardSceneUrlSync';
|
||||||
|
|
||||||
@ -59,10 +60,10 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
|||||||
public constructor(state: DashboardSceneState) {
|
public constructor(state: DashboardSceneState) {
|
||||||
super(state);
|
super(state);
|
||||||
|
|
||||||
this.addActivationHandler(() => this.onActivate());
|
this.addActivationHandler(() => this._activationHandler());
|
||||||
}
|
}
|
||||||
|
|
||||||
private onActivate() {
|
private _activationHandler() {
|
||||||
if (this.state.isEditing) {
|
if (this.state.isEditing) {
|
||||||
this.startTrackingChanges();
|
this.startTrackingChanges();
|
||||||
}
|
}
|
||||||
@ -119,13 +120,17 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
public onSave = () => {
|
public onSave = () => {
|
||||||
this.setState({ drawer: new SaveDashboardDrawer(this) });
|
this.setState({ drawer: new SaveDashboardDrawer({ dashboardRef: new SceneObjectRef(this) }) });
|
||||||
};
|
};
|
||||||
|
|
||||||
public getPageNav(location: H.Location) {
|
public getPageNav(location: H.Location) {
|
||||||
let pageNav: NavModelItem = {
|
let pageNav: NavModelItem = {
|
||||||
text: this.state.title,
|
text: this.state.title,
|
||||||
url: locationUtil.getUrlForPartial(location, { viewPanel: null, inspect: null }),
|
url: getDashboardUrl({
|
||||||
|
uid: this.state.uid,
|
||||||
|
currentQueryParams: location.search,
|
||||||
|
updateQuery: { viewPanel: null, inspect: null },
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.state.viewPanelKey) {
|
if (this.state.viewPanelKey) {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { AppEvents } from '@grafana/data';
|
import { AppEvents } from '@grafana/data';
|
||||||
import { locationService } from '@grafana/runtime';
|
import { locationService } from '@grafana/runtime';
|
||||||
import { SceneObjectUrlSyncHandler, SceneObjectUrlValues } from '@grafana/scenes';
|
import { SceneObjectRef, SceneObjectUrlSyncHandler, SceneObjectUrlValues } from '@grafana/scenes';
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
|
|
||||||
import { PanelInspectDrawer } from '../inspect/PanelInspectDrawer';
|
import { PanelInspectDrawer } from '../inspect/PanelInspectDrawer';
|
||||||
@ -34,7 +34,7 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
update.inspectPanelKey = values.inspect;
|
update.inspectPanelKey = values.inspect;
|
||||||
update.drawer = new PanelInspectDrawer(panel);
|
update.drawer = new PanelInspectDrawer({ panelRef: new SceneObjectRef(panel) });
|
||||||
} else if (inspectPanelId) {
|
} else if (inspectPanelId) {
|
||||||
update.inspectPanelKey = undefined;
|
update.inspectPanelKey = undefined;
|
||||||
update.drawer = undefined;
|
update.drawer = undefined;
|
||||||
|
@ -3,6 +3,10 @@ import { locationService } from '@grafana/runtime';
|
|||||||
import { VizPanel, VizPanelMenu } from '@grafana/scenes';
|
import { VizPanel, VizPanelMenu } from '@grafana/scenes';
|
||||||
import { t } from 'app/core/internationalization';
|
import { t } from 'app/core/internationalization';
|
||||||
|
|
||||||
|
import { getDashboardUrl, getPanelIdForVizPanel } from '../utils/utils';
|
||||||
|
|
||||||
|
import { DashboardScene } from './DashboardScene';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Behavior is called when VizPanelMenu is activated (ie when it's opened).
|
* Behavior is called when VizPanelMenu is activated (ie when it's opened).
|
||||||
*/
|
*/
|
||||||
@ -10,26 +14,45 @@ export function panelMenuBehavior(menu: VizPanelMenu) {
|
|||||||
// hm.. add another generic param to SceneObject to specify parent type?
|
// hm.. add another generic param to SceneObject to specify parent type?
|
||||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||||
const panel = menu.parent as VizPanel;
|
const panel = menu.parent as VizPanel;
|
||||||
|
|
||||||
const location = locationService.getLocation();
|
const location = locationService.getLocation();
|
||||||
const items: PanelMenuItem[] = [];
|
const items: PanelMenuItem[] = [];
|
||||||
|
const panelId = getPanelIdForVizPanel(panel);
|
||||||
|
const dashboard = panel.getRoot();
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
// Add tracking via reportInteraction (but preserve the fact that these are normal links)
|
// Add tracking via reportInteraction (but preserve the fact that these are normal links)
|
||||||
|
|
||||||
items.push({
|
if (dashboard instanceof DashboardScene) {
|
||||||
text: t('panel.header-menu.view', `View`),
|
items.push({
|
||||||
iconClassName: 'eye',
|
text: t('panel.header-menu.view', `View`),
|
||||||
shortcut: 'v',
|
iconClassName: 'eye',
|
||||||
// Hm... need the numeric id to be url compatible?
|
shortcut: 'v',
|
||||||
href: locationUtil.getUrlForPartial(location, { viewPanel: panel.state.key }),
|
href: getDashboardUrl({
|
||||||
});
|
uid: dashboard.state.uid,
|
||||||
|
currentQueryParams: location.search,
|
||||||
|
updateQuery: { filter: null, new: 'A' },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
// We could check isEditing here but I kind of think this should always be in the menu,
|
||||||
|
// and going into panel edit should make the dashboard go into edit mode is it's not already
|
||||||
|
items.push({
|
||||||
|
text: t('panel.header-menu.edit', `Edit`),
|
||||||
|
iconClassName: 'eye',
|
||||||
|
shortcut: 'v',
|
||||||
|
href: getDashboardUrl({
|
||||||
|
uid: dashboard.state.uid,
|
||||||
|
subPath: `/panel-edit/${panelId}`,
|
||||||
|
currentQueryParams: location.search,
|
||||||
|
updateQuery: { filter: null, new: 'A' },
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
items.push({
|
items.push({
|
||||||
text: t('panel.header-menu.inspect', `Inspect`),
|
text: t('panel.header-menu.inspect', `Inspect`),
|
||||||
iconClassName: 'info-circle',
|
iconClassName: 'info-circle',
|
||||||
shortcut: 'i',
|
shortcut: 'i',
|
||||||
// Hm... need the numeric id to be url compatible?
|
|
||||||
href: locationUtil.getUrlForPartial(location, { inspect: panel.state.key }),
|
href: locationUtil.getUrlForPartial(location, { inspect: panel.state.key }),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
|
import { SceneComponentProps, SceneObjectBase, SceneObjectState, SceneObjectRef } from '@grafana/scenes';
|
||||||
import { Drawer } from '@grafana/ui';
|
import { Drawer } from '@grafana/ui';
|
||||||
import { SaveDashboardDiff } from 'app/features/dashboard/components/SaveDashboard/SaveDashboardDiff';
|
import { SaveDashboardDiff } from 'app/features/dashboard/components/SaveDashboard/SaveDashboardDiff';
|
||||||
import { jsonDiff } from 'app/features/dashboard/components/VersionHistory/utils';
|
import { jsonDiff } from 'app/features/dashboard/components/VersionHistory/utils';
|
||||||
@ -9,21 +9,21 @@ import { DashboardScene } from '../scene/DashboardScene';
|
|||||||
|
|
||||||
import { transformSceneToSaveModel } from './transformSceneToSaveModel';
|
import { transformSceneToSaveModel } from './transformSceneToSaveModel';
|
||||||
|
|
||||||
interface SaveDashboardDrawerState extends SceneObjectState {}
|
interface SaveDashboardDrawerState extends SceneObjectState {
|
||||||
|
dashboardRef: SceneObjectRef<DashboardScene>;
|
||||||
|
}
|
||||||
|
|
||||||
export class SaveDashboardDrawer extends SceneObjectBase<SaveDashboardDrawerState> {
|
export class SaveDashboardDrawer extends SceneObjectBase<SaveDashboardDrawerState> {
|
||||||
constructor(public dashboard: DashboardScene) {
|
|
||||||
super({});
|
|
||||||
}
|
|
||||||
|
|
||||||
onClose = () => {
|
onClose = () => {
|
||||||
this.dashboard.setState({ drawer: undefined });
|
this.state.dashboardRef.resolve().setState({ drawer: undefined });
|
||||||
};
|
};
|
||||||
|
|
||||||
static Component = ({ model }: SceneComponentProps<SaveDashboardDrawer>) => {
|
static Component = ({ model }: SceneComponentProps<SaveDashboardDrawer>) => {
|
||||||
const initialScene = new DashboardScene(model.dashboard.getInitialState()!);
|
const dashboard = model.state.dashboardRef.resolve();
|
||||||
|
const initialState = dashboard.getInitialState();
|
||||||
|
const initialScene = new DashboardScene(initialState!);
|
||||||
const initialSaveModel = transformSceneToSaveModel(initialScene);
|
const initialSaveModel = transformSceneToSaveModel(initialScene);
|
||||||
const changedSaveModel = transformSceneToSaveModel(model.dashboard);
|
const changedSaveModel = transformSceneToSaveModel(dashboard);
|
||||||
|
|
||||||
const diff = jsonDiff(initialSaveModel, changedSaveModel);
|
const diff = jsonDiff(initialSaveModel, changedSaveModel);
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ export class SaveDashboardDrawer extends SceneObjectBase<SaveDashboardDrawerStat
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer title="Save dashboard" subtitle={model.dashboard.state.title} scrollableContent onClose={model.onClose}>
|
<Drawer title="Save dashboard" subtitle={dashboard.state.title} scrollableContent onClose={model.onClose}>
|
||||||
<SaveDashboardDiff diff={diff} oldValue={initialSaveModel} newValue={changedSaveModel} />
|
<SaveDashboardDiff diff={diff} oldValue={initialSaveModel} newValue={changedSaveModel} />
|
||||||
</Drawer>
|
</Drawer>
|
||||||
);
|
);
|
||||||
|
25
public/app/features/dashboard-scene/utils/utils.test.ts
Normal file
25
public/app/features/dashboard-scene/utils/utils.test.ts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { getDashboardUrl } from './utils';
|
||||||
|
|
||||||
|
describe('dashboard utils', () => {
|
||||||
|
it('Can getUrl', () => {
|
||||||
|
const url = getDashboardUrl({ uid: 'dash-1', currentQueryParams: '?orgId=1&filter=A' });
|
||||||
|
|
||||||
|
expect(url).toBe('/scenes/dashboard/dash-1?orgId=1&filter=A');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Can getUrl with subpath', () => {
|
||||||
|
const url = getDashboardUrl({ uid: 'dash-1', subPath: '/panel-edit/2', currentQueryParams: '?orgId=1&filter=A' });
|
||||||
|
|
||||||
|
expect(url).toBe('/scenes/dashboard/dash-1/panel-edit/2?orgId=1&filter=A');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Can getUrl with params removed and addded', () => {
|
||||||
|
const url = getDashboardUrl({
|
||||||
|
uid: 'dash-1',
|
||||||
|
currentQueryParams: '?orgId=1&filter=A',
|
||||||
|
updateQuery: { filter: null, new: 'A' },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(url).toBe('/scenes/dashboard/dash-1?orgId=1&new=A');
|
||||||
|
});
|
||||||
|
});
|
@ -1,3 +1,5 @@
|
|||||||
|
import { UrlQueryMap, urlUtil } from '@grafana/data';
|
||||||
|
import { locationSearchToObject } from '@grafana/runtime';
|
||||||
import { MultiValueVariable, sceneGraph, SceneObject, VizPanel } from '@grafana/scenes';
|
import { MultiValueVariable, sceneGraph, SceneObject, VizPanel } from '@grafana/scenes';
|
||||||
|
|
||||||
export function getVizPanelKeyForPanelId(panelId: number) {
|
export function getVizPanelKeyForPanelId(panelId: number) {
|
||||||
@ -67,6 +69,35 @@ export function forceRenderChildren(model: SceneObject, recursive?: boolean) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DashboardUrlOptions {
|
||||||
|
uid?: string;
|
||||||
|
subPath?: string;
|
||||||
|
updateQuery?: UrlQueryMap;
|
||||||
|
/**
|
||||||
|
* Set to location.search to preserve current params
|
||||||
|
*/
|
||||||
|
currentQueryParams: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDashboardUrl(options: DashboardUrlOptions) {
|
||||||
|
const url = `/scenes/dashboard/${options.uid}${options.subPath ?? ''}`;
|
||||||
|
|
||||||
|
const params = options.currentQueryParams ? locationSearchToObject(options.currentQueryParams) : {};
|
||||||
|
|
||||||
|
if (options.updateQuery) {
|
||||||
|
for (const key of Object.keys(options.updateQuery)) {
|
||||||
|
// removing params with null | undefined
|
||||||
|
if (options.updateQuery[key] === null || options.updateQuery[key] === undefined) {
|
||||||
|
delete params[key];
|
||||||
|
} else {
|
||||||
|
params[key] = options.updateQuery[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return urlUtil.renderUrl(url, params);
|
||||||
|
}
|
||||||
|
|
||||||
export function getMultiVariableValues(variable: MultiValueVariable) {
|
export function getMultiVariableValues(variable: MultiValueVariable) {
|
||||||
const { value, text, options } = variable.state;
|
const { value, text, options } = variable.state;
|
||||||
|
|
||||||
|
@ -545,6 +545,12 @@ export function getDynamicDashboardRoutes(cfg = config): RouteDescriptor[] {
|
|||||||
() => import(/* webpackChunkName: "scenes"*/ 'app/features/dashboard-scene/pages/DashboardScenePage')
|
() => import(/* webpackChunkName: "scenes"*/ 'app/features/dashboard-scene/pages/DashboardScenePage')
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/scenes/dashboard/:uid/panel-edit/:panelId',
|
||||||
|
component: SafeDynamicImport(
|
||||||
|
() => import(/* webpackChunkName: "scenes"*/ 'app/features/dashboard-scene/pages/PanelEditPage')
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/scenes/grafana-monitoring',
|
path: '/scenes/grafana-monitoring',
|
||||||
exact: false,
|
exact: false,
|
||||||
|
Loading…
Reference in New Issue
Block a user