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,
|
||||
sceneGraph,
|
||||
VizPanel,
|
||||
SceneObjectRef,
|
||||
} from '@grafana/scenes';
|
||||
import { Drawer, Tab, TabsBar } from '@grafana/ui';
|
||||
import { supportsDataQuery } from 'app/features/dashboard/components/PanelEditor/utils';
|
||||
@ -21,39 +22,38 @@ import { InspectTabState } from './types';
|
||||
|
||||
interface PanelInspectDrawerState extends SceneObjectState {
|
||||
tabs?: Array<SceneObject<InspectTabState>>;
|
||||
panelRef: SceneObjectRef<VizPanel>;
|
||||
}
|
||||
|
||||
export class PanelInspectDrawer extends SceneObjectBase<PanelInspectDrawerState> {
|
||||
static Component = PanelInspectRenderer;
|
||||
|
||||
// Not stored in state as this is just a reference and it never changes
|
||||
private _panel: VizPanel;
|
||||
constructor(state: PanelInspectDrawerState) {
|
||||
super(state);
|
||||
|
||||
constructor(panel: VizPanel) {
|
||||
super({});
|
||||
|
||||
this._panel = panel;
|
||||
this.buildTabs();
|
||||
}
|
||||
|
||||
buildTabs() {
|
||||
const plugin = this._panel.getPlugin();
|
||||
const panel = this.state.panelRef.resolve();
|
||||
const plugin = panel.getPlugin();
|
||||
const tabs: Array<SceneObject<InspectTabState>> = [];
|
||||
|
||||
if (plugin) {
|
||||
if (supportsDataQuery(plugin)) {
|
||||
tabs.push(new InspectDataTab(this._panel));
|
||||
tabs.push(new InspectStatsTab(this._panel));
|
||||
tabs.push(new InspectDataTab(panel));
|
||||
tabs.push(new InspectStatsTab(panel));
|
||||
}
|
||||
}
|
||||
|
||||
tabs.push(new InspectJsonTab(this._panel));
|
||||
tabs.push(new InspectJsonTab(panel));
|
||||
|
||||
this.setState({ tabs });
|
||||
}
|
||||
|
||||
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 = () => {
|
||||
|
@ -12,10 +12,11 @@ export interface Props extends GrafanaRouteComponentProps<{ uid: string }> {}
|
||||
|
||||
export function DashboardScenePage({ match }: Props) {
|
||||
const stateManager = getDashboardScenePageStateManager();
|
||||
const { dashboard, isLoading } = stateManager.useState();
|
||||
const { dashboard, isLoading, loadError } = stateManager.useState();
|
||||
|
||||
useEffect(() => {
|
||||
stateManager.loadAndInit(match.params.uid);
|
||||
stateManager.loadDashboard(match.params.uid);
|
||||
|
||||
return () => {
|
||||
stateManager.clearState();
|
||||
};
|
||||
@ -25,7 +26,7 @@ export function DashboardScenePage({ match }: Props) {
|
||||
return (
|
||||
<Page layout={PageLayoutType.Canvas}>
|
||||
{isLoading && <PageLoader />}
|
||||
{!isLoading && <h2>Dashboard not found</h2>}
|
||||
{loadError && <h2>{loadError}</h2>}
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
@ -12,12 +12,12 @@ describe('DashboardScenePageStateManager', () => {
|
||||
const loadDashboardMock = setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} });
|
||||
|
||||
const loader = new DashboardScenePageStateManager({});
|
||||
await loader.loadAndInit('fake-dash');
|
||||
await loader.loadDashboard('fake-dash');
|
||||
|
||||
expect(loadDashboardMock).toHaveBeenCalledWith('db', '', 'fake-dash');
|
||||
|
||||
// should use cache second time
|
||||
await loader.loadAndInit('fake-dash');
|
||||
await loader.loadDashboard('fake-dash');
|
||||
expect(loadDashboardMock.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
@ -25,7 +25,7 @@ describe('DashboardScenePageStateManager', () => {
|
||||
setupLoadDashboardMock({ dashboard: undefined, meta: {} });
|
||||
|
||||
const loader = new DashboardScenePageStateManager({});
|
||||
await loader.loadAndInit('fake-dash');
|
||||
await loader.loadDashboard('fake-dash');
|
||||
|
||||
expect(loader.state.dashboard).toBeUndefined();
|
||||
expect(loader.state.isLoading).toBe(false);
|
||||
@ -36,7 +36,7 @@ describe('DashboardScenePageStateManager', () => {
|
||||
setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} });
|
||||
|
||||
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.loadError).toBe(undefined);
|
||||
@ -47,7 +47,7 @@ describe('DashboardScenePageStateManager', () => {
|
||||
setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} });
|
||||
|
||||
const loader = new DashboardScenePageStateManager({});
|
||||
await loader.loadAndInit('fake-dash');
|
||||
await loader.loadDashboard('fake-dash');
|
||||
|
||||
expect(loader.state.dashboard).toBeInstanceOf(DashboardScene);
|
||||
expect(loader.state.isLoading).toBe(false);
|
||||
@ -59,7 +59,7 @@ describe('DashboardScenePageStateManager', () => {
|
||||
locationService.partial({ from: 'now-5m', to: 'now' });
|
||||
|
||||
const loader = new DashboardScenePageStateManager({});
|
||||
await loader.loadAndInit('fake-dash');
|
||||
await loader.loadDashboard('fake-dash');
|
||||
const dash = loader.state.dashboard;
|
||||
|
||||
expect(dash!.state.$timeRange?.state.from).toEqual('now-5m');
|
||||
@ -69,7 +69,7 @@ describe('DashboardScenePageStateManager', () => {
|
||||
// try loading again (and hitting cache)
|
||||
locationService.partial({ from: 'now-10m', to: 'now' });
|
||||
|
||||
await loader.loadAndInit('fake-dash');
|
||||
await loader.loadDashboard('fake-dash');
|
||||
const dash2 = loader.state.dashboard;
|
||||
|
||||
expect(dash2!.state.$timeRange?.state.from).toEqual('now-10m');
|
||||
|
@ -1,11 +1,14 @@
|
||||
import { StateManagerBase } from 'app/core/services/StateManagerBase';
|
||||
import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
|
||||
|
||||
import { buildPanelEditScene, PanelEditor } from '../panel-edit/PanelEditor';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
||||
import { getVizPanelKeyForPanelId, findVizPanelByKey } from '../utils/utils';
|
||||
|
||||
export interface DashboardScenePageState {
|
||||
dashboard?: DashboardScene;
|
||||
panelEditor?: PanelEditor;
|
||||
isLoading?: boolean;
|
||||
loadError?: string;
|
||||
}
|
||||
@ -13,13 +16,31 @@ export interface DashboardScenePageState {
|
||||
export class DashboardScenePageStateManager extends StateManagerBase<DashboardScenePageState> {
|
||||
private cache: Record<string, DashboardScene> = {};
|
||||
|
||||
async loadAndInit(uid: string) {
|
||||
public async loadDashboard(uid: string) {
|
||||
try {
|
||||
const scene = await this.loadScene(uid);
|
||||
scene.startUrlSync();
|
||||
const dashboard = await this.loadScene(uid);
|
||||
dashboard.startUrlSync();
|
||||
|
||||
this.cache[uid] = scene;
|
||||
this.setState({ dashboard: scene, isLoading: false });
|
||||
this.setState({ dashboard: dashboard, 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) {
|
||||
this.setState({ isLoading: false, loadError: String(err) });
|
||||
}
|
||||
@ -36,14 +57,16 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
|
||||
const rsp = await dashboardLoaderSrv.loadDashboard('db', '', uid);
|
||||
|
||||
if (rsp.dashboard) {
|
||||
return transformSaveModelToScene(rsp);
|
||||
const scene = transformSaveModelToScene(rsp);
|
||||
this.cache[uid] = scene;
|
||||
return scene;
|
||||
}
|
||||
|
||||
throw new Error('Dashboard not found');
|
||||
}
|
||||
|
||||
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() {
|
||||
const scene = new DashboardScene({
|
||||
title: 'hello',
|
||||
uid: 'dash-1',
|
||||
body: new SceneGridLayout({
|
||||
children: [
|
||||
new SceneGridItem({
|
||||
|
@ -1,7 +1,7 @@
|
||||
import * as H from 'history';
|
||||
import { Unsubscribable } from 'rxjs';
|
||||
|
||||
import { locationUtil, NavModelItem, UrlQueryMap } from '@grafana/data';
|
||||
import { NavModelItem, UrlQueryMap } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import {
|
||||
getUrlSyncManager,
|
||||
@ -9,6 +9,7 @@ import {
|
||||
SceneGridLayout,
|
||||
SceneObject,
|
||||
SceneObjectBase,
|
||||
SceneObjectRef,
|
||||
SceneObjectState,
|
||||
SceneObjectStateChangedEvent,
|
||||
sceneUtils,
|
||||
@ -16,7 +17,7 @@ import {
|
||||
|
||||
import { DashboardSceneRenderer } from '../scene/DashboardSceneRenderer';
|
||||
import { SaveDashboardDrawer } from '../serialization/SaveDashboardDrawer';
|
||||
import { findVizPanelByKey, forceRenderChildren } from '../utils/utils';
|
||||
import { findVizPanelByKey, forceRenderChildren, getDashboardUrl } from '../utils/utils';
|
||||
|
||||
import { DashboardSceneUrlSync } from './DashboardSceneUrlSync';
|
||||
|
||||
@ -59,10 +60,10 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
public constructor(state: DashboardSceneState) {
|
||||
super(state);
|
||||
|
||||
this.addActivationHandler(() => this.onActivate());
|
||||
this.addActivationHandler(() => this._activationHandler());
|
||||
}
|
||||
|
||||
private onActivate() {
|
||||
private _activationHandler() {
|
||||
if (this.state.isEditing) {
|
||||
this.startTrackingChanges();
|
||||
}
|
||||
@ -119,13 +120,17 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
};
|
||||
|
||||
public onSave = () => {
|
||||
this.setState({ drawer: new SaveDashboardDrawer(this) });
|
||||
this.setState({ drawer: new SaveDashboardDrawer({ dashboardRef: new SceneObjectRef(this) }) });
|
||||
};
|
||||
|
||||
public getPageNav(location: H.Location) {
|
||||
let pageNav: NavModelItem = {
|
||||
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) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { AppEvents } from '@grafana/data';
|
||||
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 { PanelInspectDrawer } from '../inspect/PanelInspectDrawer';
|
||||
@ -34,7 +34,7 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
|
||||
}
|
||||
|
||||
update.inspectPanelKey = values.inspect;
|
||||
update.drawer = new PanelInspectDrawer(panel);
|
||||
update.drawer = new PanelInspectDrawer({ panelRef: new SceneObjectRef(panel) });
|
||||
} else if (inspectPanelId) {
|
||||
update.inspectPanelKey = undefined;
|
||||
update.drawer = undefined;
|
||||
|
@ -3,6 +3,10 @@ import { locationService } from '@grafana/runtime';
|
||||
import { VizPanel, VizPanelMenu } from '@grafana/scenes';
|
||||
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).
|
||||
*/
|
||||
@ -10,26 +14,45 @@ export function panelMenuBehavior(menu: VizPanelMenu) {
|
||||
// hm.. add another generic param to SceneObject to specify parent type?
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
const panel = menu.parent as VizPanel;
|
||||
|
||||
const location = locationService.getLocation();
|
||||
const items: PanelMenuItem[] = [];
|
||||
const panelId = getPanelIdForVizPanel(panel);
|
||||
const dashboard = panel.getRoot();
|
||||
|
||||
// TODO
|
||||
// Add tracking via reportInteraction (but preserve the fact that these are normal links)
|
||||
|
||||
if (dashboard instanceof DashboardScene) {
|
||||
items.push({
|
||||
text: t('panel.header-menu.view', `View`),
|
||||
iconClassName: 'eye',
|
||||
shortcut: 'v',
|
||||
// Hm... need the numeric id to be url compatible?
|
||||
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({
|
||||
text: t('panel.header-menu.inspect', `Inspect`),
|
||||
iconClassName: 'info-circle',
|
||||
shortcut: 'i',
|
||||
// Hm... need the numeric id to be url compatible?
|
||||
href: locationUtil.getUrlForPartial(location, { inspect: panel.state.key }),
|
||||
});
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
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 { SaveDashboardDiff } from 'app/features/dashboard/components/SaveDashboard/SaveDashboardDiff';
|
||||
import { jsonDiff } from 'app/features/dashboard/components/VersionHistory/utils';
|
||||
@ -9,21 +9,21 @@ import { DashboardScene } from '../scene/DashboardScene';
|
||||
|
||||
import { transformSceneToSaveModel } from './transformSceneToSaveModel';
|
||||
|
||||
interface SaveDashboardDrawerState extends SceneObjectState {}
|
||||
|
||||
export class SaveDashboardDrawer extends SceneObjectBase<SaveDashboardDrawerState> {
|
||||
constructor(public dashboard: DashboardScene) {
|
||||
super({});
|
||||
interface SaveDashboardDrawerState extends SceneObjectState {
|
||||
dashboardRef: SceneObjectRef<DashboardScene>;
|
||||
}
|
||||
|
||||
export class SaveDashboardDrawer extends SceneObjectBase<SaveDashboardDrawerState> {
|
||||
onClose = () => {
|
||||
this.dashboard.setState({ drawer: undefined });
|
||||
this.state.dashboardRef.resolve().setState({ drawer: undefined });
|
||||
};
|
||||
|
||||
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 changedSaveModel = transformSceneToSaveModel(model.dashboard);
|
||||
const changedSaveModel = transformSceneToSaveModel(dashboard);
|
||||
|
||||
const diff = jsonDiff(initialSaveModel, changedSaveModel);
|
||||
|
||||
@ -33,7 +33,7 @@ export class SaveDashboardDrawer extends SceneObjectBase<SaveDashboardDrawerStat
|
||||
// }
|
||||
|
||||
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} />
|
||||
</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';
|
||||
|
||||
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) {
|
||||
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')
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/scenes/dashboard/:uid/panel-edit/:panelId',
|
||||
component: SafeDynamicImport(
|
||||
() => import(/* webpackChunkName: "scenes"*/ 'app/features/dashboard-scene/pages/PanelEditPage')
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/scenes/grafana-monitoring',
|
||||
exact: false,
|
||||
|
Loading…
Reference in New Issue
Block a user