mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
DashboardScene: Support for discard, start at transform back to save model and save drawer (#73873)
* SceneDashboard: Discard changes now works * To save model works and start at save drawer * Update * Added missing file * Refactorings to keep responsibility more logical * Refactorings * Removed file * Fixed state issue * Update * Update
This commit is contained in:
parent
45a8ca3111
commit
412e545503
@ -1916,6 +1916,12 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/utils/test-utils.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"],
|
||||
|
@ -245,7 +245,7 @@
|
||||
"@grafana/lezer-traceql": "0.0.4",
|
||||
"@grafana/monaco-logql": "^0.0.7",
|
||||
"@grafana/runtime": "workspace:*",
|
||||
"@grafana/scenes": "^0.25.0",
|
||||
"@grafana/scenes": "^0.26.0",
|
||||
"@grafana/schema": "workspace:*",
|
||||
"@grafana/ui": "workspace:*",
|
||||
"@kusto/monaco-kusto": "^7.4.0",
|
||||
|
@ -24,7 +24,7 @@ interface PanelInspectDrawerState extends SceneObjectState {
|
||||
}
|
||||
|
||||
export class PanelInspectDrawer extends SceneObjectBase<PanelInspectDrawerState> {
|
||||
static Component = ScenePanelInspectorRenderer;
|
||||
static Component = PanelInspectRenderer;
|
||||
|
||||
// Not stored in state as this is just a reference and it never changes
|
||||
private _panel: VizPanel;
|
||||
@ -61,7 +61,7 @@ export class PanelInspectDrawer extends SceneObjectBase<PanelInspectDrawerState>
|
||||
};
|
||||
}
|
||||
|
||||
function ScenePanelInspectorRenderer({ model }: SceneComponentProps<PanelInspectDrawer>) {
|
||||
function PanelInspectRenderer({ model }: SceneComponentProps<PanelInspectDrawer>) {
|
||||
const { tabs } = model.useState();
|
||||
const location = useLocation();
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
|
@ -6,20 +6,20 @@ import { Page } from 'app/core/components/Page/Page';
|
||||
import PageLoader from 'app/core/components/PageLoader/PageLoader';
|
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
|
||||
import { getDashboardLoader } from '../serialization/DashboardsLoader';
|
||||
import { getDashboardScenePageStateManager } from './DashboardScenePageStateManager';
|
||||
|
||||
export interface Props extends GrafanaRouteComponentProps<{ uid: string }> {}
|
||||
|
||||
export const DashboardScenePage = ({ match }: Props) => {
|
||||
const loader = getDashboardLoader();
|
||||
const { dashboard, isLoading } = loader.useState();
|
||||
export function DashboardScenePage({ match }: Props) {
|
||||
const stateManager = getDashboardScenePageStateManager();
|
||||
const { dashboard, isLoading } = stateManager.useState();
|
||||
|
||||
useEffect(() => {
|
||||
loader.loadAndInit(match.params.uid);
|
||||
stateManager.loadAndInit(match.params.uid);
|
||||
return () => {
|
||||
loader.clearState();
|
||||
stateManager.clearState();
|
||||
};
|
||||
}, [loader, match.params.uid]);
|
||||
}, [stateManager, match.params.uid]);
|
||||
|
||||
if (!dashboard) {
|
||||
return (
|
||||
@ -31,6 +31,6 @@ export const DashboardScenePage = ({ match }: Props) => {
|
||||
}
|
||||
|
||||
return <dashboard.Component model={dashboard} />;
|
||||
};
|
||||
}
|
||||
|
||||
export default DashboardScenePage;
|
||||
|
@ -0,0 +1,78 @@
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { getUrlSyncManager } from '@grafana/scenes';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { setupLoadDashboardMock } from '../utils/test-utils';
|
||||
|
||||
import { DashboardScenePageStateManager } from './DashboardScenePageStateManager';
|
||||
|
||||
describe('DashboardScenePageStateManager', () => {
|
||||
describe('when fetching/loading a dashboard', () => {
|
||||
it('should call loader from server if the dashboard is not cached', async () => {
|
||||
const loadDashboardMock = setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} });
|
||||
|
||||
const loader = new DashboardScenePageStateManager({});
|
||||
await loader.loadAndInit('fake-dash');
|
||||
|
||||
expect(loadDashboardMock).toHaveBeenCalledWith('db', '', 'fake-dash');
|
||||
|
||||
// should use cache second time
|
||||
await loader.loadAndInit('fake-dash');
|
||||
expect(loadDashboardMock.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it("should error when the dashboard doesn't exist", async () => {
|
||||
setupLoadDashboardMock({ dashboard: undefined, meta: {} });
|
||||
|
||||
const loader = new DashboardScenePageStateManager({});
|
||||
await loader.loadAndInit('fake-dash');
|
||||
|
||||
expect(loader.state.dashboard).toBeUndefined();
|
||||
expect(loader.state.isLoading).toBe(false);
|
||||
expect(loader.state.loadError).toBe('Error: Dashboard not found');
|
||||
});
|
||||
|
||||
it('should initialize the dashboard scene with the loaded dashboard', async () => {
|
||||
setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} });
|
||||
|
||||
const loader = new DashboardScenePageStateManager({});
|
||||
await loader.loadAndInit('fake-dash');
|
||||
|
||||
expect(loader.state.dashboard?.state.uid).toBe('fake-dash');
|
||||
expect(loader.state.loadError).toBe(undefined);
|
||||
expect(loader.state.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
it('should use DashboardScene creator to initialize the scene', async () => {
|
||||
setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} });
|
||||
|
||||
const loader = new DashboardScenePageStateManager({});
|
||||
await loader.loadAndInit('fake-dash');
|
||||
|
||||
expect(loader.state.dashboard).toBeInstanceOf(DashboardScene);
|
||||
expect(loader.state.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
it('should initialize url sync', async () => {
|
||||
setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} });
|
||||
|
||||
locationService.partial({ from: 'now-5m', to: 'now' });
|
||||
|
||||
const loader = new DashboardScenePageStateManager({});
|
||||
await loader.loadAndInit('fake-dash');
|
||||
const dash = loader.state.dashboard;
|
||||
|
||||
expect(dash!.state.$timeRange?.state.from).toEqual('now-5m');
|
||||
|
||||
getUrlSyncManager().cleanUp(dash!);
|
||||
|
||||
// try loading again (and hitting cache)
|
||||
locationService.partial({ from: 'now-10m', to: 'now' });
|
||||
|
||||
await loader.loadAndInit('fake-dash');
|
||||
const dash2 = loader.state.dashboard;
|
||||
|
||||
expect(dash2!.state.$timeRange?.state.from).toEqual('now-10m');
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,58 @@
|
||||
import { StateManagerBase } from 'app/core/services/StateManagerBase';
|
||||
import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
|
||||
|
||||
export interface DashboardScenePageState {
|
||||
dashboard?: DashboardScene;
|
||||
isLoading?: boolean;
|
||||
loadError?: string;
|
||||
}
|
||||
|
||||
export class DashboardScenePageStateManager extends StateManagerBase<DashboardScenePageState> {
|
||||
private cache: Record<string, DashboardScene> = {};
|
||||
|
||||
async loadAndInit(uid: string) {
|
||||
try {
|
||||
const scene = await this.loadScene(uid);
|
||||
scene.startUrlSync();
|
||||
|
||||
this.cache[uid] = scene;
|
||||
this.setState({ dashboard: scene, isLoading: false });
|
||||
} catch (err) {
|
||||
this.setState({ isLoading: false, loadError: String(err) });
|
||||
}
|
||||
}
|
||||
|
||||
private async loadScene(uid: string): Promise<DashboardScene> {
|
||||
const fromCache = this.cache[uid];
|
||||
if (fromCache) {
|
||||
return fromCache;
|
||||
}
|
||||
|
||||
this.setState({ isLoading: true });
|
||||
|
||||
const rsp = await dashboardLoaderSrv.loadDashboard('db', '', uid);
|
||||
|
||||
if (rsp.dashboard) {
|
||||
return transformSaveModelToScene(rsp);
|
||||
}
|
||||
|
||||
throw new Error('Dashboard not found');
|
||||
}
|
||||
|
||||
public clearState() {
|
||||
this.setState({ dashboard: undefined, loadError: undefined, isLoading: false });
|
||||
}
|
||||
}
|
||||
|
||||
let stateManager: DashboardScenePageStateManager | null = null;
|
||||
|
||||
export function getDashboardScenePageStateManager(): DashboardScenePageStateManager {
|
||||
if (!stateManager) {
|
||||
stateManager = new DashboardScenePageStateManager({});
|
||||
}
|
||||
|
||||
return stateManager;
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { SceneGridItem, SceneGridLayout, VizPanel } from '@grafana/scenes';
|
||||
import { sceneGraph, SceneGridItem, SceneGridLayout, VizPanel } from '@grafana/scenes';
|
||||
|
||||
import { DashboardScene } from './DashboardScene';
|
||||
|
||||
@ -22,6 +22,34 @@ describe('DashboardScene', () => {
|
||||
expect(scene.state.viewPanelKey).toBe('panel-2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Editing and discarding', () => {
|
||||
describe('Given scene in edit mode', () => {
|
||||
let scene: DashboardScene;
|
||||
|
||||
beforeEach(() => {
|
||||
scene = buildTestScene();
|
||||
scene.onEnterEditMode();
|
||||
});
|
||||
|
||||
it('Should set isEditing to true', () => {
|
||||
expect(scene.state.isEditing).toBe(true);
|
||||
});
|
||||
|
||||
it('A change to griditem pos should set isDirty true', () => {
|
||||
const gridItem = sceneGraph.findObject(scene, (p) => p.state.key === 'griditem-1') as SceneGridItem;
|
||||
gridItem.setState({ x: 10, y: 0, width: 10, height: 10 });
|
||||
|
||||
expect(scene.state.isDirty).toBe(true);
|
||||
|
||||
// verify can discard change
|
||||
scene.onDiscard();
|
||||
|
||||
const gridItem2 = sceneGraph.findObject(scene, (p) => p.state.key === 'griditem-1') as SceneGridItem;
|
||||
expect(gridItem2.state.x).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function buildTestScene() {
|
||||
@ -30,6 +58,8 @@ function buildTestScene() {
|
||||
body: new SceneGridLayout({
|
||||
children: [
|
||||
new SceneGridItem({
|
||||
key: 'griditem-1',
|
||||
x: 0,
|
||||
body: new VizPanel({
|
||||
title: 'Panel A',
|
||||
key: 'panel-1',
|
||||
|
@ -1,6 +1,7 @@
|
||||
import * as H from 'history';
|
||||
import { Unsubscribable } from 'rxjs';
|
||||
|
||||
import { AppEvents, locationUtil, NavModelItem } from '@grafana/data';
|
||||
import { locationUtil, NavModelItem, UrlQueryMap } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import {
|
||||
getUrlSyncManager,
|
||||
@ -10,16 +11,16 @@ import {
|
||||
SceneObjectBase,
|
||||
SceneObjectState,
|
||||
SceneObjectStateChangedEvent,
|
||||
SceneObjectUrlSyncHandler,
|
||||
SceneObjectUrlValues,
|
||||
sceneUtils,
|
||||
} from '@grafana/scenes';
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
||||
import { PanelInspectDrawer } from '../inspect/PanelInspectDrawer';
|
||||
import { DashboardSceneRenderer } from '../scene/DashboardSceneRenderer';
|
||||
import { SaveDashboardDrawer } from '../serialization/SaveDashboardDrawer';
|
||||
import { findVizPanel } from '../utils/findVizPanel';
|
||||
import { forceRenderChildren } from '../utils/utils';
|
||||
|
||||
import { DashboardSceneUrlSync } from './DashboardSceneUrlSync';
|
||||
|
||||
export interface DashboardSceneState extends SceneObjectState {
|
||||
title: string;
|
||||
uid?: string;
|
||||
@ -39,45 +40,77 @@ export interface DashboardSceneState extends SceneObjectState {
|
||||
export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
static Component = DashboardSceneRenderer;
|
||||
|
||||
/**
|
||||
* Handles url sync
|
||||
*/
|
||||
protected _urlSync = new DashboardSceneUrlSync(this);
|
||||
/**
|
||||
* State before editing started
|
||||
*/
|
||||
private _initialState?: DashboardSceneState;
|
||||
/**
|
||||
* Url state before editing started
|
||||
*/
|
||||
private _initiallUrlState?: UrlQueryMap;
|
||||
/**
|
||||
* change tracking subscription
|
||||
*/
|
||||
private _changeTrackerSub?: Unsubscribable;
|
||||
|
||||
constructor(state: DashboardSceneState) {
|
||||
public constructor(state: DashboardSceneState) {
|
||||
super(state);
|
||||
|
||||
this.addActivationHandler(() => {
|
||||
return () => {
|
||||
getUrlSyncManager().cleanUp(this);
|
||||
};
|
||||
});
|
||||
|
||||
this.subscribeToEvent(SceneObjectStateChangedEvent, this.onChildStateChanged);
|
||||
this.addActivationHandler(() => this.onActivate());
|
||||
}
|
||||
|
||||
public onChildStateChanged = (event: SceneObjectStateChangedEvent) => {
|
||||
// Temporary hacky way to detect changes
|
||||
if (event.payload.changedObject instanceof SceneGridItem) {
|
||||
this.setState({ isDirty: true });
|
||||
private onActivate() {
|
||||
if (this.state.isEditing) {
|
||||
this.startTrackingChanges();
|
||||
}
|
||||
};
|
||||
|
||||
initUrlSync() {
|
||||
// Deactivation logic
|
||||
return () => {
|
||||
this.stopTrackingChanges();
|
||||
this.stopUrlSync();
|
||||
};
|
||||
}
|
||||
|
||||
public startUrlSync() {
|
||||
getUrlSyncManager().initSync(this);
|
||||
}
|
||||
|
||||
onEnterEditMode = () => {
|
||||
public stopUrlSync() {
|
||||
getUrlSyncManager().cleanUp(this);
|
||||
}
|
||||
|
||||
public onEnterEditMode = () => {
|
||||
// Save this state
|
||||
this._initialState = sceneUtils.cloneSceneObjectState(this.state);
|
||||
this._initiallUrlState = locationService.getSearchObject();
|
||||
|
||||
// Switch to edit mode
|
||||
this.setState({ isEditing: true });
|
||||
|
||||
// Make grid draggable
|
||||
// Propagate change edit mode change to children
|
||||
if (this.state.body instanceof SceneGridLayout) {
|
||||
this.state.body.setState({ isDraggable: true, isResizable: true });
|
||||
forceRenderChildren(this.state.body, true);
|
||||
}
|
||||
|
||||
this.startTrackingChanges();
|
||||
};
|
||||
|
||||
onDiscard = () => {
|
||||
// TODO open confirm modal if dirty
|
||||
// TODO actually discard changes
|
||||
this.setState({ isEditing: false });
|
||||
public onDiscard = () => {
|
||||
// No need to listen to changes anymore
|
||||
this.stopTrackingChanges();
|
||||
// Stop url sync before updating url
|
||||
this.stopUrlSync();
|
||||
// Now we can update url
|
||||
locationService.partial(this._initiallUrlState!, true);
|
||||
// Update state and disable editing
|
||||
this.setState({ ...this._initialState, isEditing: false });
|
||||
// and start url sync again
|
||||
this.startUrlSync();
|
||||
|
||||
// Disable grid dragging
|
||||
if (this.state.body instanceof SceneGridLayout) {
|
||||
@ -86,7 +119,11 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
}
|
||||
};
|
||||
|
||||
getPageNav(location: H.Location) {
|
||||
public onSave = () => {
|
||||
this.setState({ drawer: new SaveDashboardDrawer(this) });
|
||||
};
|
||||
|
||||
public getPageNav(location: H.Location) {
|
||||
let pageNav: NavModelItem = {
|
||||
text: this.state.title,
|
||||
url: locationUtil.getUrlForPartial(location, { viewPanel: null, inspect: null }),
|
||||
@ -105,60 +142,27 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
/**
|
||||
* Returns the body (layout) or the full view panel
|
||||
*/
|
||||
getBodyToRender(viewPanelKey?: string): SceneObject {
|
||||
public getBodyToRender(viewPanelKey?: string): SceneObject {
|
||||
const viewPanel = findVizPanel(this, viewPanelKey);
|
||||
return viewPanel ?? this.state.body;
|
||||
}
|
||||
}
|
||||
|
||||
class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
|
||||
constructor(private _scene: DashboardScene) {}
|
||||
|
||||
getKeys(): string[] {
|
||||
return ['inspect', 'viewPanel'];
|
||||
private startTrackingChanges() {
|
||||
this._changeTrackerSub = this.subscribeToEvent(
|
||||
SceneObjectStateChangedEvent,
|
||||
(event: SceneObjectStateChangedEvent) => {
|
||||
if (event.payload.changedObject instanceof SceneGridItem) {
|
||||
this.setState({ isDirty: true });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getUrlState(): SceneObjectUrlValues {
|
||||
const state = this._scene.state;
|
||||
return { inspect: state.inspectPanelKey, viewPanel: state.viewPanelKey };
|
||||
private stopTrackingChanges() {
|
||||
this._changeTrackerSub?.unsubscribe();
|
||||
}
|
||||
|
||||
updateFromUrl(values: SceneObjectUrlValues): void {
|
||||
const { inspectPanelKey, viewPanelKey } = this._scene.state;
|
||||
const update: Partial<DashboardSceneState> = {};
|
||||
|
||||
// Handle inspect object state
|
||||
if (typeof values.inspect === 'string') {
|
||||
const panel = findVizPanel(this._scene, values.inspect);
|
||||
if (!panel) {
|
||||
appEvents.emit(AppEvents.alertError, ['Panel not found']);
|
||||
locationService.partial({ inspect: null });
|
||||
return;
|
||||
}
|
||||
|
||||
update.inspectPanelKey = values.inspect;
|
||||
update.drawer = new PanelInspectDrawer(panel);
|
||||
} else if (inspectPanelKey) {
|
||||
update.inspectPanelKey = undefined;
|
||||
update.drawer = undefined;
|
||||
}
|
||||
|
||||
// Handle view panel state
|
||||
if (typeof values.viewPanel === 'string') {
|
||||
const panel = findVizPanel(this._scene, values.viewPanel);
|
||||
if (!panel) {
|
||||
appEvents.emit(AppEvents.alertError, ['Panel not found']);
|
||||
locationService.partial({ viewPanel: null });
|
||||
return;
|
||||
}
|
||||
|
||||
update.viewPanelKey = values.viewPanel;
|
||||
} else if (viewPanelKey) {
|
||||
update.viewPanelKey = undefined;
|
||||
}
|
||||
|
||||
if (Object.keys(update).length > 0) {
|
||||
this._scene.setState(update);
|
||||
}
|
||||
public getInitialState(): DashboardSceneState | undefined {
|
||||
return this._initialState;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,61 @@
|
||||
import { AppEvents } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { SceneObjectUrlSyncHandler, SceneObjectUrlValues } from '@grafana/scenes';
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
||||
import { PanelInspectDrawer } from '../inspect/PanelInspectDrawer';
|
||||
import { findVizPanel } from '../utils/findVizPanel';
|
||||
|
||||
import { DashboardScene, DashboardSceneState } from './DashboardScene';
|
||||
|
||||
export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler {
|
||||
constructor(private _scene: DashboardScene) {}
|
||||
|
||||
getKeys(): string[] {
|
||||
return ['inspect', 'viewPanel'];
|
||||
}
|
||||
|
||||
getUrlState(): SceneObjectUrlValues {
|
||||
const state = this._scene.state;
|
||||
return { inspect: state.inspectPanelKey, viewPanel: state.viewPanelKey };
|
||||
}
|
||||
|
||||
updateFromUrl(values: SceneObjectUrlValues): void {
|
||||
const { inspectPanelKey, viewPanelKey } = this._scene.state;
|
||||
const update: Partial<DashboardSceneState> = {};
|
||||
|
||||
// Handle inspect object state
|
||||
if (typeof values.inspect === 'string') {
|
||||
const panel = findVizPanel(this._scene, values.inspect);
|
||||
if (!panel) {
|
||||
appEvents.emit(AppEvents.alertError, ['Panel not found']);
|
||||
locationService.partial({ inspect: null });
|
||||
return;
|
||||
}
|
||||
|
||||
update.inspectPanelKey = values.inspect;
|
||||
update.drawer = new PanelInspectDrawer(panel);
|
||||
} else if (inspectPanelKey) {
|
||||
update.inspectPanelKey = undefined;
|
||||
update.drawer = undefined;
|
||||
}
|
||||
|
||||
// Handle view panel state
|
||||
if (typeof values.viewPanel === 'string') {
|
||||
const panel = findVizPanel(this._scene, values.viewPanel);
|
||||
if (!panel) {
|
||||
appEvents.emit(AppEvents.alertError, ['Panel not found']);
|
||||
locationService.partial({ viewPanel: null });
|
||||
return;
|
||||
}
|
||||
|
||||
update.viewPanelKey = values.viewPanel;
|
||||
} else if (viewPanelKey) {
|
||||
update.viewPanelKey = undefined;
|
||||
}
|
||||
|
||||
if (Object.keys(update).length > 0) {
|
||||
this._scene.setState(update);
|
||||
}
|
||||
}
|
||||
}
|
@ -62,7 +62,7 @@ export const NavToolbarActions = React.memo<Props>(({ dashboard }) => {
|
||||
} else {
|
||||
// TODO check permissions
|
||||
toolbarActions.push(
|
||||
<Button onClick={dashboard.onEnterEditMode} tooltip="Save as copy" fill="text" key="save-as">
|
||||
<Button onClick={dashboard.onSave} tooltip="Save as copy" fill="text" key="save-as">
|
||||
Save as
|
||||
</Button>
|
||||
);
|
||||
@ -72,7 +72,7 @@ export const NavToolbarActions = React.memo<Props>(({ dashboard }) => {
|
||||
</Button>
|
||||
);
|
||||
toolbarActions.push(
|
||||
<Button onClick={dashboard.onEnterEditMode} tooltip="Save changes" key="save" disabled={!isDirty}>
|
||||
<Button onClick={dashboard.onSave} tooltip="Save changes" key="save" disabled={!isDirty}>
|
||||
Save
|
||||
</Button>
|
||||
);
|
||||
|
@ -0,0 +1,41 @@
|
||||
import React from 'react';
|
||||
|
||||
import { SceneComponentProps, SceneObjectBase, SceneObjectState } 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';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
|
||||
import { transformSceneToSaveModel } from './transformSceneToSaveModel';
|
||||
|
||||
interface SaveDashboardDrawerState extends SceneObjectState {}
|
||||
|
||||
export class SaveDashboardDrawer extends SceneObjectBase<SaveDashboardDrawerState> {
|
||||
constructor(public dashboard: DashboardScene) {
|
||||
super({});
|
||||
}
|
||||
|
||||
onClose = () => {
|
||||
this.dashboard.setState({ drawer: undefined });
|
||||
};
|
||||
|
||||
static Component = ({ model }: SceneComponentProps<SaveDashboardDrawer>) => {
|
||||
const initialScene = new DashboardScene(model.dashboard.getInitialState()!);
|
||||
const initialSaveModel = transformSceneToSaveModel(initialScene);
|
||||
const changedSaveModel = transformSceneToSaveModel(model.dashboard);
|
||||
|
||||
const diff = jsonDiff(initialSaveModel, changedSaveModel);
|
||||
|
||||
// let diffCount = 0;
|
||||
// for (const d of Object.values(diff)) {
|
||||
// diffCount += d.length;
|
||||
// }
|
||||
|
||||
return (
|
||||
<Drawer title="Save dashboard" subtitle={model.dashboard.state.title} scrollableContent onClose={model.onClose}>
|
||||
<SaveDashboardDiff diff={diff} oldValue={initialSaveModel} newValue={changedSaveModel} />
|
||||
</Drawer>
|
||||
);
|
||||
};
|
||||
}
|
@ -0,0 +1,98 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`transformSceneToSaveModel Given a scene Should transfrom back to peristed model 1`] = `
|
||||
{
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"links": [],
|
||||
"panels": [
|
||||
{
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic",
|
||||
},
|
||||
"custom": {
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false,
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear",
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none",
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off",
|
||||
},
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"overrides": [],
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
},
|
||||
"id": 28,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true,
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none",
|
||||
},
|
||||
},
|
||||
"title": "Simple time series graph ",
|
||||
"transformations": [],
|
||||
"transparent": false,
|
||||
"type": "timeseries",
|
||||
},
|
||||
],
|
||||
"schemaVersion": 36,
|
||||
"tags": [],
|
||||
"time": {
|
||||
"from": "now-5m",
|
||||
"to": "now",
|
||||
},
|
||||
"timezone": "browser",
|
||||
"title": "Dashboard to load1",
|
||||
"uid": "nP8rcffGkasd",
|
||||
}
|
||||
`;
|
@ -0,0 +1,294 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"type": "datasource",
|
||||
"uid": "grafana"
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": 1351,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 28,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"alias": "series",
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk",
|
||||
"seriesCount": 1
|
||||
}
|
||||
],
|
||||
"title": "Simple time series graph ",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"collapsed": false,
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 1,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 8
|
||||
},
|
||||
"id": 5,
|
||||
"panels": [],
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Row title",
|
||||
"type": "row"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 0,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"insertNulls": false,
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 10,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 9
|
||||
},
|
||||
"id": 29,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"alias": "series",
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk",
|
||||
"seriesCount": 1
|
||||
}
|
||||
],
|
||||
"title": "panel inside row",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 10,
|
||||
"w": 11,
|
||||
"x": 12,
|
||||
"y": 9
|
||||
},
|
||||
"id": 25,
|
||||
"links": [],
|
||||
"options": {
|
||||
"code": {
|
||||
"language": "plaintext",
|
||||
"showLineNumbers": false,
|
||||
"showMiniMap": false
|
||||
},
|
||||
"content": "content",
|
||||
"mode": "markdown"
|
||||
},
|
||||
"pluginVersion": "10.2.0-pre",
|
||||
"targets": [
|
||||
{
|
||||
"alias": "__server_names",
|
||||
"datasource": {
|
||||
"type": "testdata",
|
||||
"uid": "PD8C576611E62080A"
|
||||
},
|
||||
"refId": "A",
|
||||
"scenarioId": "random_walk",
|
||||
"seriesCount": 6
|
||||
}
|
||||
],
|
||||
"title": "Transparent text panel",
|
||||
"transparent": true,
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
"refresh": "",
|
||||
"schemaVersion": 38,
|
||||
"style": "dark",
|
||||
"tags": ["gdev", "graph-ng", "demo"],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-5m",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {
|
||||
"refresh_intervals": ["10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"]
|
||||
},
|
||||
"timezone": "America/New_York",
|
||||
"title": "Dashboard to load1",
|
||||
"uid": "nP8rcffGkasd",
|
||||
"version": 2,
|
||||
"weekStart": ""
|
||||
}
|
@ -1,10 +1,9 @@
|
||||
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import { config } from '@grafana/runtime';
|
||||
import {
|
||||
behaviors,
|
||||
CustomVariable,
|
||||
DataSourceVariable,
|
||||
getUrlSyncManager,
|
||||
QueryVariable,
|
||||
SceneDataTransformer,
|
||||
SceneGridItem,
|
||||
@ -19,91 +18,15 @@ import { createPanelJSONFixture } from 'app/features/dashboard/state/__fixtures_
|
||||
import { SHARED_DASHBOARD_QUERY } from 'app/plugins/datasource/dashboard';
|
||||
import { DASHBOARD_DATASOURCE_PLUGIN_ID } from 'app/plugins/datasource/dashboard/types';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { ShareQueryDataProvider } from '../scene/ShareQueryDataProvider';
|
||||
import { setupLoadDashboardMock } from '../utils/test-utils';
|
||||
|
||||
import {
|
||||
createDashboardSceneFromDashboardModel,
|
||||
createVizPanelFromPanelModel,
|
||||
createSceneVariableFromVariableModel,
|
||||
DashboardLoader,
|
||||
} from './DashboardsLoader';
|
||||
} from './transformSaveModelToScene';
|
||||
|
||||
describe('DashboardLoader', () => {
|
||||
describe('when fetching/loading a dashboard', () => {
|
||||
beforeEach(() => {
|
||||
new DashboardLoader({});
|
||||
});
|
||||
|
||||
it('should call dashboard loader server if the dashboard is not cached', async () => {
|
||||
const loadDashboardMock = setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} });
|
||||
|
||||
const loader = new DashboardLoader({});
|
||||
await loader.loadAndInit('fake-dash');
|
||||
|
||||
expect(loadDashboardMock).toHaveBeenCalledWith('db', '', 'fake-dash');
|
||||
|
||||
// should use cache second time
|
||||
await loader.loadAndInit('fake-dash');
|
||||
expect(loadDashboardMock.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it("should error when the dashboard doesn't exist", async () => {
|
||||
setupLoadDashboardMock({ dashboard: undefined, meta: {} });
|
||||
|
||||
const loader = new DashboardLoader({});
|
||||
await loader.loadAndInit('fake-dash');
|
||||
|
||||
expect(loader.state.dashboard).toBeUndefined();
|
||||
expect(loader.state.isLoading).toBe(false);
|
||||
expect(loader.state.loadError).toBe('Error: Dashboard not found');
|
||||
});
|
||||
|
||||
it('should initialize the dashboard scene with the loaded dashboard', async () => {
|
||||
setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} });
|
||||
|
||||
const loader = new DashboardLoader({});
|
||||
await loader.loadAndInit('fake-dash');
|
||||
|
||||
expect(loader.state.dashboard?.state.uid).toBe('fake-dash');
|
||||
expect(loader.state.loadError).toBe(undefined);
|
||||
expect(loader.state.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
it('should use DashboardScene creator to initialize the scene', async () => {
|
||||
setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} });
|
||||
|
||||
const loader = new DashboardLoader({});
|
||||
await loader.loadAndInit('fake-dash');
|
||||
|
||||
expect(loader.state.dashboard).toBeInstanceOf(DashboardScene);
|
||||
expect(loader.state.isLoading).toBe(false);
|
||||
});
|
||||
|
||||
it('should initialize url sync', async () => {
|
||||
setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} });
|
||||
|
||||
locationService.partial({ from: 'now-5m', to: 'now' });
|
||||
|
||||
const loader = new DashboardLoader({});
|
||||
await loader.loadAndInit('fake-dash');
|
||||
const dash = loader.state.dashboard;
|
||||
|
||||
expect(dash!.state.$timeRange?.state.from).toEqual('now-5m');
|
||||
|
||||
getUrlSyncManager().cleanUp(dash!);
|
||||
|
||||
// try loading again (and hitting cache)
|
||||
locationService.partial({ from: 'now-10m', to: 'now' });
|
||||
|
||||
await loader.loadAndInit('fake-dash');
|
||||
const dash2 = loader.state.dashboard;
|
||||
|
||||
expect(dash2!.state.$timeRange?.state.from).toEqual('now-10m');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when creating dashboard scene', () => {
|
||||
it('should initialize the DashboardScene with the model state', () => {
|
||||
const dash = {
|
@ -25,9 +25,8 @@ import {
|
||||
VizPanelMenu,
|
||||
behaviors,
|
||||
} from '@grafana/scenes';
|
||||
import { StateManagerBase } from 'app/core/services/StateManagerBase';
|
||||
import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
|
||||
import { DashboardModel, PanelModel } from 'app/features/dashboard/state';
|
||||
import { DashboardDTO } from 'app/types';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { LibraryVizPanel } from '../scene/LibraryVizPanel';
|
||||
@ -41,46 +40,13 @@ export interface DashboardLoaderState {
|
||||
loadError?: string;
|
||||
}
|
||||
|
||||
export class DashboardLoader extends StateManagerBase<DashboardLoaderState> {
|
||||
private cache: Record<string, DashboardScene> = {};
|
||||
export function transformSaveModelToScene(rsp: DashboardDTO): DashboardScene {
|
||||
// Just to have migrations run
|
||||
const oldModel = new DashboardModel(rsp.dashboard, rsp.meta, {
|
||||
autoMigrateOldPanels: true,
|
||||
});
|
||||
|
||||
async loadAndInit(uid: string) {
|
||||
try {
|
||||
const scene = await this.loadScene(uid);
|
||||
scene.initUrlSync();
|
||||
|
||||
this.cache[uid] = scene;
|
||||
this.setState({ dashboard: scene, isLoading: false });
|
||||
} catch (err) {
|
||||
this.setState({ isLoading: false, loadError: String(err) });
|
||||
}
|
||||
}
|
||||
|
||||
private async loadScene(uid: string): Promise<DashboardScene> {
|
||||
const fromCache = this.cache[uid];
|
||||
if (fromCache) {
|
||||
return fromCache;
|
||||
}
|
||||
|
||||
this.setState({ isLoading: true });
|
||||
|
||||
const rsp = await dashboardLoaderSrv.loadDashboard('db', '', uid);
|
||||
|
||||
if (rsp.dashboard) {
|
||||
// Just to have migrations run
|
||||
const oldModel = new DashboardModel(rsp.dashboard, rsp.meta, {
|
||||
autoMigrateOldPanels: true,
|
||||
});
|
||||
|
||||
return createDashboardSceneFromDashboardModel(oldModel);
|
||||
}
|
||||
|
||||
throw new Error('Dashboard not found');
|
||||
}
|
||||
|
||||
public clearState() {
|
||||
this.setState({ dashboard: undefined, loadError: undefined, isLoading: false });
|
||||
}
|
||||
return createDashboardSceneFromDashboardModel(oldModel);
|
||||
}
|
||||
|
||||
export function createSceneObjectsForPanels(oldPanels: PanelModel[]): Array<SceneGridItem | SceneGridRow> {
|
||||
@ -280,6 +246,7 @@ export function createSceneVariableFromVariableModel(variable: VariableModel): S
|
||||
|
||||
export function createVizPanelFromPanelModel(panel: PanelModel) {
|
||||
return new SceneGridItem({
|
||||
key: `grid-item-${panel.id}`,
|
||||
x: panel.gridPos.x,
|
||||
y: panel.gridPos.y,
|
||||
width: panel.gridPos.w,
|
||||
@ -302,16 +269,6 @@ export function createVizPanelFromPanelModel(panel: PanelModel) {
|
||||
});
|
||||
}
|
||||
|
||||
let loader: DashboardLoader | null = null;
|
||||
|
||||
export function getDashboardLoader(): DashboardLoader {
|
||||
if (!loader) {
|
||||
loader = new DashboardLoader({});
|
||||
}
|
||||
|
||||
return loader;
|
||||
}
|
||||
|
||||
const isCustomVariable = (v: VariableModel): v is CustomVariableModel => v.type === 'custom';
|
||||
const isQueryVariable = (v: VariableModel): v is QueryVariableModel => v.type === 'query';
|
||||
const isDataSourceVariable = (v: VariableModel): v is DataSourceVariableModel => v.type === 'datasource';
|
@ -0,0 +1,14 @@
|
||||
import dashboard_to_load1 from './testfiles/dashboard_to_load1.json';
|
||||
import { transformSaveModelToScene } from './transformSaveModelToScene';
|
||||
import { transformSceneToSaveModel } from './transformSceneToSaveModel';
|
||||
|
||||
describe('transformSceneToSaveModel', () => {
|
||||
describe('Given a scene', () => {
|
||||
it('Should transfrom back to peristed model', () => {
|
||||
const scene = transformSaveModelToScene({ dashboard: dashboard_to_load1 as any, meta: {} });
|
||||
const saveModel = transformSceneToSaveModel(scene);
|
||||
|
||||
expect(saveModel).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,59 @@
|
||||
import { SceneGridItem, SceneGridLayout, VizPanel } from '@grafana/scenes';
|
||||
import { Dashboard, defaultDashboard, FieldConfigSource, Panel } from '@grafana/schema';
|
||||
import { sortedDeepCloneWithoutNulls } from 'app/core/utils/object';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { getPanelIdForVizPanelKey } from '../utils/utils';
|
||||
|
||||
export function transformSceneToSaveModel(scene: DashboardScene): Dashboard {
|
||||
const state = scene.state;
|
||||
const timeRange = state.$timeRange!.state;
|
||||
const body = state.body;
|
||||
const panels: Panel[] = [];
|
||||
|
||||
if (body instanceof SceneGridLayout) {
|
||||
for (const child of body.state.children) {
|
||||
if (child instanceof SceneGridItem) {
|
||||
panels.push(gridItemToPanel(child));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const dashboard: Dashboard = {
|
||||
...defaultDashboard,
|
||||
title: state.title,
|
||||
uid: state.uid,
|
||||
time: {
|
||||
from: timeRange.from,
|
||||
to: timeRange.to,
|
||||
},
|
||||
panels,
|
||||
};
|
||||
|
||||
return sortedDeepCloneWithoutNulls(dashboard);
|
||||
}
|
||||
|
||||
function gridItemToPanel(gridItem: SceneGridItem): Panel {
|
||||
const vizPanel = gridItem.state.body;
|
||||
if (!(vizPanel instanceof VizPanel)) {
|
||||
throw new Error('SceneGridItem body expected to be VizPanel');
|
||||
}
|
||||
|
||||
const panel: Panel = {
|
||||
id: getPanelIdForVizPanelKey(vizPanel.state.key!),
|
||||
type: vizPanel.state.pluginId,
|
||||
title: vizPanel.state.title,
|
||||
gridPos: {
|
||||
x: gridItem.state.x ?? 0,
|
||||
y: gridItem.state.y ?? 0,
|
||||
w: gridItem.state.width ?? 0,
|
||||
h: gridItem.state.height ?? 0,
|
||||
},
|
||||
options: vizPanel.state.options,
|
||||
fieldConfig: (vizPanel.state.fieldConfig as FieldConfigSource) ?? { defaults: {}, overrides: [] },
|
||||
transformations: [],
|
||||
transparent: false,
|
||||
};
|
||||
|
||||
return panel;
|
||||
}
|
@ -4,6 +4,10 @@ export function getVizPanelKeyForPanelId(panelId: number) {
|
||||
return `panel-${panelId}`;
|
||||
}
|
||||
|
||||
export function getPanelIdForVizPanelKey(key: string) {
|
||||
return parseInt(key.replace('panel-', ''), 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* Useful from tests to simulate mounting a full scene. Children are activated before parents to simulate the real order
|
||||
* of React mount order and useEffect ordering.
|
||||
|
@ -4,7 +4,7 @@ import { dateTimeFormat, formattedValueToString, getValueFormat, SelectableValue
|
||||
import { config } from '@grafana/runtime';
|
||||
import { SceneObject } from '@grafana/scenes';
|
||||
import { StateManagerBase } from 'app/core/services/StateManagerBase';
|
||||
import { createDashboardSceneFromDashboardModel } from 'app/features/dashboard-scene/serialization/DashboardsLoader';
|
||||
import { createDashboardSceneFromDashboardModel } from 'app/features/dashboard-scene/serialization/transformSaveModelToScene';
|
||||
|
||||
import { getTimeSrv } from '../../services/TimeSrv';
|
||||
import { DashboardModel, PanelModel } from '../../state';
|
||||
|
10
yarn.lock
10
yarn.lock
@ -3937,9 +3937,9 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@grafana/scenes@npm:^0.25.0":
|
||||
version: 0.25.0
|
||||
resolution: "@grafana/scenes@npm:0.25.0"
|
||||
"@grafana/scenes@npm:^0.26.0":
|
||||
version: 0.26.0
|
||||
resolution: "@grafana/scenes@npm:0.26.0"
|
||||
dependencies:
|
||||
"@grafana/e2e-selectors": 10.0.2
|
||||
react-grid-layout: 1.3.4
|
||||
@ -3951,7 +3951,7 @@ __metadata:
|
||||
"@grafana/runtime": 10.0.3
|
||||
"@grafana/schema": 10.0.3
|
||||
"@grafana/ui": 10.0.3
|
||||
checksum: ab81d18bb65b0b5d612708fbd9351022141248b476009bdde01660cce1747bd0724fdf599842127f48feea49b31858dc64e45af2638d90fd3b09d59e350f79e4
|
||||
checksum: 041458e463cde07179c75444f245411715ed50f072a5c4d519e258d0e0d57864c55e9cce3fb245943e2bc4245ee48e243701f5f0290c33e0374d4e948820eb14
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@ -19257,7 +19257,7 @@ __metadata:
|
||||
"@grafana/lezer-traceql": 0.0.4
|
||||
"@grafana/monaco-logql": ^0.0.7
|
||||
"@grafana/runtime": "workspace:*"
|
||||
"@grafana/scenes": ^0.25.0
|
||||
"@grafana/scenes": ^0.26.0
|
||||
"@grafana/schema": "workspace:*"
|
||||
"@grafana/toolkit": "workspace:*"
|
||||
"@grafana/tsconfig": ^1.3.0-rc1
|
||||
|
Loading…
Reference in New Issue
Block a user