dashboard: expose DashboardScene JSON API with recovery fallback

This commit is contained in:
oscarkilhed
2025-12-20 14:55:18 +01:00
parent ece38641ca
commit 1a7c2a4f38
10 changed files with 701 additions and 0 deletions
@@ -0,0 +1,57 @@
export interface DashboardSceneJsonApiV2 {
/**
* Read the currently open dashboard as v2beta1 Dashboard kind JSON (JSON string).
*/
getCurrentDashboard(space?: number): string;
/**
* Apply a v2beta1 Dashboard kind JSON (JSON string).
*
* Implementations must enforce **spec-only** updates by rejecting any changes to
* `apiVersion`, `kind`, `metadata`, or `status`.
*/
applyCurrentDashboard(resourceJson: string): void;
}
let singletonInstance: DashboardSceneJsonApiV2 | undefined;
/**
* Used during startup by Grafana to register the implementation.
*
* @internal
*/
export function setDashboardSceneJsonApiV2(instance: DashboardSceneJsonApiV2) {
singletonInstance = instance;
}
/**
* Returns the registered DashboardScene JSON API.
*
* @public
*/
export function getDashboardSceneJsonApiV2(): DashboardSceneJsonApiV2 {
if (!singletonInstance) {
throw new Error('DashboardScene JSON API is not available');
}
return singletonInstance;
}
/**
* Plugin-friendly helper to read the current dashboard as kind JSON (JSON string).
*
* @public
*/
export function getCurrentDashboard(space = 2): string {
return getDashboardSceneJsonApiV2().getCurrentDashboard(space);
}
/**
* JSON-string helper to apply a v2 Dashboard kind JSON (spec-only enforcement happens in the implementation).
*
* @public
*/
export function applyCurrentDashboard(resourceJson: string): void {
return getDashboardSceneJsonApiV2().applyCurrentDashboard(resourceJson);
}
@@ -6,6 +6,7 @@ export * from './templateSrv';
export * from './live';
export * from './LocationService';
export * from './appEvents';
export * from './dashboardSceneJsonApi';
export {
setPluginComponentHook,
+6
View File
@@ -41,6 +41,7 @@ import {
setCorrelationsService,
setPluginFunctionsHook,
setMegaMenuOpenHook,
setDashboardSceneJsonApiV2,
} from '@grafana/runtime';
import {
initOpenFeature,
@@ -85,6 +86,7 @@ import { startMeasure, stopMeasure } from './core/utils/metrics';
import { initAlerting } from './features/alerting/unified/initAlerting';
import { initAuthConfig } from './features/auth-config';
import { getTimeSrv } from './features/dashboard/services/TimeSrv';
import { dashboardSceneJsonApiV2 } from './features/dashboard-scene/api/runtimeDashboardSceneJsonApiV2';
import { EmbeddedDashboardLazy } from './features/dashboard-scene/embedding/EmbeddedDashboardLazy';
import { DashboardLevelTimeMacro } from './features/dashboard-scene/scene/DashboardLevelTimeMacro';
import { initGrafanaLive } from './features/live';
@@ -251,6 +253,10 @@ export class GrafanaApp {
const dataSourceSrv = new DatasourceSrv();
dataSourceSrv.init(config.datasources, config.defaultDatasource);
setDataSourceSrv(dataSourceSrv);
// Expose current-dashboard schema-v2 JSON APIs to plugins via @grafana/runtime
setDashboardSceneJsonApiV2(dashboardSceneJsonApiV2);
initWindowRuntime();
// Do not pre-load apps if rendererDisableAppPluginsPreload is true and the request comes from the image renderer
@@ -0,0 +1,68 @@
import { defaultSpec as defaultDashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2';
import { getCurrentDashboardKindV2 } from './currentDashboardKindV2';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
config: {
featureToggles: {
kubernetesDashboards: true,
kubernetesDashboardsV2: true,
dashboardNewLayouts: false,
},
},
}));
jest.mock('../pages/DashboardScenePageStateManager', () => ({
getDashboardScenePageStateManager: jest.fn(),
}));
describe('getCurrentDashboardKindV2', () => {
const { getDashboardScenePageStateManager } = jest.requireMock('../pages/DashboardScenePageStateManager');
beforeEach(() => {
jest.clearAllMocks();
});
it('returns v2beta1 Dashboard kind JSON for the currently open dashboard', () => {
const spec = defaultDashboardV2Spec();
const dashboard = {
state: {
uid: 'dash-uid',
meta: {
k8s: {
name: 'dash-uid',
resourceVersion: '1',
creationTimestamp: 'now',
annotations: {},
labels: {},
},
},
},
getSaveResource: () => ({
apiVersion: 'dashboard.grafana.app/v2beta1',
kind: 'Dashboard',
metadata: { name: 'dash-uid' },
spec,
}),
};
getDashboardScenePageStateManager.mockReturnValue({
state: { dashboard },
});
const res = getCurrentDashboardKindV2();
expect(res.apiVersion).toBe('dashboard.grafana.app/v2beta1');
expect(res.kind).toBe('Dashboard');
expect(res.metadata.name).toBe('dash-uid');
expect(res.spec).toBe(spec);
});
it('throws if no dashboard is currently open', () => {
getDashboardScenePageStateManager.mockReturnValue({ state: { dashboard: undefined } });
expect(() => getCurrentDashboardKindV2()).toThrow('No dashboard is currently open');
});
});
@@ -0,0 +1,70 @@
import { config } from '@grafana/runtime';
import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2';
import { Status } from '@grafana/schema/src/schema/dashboard/v2';
import { Resource } from 'app/features/apiserver/types';
import { isDashboardV2Spec } from 'app/features/dashboard/api/utils';
import { getDashboardScenePageStateManager } from '../pages/DashboardScenePageStateManager';
function assertDashboardV2Enabled() {
const isKubernetesDashboardsEnabled = Boolean(config.featureToggles.kubernetesDashboards);
const isV2Enabled = Boolean(config.featureToggles.kubernetesDashboardsV2 || config.featureToggles.dashboardNewLayouts);
if (!isKubernetesDashboardsEnabled || !isV2Enabled) {
throw new Error('V2 dashboard kinds API requires kubernetes dashboards v2 to be enabled');
}
}
function getCurrentDashboardScene() {
const mgr = getDashboardScenePageStateManager();
const dashboard = mgr.state.dashboard;
if (!dashboard) {
throw new Error('No dashboard is currently open');
}
return dashboard;
}
/**
* Returns the currently open dashboard as a v2beta1 Dashboard kind JSON resource.
*
* Note: This is intentionally scoped to the current `DashboardScene` only (no lookups by UID).
*/
export function getCurrentDashboardKindV2(): Resource<DashboardV2Spec, Status, 'Dashboard'> {
assertDashboardV2Enabled();
const scene = getCurrentDashboardScene();
// Use the scene’s canonical “save resource” representation to avoid hand-assembling fields.
const saveResource = scene.getSaveResource({ isNew: !scene.state.uid });
if (saveResource.apiVersion !== 'dashboard.grafana.app/v2beta1' || saveResource.kind !== 'Dashboard') {
throw new Error('Current dashboard is not a v2beta1 Dashboard resource');
}
const spec = saveResource.spec as unknown;
if (!isDashboardV2Spec(spec)) {
throw new Error('Current dashboard is not using schema v2 spec');
}
const k8sMeta = scene.state.meta.k8s;
if (!k8sMeta) {
throw new Error('Current dashboard is missing Kubernetes metadata');
}
return {
apiVersion: saveResource.apiVersion,
kind: 'Dashboard',
metadata: {
...k8sMeta,
// Prefer the metadata coming from the save resource for name/generateName if present.
name: (saveResource.metadata?.name ?? k8sMeta.name) as string,
namespace: saveResource.metadata?.namespace ?? k8sMeta.namespace,
labels: saveResource.metadata?.labels ?? k8sMeta.labels,
annotations: saveResource.metadata?.annotations ?? k8sMeta.annotations,
},
spec,
// We currently don’t persist/status-sync status in the scene; keep it stable and non-authoritative.
status: {} as Status,
};
}
@@ -0,0 +1,141 @@
import { defaultSpec as defaultDashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2';
import { getCurrentDashboardKindV2 } from './currentDashboardKindV2';
import { applyCurrentDashboardKindV2, applyCurrentDashboardSpecV2 } from './currentDashboardSpecApplyV2';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
config: {
featureToggles: {
kubernetesDashboards: true,
kubernetesDashboardsV2: true,
dashboardNewLayouts: false,
},
},
}));
jest.mock('../pages/DashboardScenePageStateManager', () => ({
getDashboardScenePageStateManager: jest.fn(),
}));
jest.mock('../serialization/transformSaveModelSchemaV2ToScene', () => ({
transformSaveModelSchemaV2ToScene: jest.fn(),
}));
describe('current dashboard spec apply API', () => {
const { getDashboardScenePageStateManager } = jest.requireMock('../pages/DashboardScenePageStateManager');
const { transformSaveModelSchemaV2ToScene } = jest.requireMock('../serialization/transformSaveModelSchemaV2ToScene');
beforeEach(() => {
jest.clearAllMocks();
});
it('applyCurrentDashboardSpecV2 swaps in a new scene immediately and marks it dirty', () => {
const currentSpec = defaultDashboardV2Spec();
const nextSpec = { ...defaultDashboardV2Spec(), title: 'new title' };
const currentScene = {
state: {
uid: 'dash-uid',
meta: {
url: '/d/dash-uid/slug',
slug: 'slug',
canSave: true,
canEdit: true,
canDelete: true,
canShare: true,
canStar: true,
canAdmin: true,
publicDashboardEnabled: false,
k8s: {
name: 'dash-uid',
resourceVersion: '1',
creationTimestamp: 'now',
annotations: {},
labels: {},
},
},
isEditing: true,
},
getSaveModel: () => currentSpec,
};
const nextScene = {
onEnterEditMode: jest.fn(),
setState: jest.fn(),
};
transformSaveModelSchemaV2ToScene.mockReturnValue(nextScene);
const mgr = {
state: { dashboard: currentScene },
setSceneCache: jest.fn(),
setState: jest.fn(),
};
getDashboardScenePageStateManager.mockReturnValue(mgr);
applyCurrentDashboardSpecV2(nextSpec);
expect(transformSaveModelSchemaV2ToScene).toHaveBeenCalledTimes(1);
expect(nextScene.onEnterEditMode).toHaveBeenCalled();
expect(nextScene.setState).toHaveBeenCalledWith({ isDirty: true });
expect(mgr.setSceneCache).toHaveBeenCalledWith('dash-uid', nextScene);
expect(mgr.setState).toHaveBeenCalledWith({ dashboard: nextScene });
});
it('applyCurrentDashboardKindV2 rejects metadata changes and applies only spec when unchanged', () => {
const spec = defaultDashboardV2Spec();
const currentScene = {
state: {
uid: 'dash-uid',
meta: {
url: '/d/dash-uid/slug',
slug: 'slug',
canSave: true,
canEdit: true,
canDelete: true,
canShare: true,
canStar: true,
canAdmin: true,
publicDashboardEnabled: false,
k8s: {
name: 'dash-uid',
resourceVersion: '1',
creationTimestamp: 'now',
annotations: {},
labels: {},
},
},
isEditing: true,
},
getSaveModel: () => spec,
getSaveResource: () => ({
apiVersion: 'dashboard.grafana.app/v2beta1',
kind: 'Dashboard',
metadata: { name: 'dash-uid' },
spec,
}),
};
const nextScene = { onEnterEditMode: jest.fn(), setState: jest.fn() };
transformSaveModelSchemaV2ToScene.mockReturnValue(nextScene);
const mgr = { state: { dashboard: currentScene }, setSceneCache: jest.fn(), setState: jest.fn() };
getDashboardScenePageStateManager.mockReturnValue(mgr);
const current = getCurrentDashboardKindV2();
expect(() =>
applyCurrentDashboardKindV2({
...current,
metadata: {
...current.metadata,
annotations: { ...(current.metadata.annotations ?? {}), 'grafana.app/message': 'changed' },
},
})
).toThrow('Changing metadata is not allowed');
});
});
@@ -0,0 +1,126 @@
import { isEqual } from 'lodash';
import { config } from '@grafana/runtime';
import { Spec as DashboardV2Spec } from '@grafana/schema/dist/esm/schema/dashboard/v2';
import { Status } from '@grafana/schema/src/schema/dashboard/v2';
import { Resource } from 'app/features/apiserver/types';
import { DashboardWithAccessInfo } from 'app/features/dashboard/api/types';
import { isDashboardV2Spec } from 'app/features/dashboard/api/utils';
import { getDashboardScenePageStateManager } from '../pages/DashboardScenePageStateManager';
import { transformSaveModelSchemaV2ToScene } from '../serialization/transformSaveModelSchemaV2ToScene';
import { validateDashboardSchemaV2 } from '../serialization/transformSceneToSaveModelSchemaV2';
import { getCurrentDashboardKindV2 } from './currentDashboardKindV2';
function assertDashboardV2Enabled() {
const isKubernetesDashboardsEnabled = Boolean(config.featureToggles.kubernetesDashboards);
const isV2Enabled = Boolean(config.featureToggles.kubernetesDashboardsV2 || config.featureToggles.dashboardNewLayouts);
if (!isKubernetesDashboardsEnabled || !isV2Enabled) {
throw new Error('V2 dashboard kinds API requires kubernetes dashboards v2 to be enabled');
}
}
function getCurrentSceneOrThrow() {
const mgr = getDashboardScenePageStateManager();
const dashboard = mgr.state.dashboard;
if (!dashboard) {
throw new Error('No dashboard is currently open');
}
return { mgr, dashboard };
}
/**
* Immediately applies a schema-v2 dashboard spec to the currently open `DashboardScene`.
*
* This is a **spec-only** mutation API: it does not allow changing `apiVersion`, `kind`, `metadata`, or `status`.
*/
export function applyCurrentDashboardSpecV2(nextSpec: DashboardV2Spec): void {
assertDashboardV2Enabled();
// Validate (throws on error)
validateDashboardSchemaV2(nextSpec as unknown);
const { mgr, dashboard: currentScene } = getCurrentSceneOrThrow();
// Only operate on v2 scenes
const currentModel = currentScene.getSaveModel();
if (!isDashboardV2Spec(currentModel)) {
throw new Error('Current dashboard is not using schema v2');
}
const k8sMeta = currentScene.state.meta.k8s;
if (!k8sMeta) {
throw new Error('Current dashboard is missing Kubernetes metadata');
}
// Rebuild a new scene from the current immutable wrapper (metadata/access) + new spec.
// This guarantees the UI updates immediately to match the JSON.
const dto: DashboardWithAccessInfo<DashboardV2Spec> = {
apiVersion: 'dashboard.grafana.app/v2beta1',
kind: 'DashboardWithAccessInfo',
metadata: k8sMeta,
spec: nextSpec,
status: {} as Status,
access: {
url: currentScene.state.meta.url,
slug: currentScene.state.meta.slug,
canSave: currentScene.state.meta.canSave,
canEdit: currentScene.state.meta.canEdit,
canDelete: currentScene.state.meta.canDelete,
canShare: currentScene.state.meta.canShare,
canStar: currentScene.state.meta.canStar,
canAdmin: currentScene.state.meta.canAdmin,
annotationsPermissions: currentScene.state.meta.annotationsPermissions,
isPublic: currentScene.state.meta.publicDashboardEnabled,
},
};
const nextScene = transformSaveModelSchemaV2ToScene(dto);
// Keep edit mode semantics consistent with other JSON-apply flows:
// - Enter edit mode if needed
// - Mark dirty because the spec changed
if (currentScene.state.isEditing) {
nextScene.onEnterEditMode();
} else {
nextScene.onEnterEditMode();
}
nextScene.setState({ isDirty: true });
// Keep cache coherent for the currently open dashboard
if (currentScene.state.uid) {
mgr.setSceneCache(currentScene.state.uid, nextScene);
}
mgr.setState({ dashboard: nextScene });
}
/**
* Convenience helper that accepts a full Dashboard kind JSON object, but enforces **spec-only** updates.
*
* It rejects any attempt to change `apiVersion`, `kind`, `metadata`, or `status` from the currently open dashboard.
*/
export function applyCurrentDashboardKindV2(resource: Resource<DashboardV2Spec, Status, 'Dashboard'>): void {
assertDashboardV2Enabled();
const current = getCurrentDashboardKindV2();
if (!isEqual(resource.apiVersion, current.apiVersion)) {
throw new Error('Changing apiVersion is not allowed');
}
if (!isEqual(resource.kind, current.kind)) {
throw new Error('Changing kind is not allowed');
}
if (!isEqual(resource.metadata, current.metadata)) {
throw new Error('Changing metadata is not allowed');
}
if ('status' in resource && !isEqual(resource.status, current.status)) {
throw new Error('Changing status is not allowed');
}
applyCurrentDashboardSpecV2(resource.spec);
}
@@ -0,0 +1,92 @@
import { dashboardSceneJsonApiV2 } from './runtimeDashboardSceneJsonApiV2';
jest.mock('./currentDashboardKindV2', () => ({
getCurrentDashboardKindV2: jest.fn(),
}));
jest.mock('./currentDashboardSpecApplyV2', () => ({
applyCurrentDashboardSpecV2: jest.fn(),
}));
describe('dashboardSceneJsonApiV2 (runtime adapter)', () => {
const { getCurrentDashboardKindV2 } = jest.requireMock('./currentDashboardKindV2');
const { applyCurrentDashboardSpecV2 } = jest.requireMock('./currentDashboardSpecApplyV2');
const baseResource = {
apiVersion: 'dashboard.grafana.app/v2beta1',
kind: 'Dashboard',
metadata: { name: 'dash-uid', namespace: 'default' },
spec: { title: 'x' },
status: {},
};
beforeEach(() => {
jest.clearAllMocks();
window.history.pushState({}, '', '/d/dash-uid/slug');
});
it('getCurrentDashboard returns cached JSON if live serialization fails', () => {
getCurrentDashboardKindV2.mockReturnValue(baseResource);
const first = dashboardSceneJsonApiV2.getCurrentDashboard(0);
expect(JSON.parse(first).metadata.name).toBe('dash-uid');
getCurrentDashboardKindV2.mockImplementation(() => {
throw new Error('Unsupported transformation type');
});
const second = dashboardSceneJsonApiV2.getCurrentDashboard(0);
expect(second).toBe(first);
});
it('applyCurrentDashboard uses cached baseline to enforce immutability if live serialization fails', () => {
// Prime cache
getCurrentDashboardKindV2.mockReturnValue(baseResource);
dashboardSceneJsonApiV2.getCurrentDashboard(0);
// Now break live serialization
getCurrentDashboardKindV2.mockImplementation(() => {
throw new Error('Unsupported transformation type');
});
expect(() =>
dashboardSceneJsonApiV2.applyCurrentDashboard(
JSON.stringify({
...baseResource,
apiVersion: 'dashboard.grafana.app/v2alpha1',
})
)
).toThrow('Changing apiVersion is not allowed');
});
it('applyCurrentDashboard can recover without a baseline by validating against URL UID and applying spec', () => {
getCurrentDashboardKindV2.mockImplementation(() => {
throw new Error('Unsupported transformation type');
});
const nextSpec = { title: 'recovered' };
dashboardSceneJsonApiV2.applyCurrentDashboard(
JSON.stringify({
...baseResource,
spec: nextSpec,
})
);
expect(applyCurrentDashboardSpecV2).toHaveBeenCalledWith(nextSpec);
});
it('applyCurrentDashboard rejects recovery attempts targeting a different dashboard UID', () => {
getCurrentDashboardKindV2.mockImplementation(() => {
throw new Error('Unsupported transformation type');
});
expect(() =>
dashboardSceneJsonApiV2.applyCurrentDashboard(
JSON.stringify({
...baseResource,
metadata: { ...baseResource.metadata, name: 'other-uid' },
})
)
).toThrow('Changing metadata is not allowed');
});
});
@@ -0,0 +1,122 @@
import { isEqual } from 'lodash';
import type { DashboardSceneJsonApiV2 } from '@grafana/runtime';
import { getCurrentDashboardKindV2 as getCurrentDashboardResourceV2 } from './currentDashboardKindV2';
import { applyCurrentDashboardSpecV2 } from './currentDashboardSpecApplyV2';
type DashboardResourceV2 = ReturnType<typeof getCurrentDashboardResourceV2>;
/**
* The dashboard JSON API is required to be resilient for automation.
*
* In practice, the currently loaded `DashboardScene` might temporarily be in a state that cannot be
* serialized back to a v2 resource (for example, if the scene contains unsupported transformation types).
*
* To avoid “bricking” the API in that situation (where both `getCurrentDashboard()` and
* `applyCurrentDashboard()` would fail because they need to read the current resource),
* we keep a last-known-good dashboard resource cached as a recovery fallback.
*/
let lastKnownGoodResource: DashboardResourceV2 | undefined;
function getDashboardUidFromUrl(): string | undefined {
const pathname = globalThis.location?.pathname ?? '';
// Expected: /d/<uid>/<slug>
const match = pathname.match(/\/d\/([^/]+)/);
return match?.[1];
}
function getCurrentDashboardResourceWithFallback(): { resource: DashboardResourceV2; source: 'live' | 'cache' } {
try {
const resource = getCurrentDashboardResourceV2();
lastKnownGoodResource = resource;
return { resource, source: 'live' };
} catch (err) {
if (lastKnownGoodResource) {
return { resource: lastKnownGoodResource, source: 'cache' };
}
const details = err instanceof Error ? err.message : String(err);
throw new Error(
'DashboardScene JSON API could not read the current dashboard resource. ' +
'This can happen if the loaded DashboardScene cannot be serialized to schema v2. ' +
'To recover, call applyCurrentDashboard() with a valid v2beta1 Dashboard JSON (spec-only changes) ' +
'whose metadata.name matches the dashboard UID in the URL.\n\n' +
`Underlying error: ${details}`
);
}
}
export const dashboardSceneJsonApiV2: DashboardSceneJsonApiV2 = {
getCurrentDashboard: (space = 2) => {
const { resource } = getCurrentDashboardResourceWithFallback();
return JSON.stringify(resource, null, space);
},
applyCurrentDashboard: (resourceJson: string) => {
const resource = JSON.parse(resourceJson);
let current: DashboardResourceV2 | undefined;
try {
// Prefer live for strict immutability checks, but fall back to cached baseline.
current = getCurrentDashboardResourceV2();
lastKnownGoodResource = current;
} catch {
current = lastKnownGoodResource;
}
// If we can’t read the current resource at all (no cache), we still allow recovery by validating
// that the caller targets the currently open dashboard, and that the payload is a v2beta1 Dashboard.
if (!current) {
const uidFromUrl = getDashboardUidFromUrl();
if (resource.apiVersion !== 'dashboard.grafana.app/v2beta1') {
throw new Error('Changing apiVersion is not allowed');
}
if (resource.kind !== 'Dashboard') {
throw new Error('Changing kind is not allowed');
}
if (!resource.metadata || typeof resource.metadata !== 'object') {
throw new Error('Changing metadata is not allowed');
}
if (uidFromUrl && resource.metadata.name !== uidFromUrl) {
throw new Error('Changing metadata is not allowed');
}
if (!('status' in resource)) {
// Keep error message consistent; callers should include status even if empty.
throw new Error('Changing status is not allowed');
}
applyCurrentDashboardSpecV2(resource.spec);
// Best-effort refresh cache after recovery.
try {
lastKnownGoodResource = getCurrentDashboardResourceV2();
} catch {
// ignore
}
return;
}
if (!isEqual(resource.apiVersion, current.apiVersion)) {
throw new Error('Changing apiVersion is not allowed');
}
if (!isEqual(resource.kind, current.kind)) {
throw new Error('Changing kind is not allowed');
}
if (!isEqual(resource.metadata, current.metadata)) {
throw new Error('Changing metadata is not allowed');
}
if (!isEqual(resource.status, current.status)) {
throw new Error('Changing status is not allowed');
}
applyCurrentDashboardSpecV2(resource.spec);
// Best-effort cache refresh after apply.
try {
lastKnownGoodResource = getCurrentDashboardResourceV2();
} catch {
// ignore
}
},
};
+18
View File
@@ -1,4 +1,8 @@
import { PanelData, RawTimeRange } from '@grafana/data';
import {
applyCurrentDashboard,
getCurrentDashboard,
} from '@grafana/runtime';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
@@ -11,6 +15,14 @@ declare global {
getDashboardTimeRange: () => { from: number; to: number; raw: RawTimeRange };
getPanelData: () => Record<number, PanelData | undefined> | undefined;
};
/**
* Exposes the current-dashboard schema v2 JSON API for debugging / automation.
* Intended for browser console usage.
*/
dashboardApi?: {
getCurrentDashboard: (space?: number) => string;
applyCurrentDashboard: (resourceJson: string) => void;
};
}
}
@@ -54,4 +66,10 @@ export function initWindowRuntime() {
}, {});
},
};
// Expose the same API that plugins use via @grafana/runtime, but on `window` for easy console access.
window.dashboardApi = {
getCurrentDashboard,
applyCurrentDashboard,
};
}