get dashboard errors
This commit is contained in:
@@ -4,6 +4,14 @@ export interface DashboardSceneJsonApiV2 {
|
||||
*/
|
||||
getCurrentDashboard(space?: number): string;
|
||||
|
||||
/**
|
||||
* Read query errors for the currently open dashboard (JSON string).
|
||||
*
|
||||
* This returns a JSON array of objects shaped like:
|
||||
* `{ panelId, panelTitle, refId?, datasource?, message, severity }`.
|
||||
*/
|
||||
getCurrentDashboardErrors(space?: number): string;
|
||||
|
||||
/**
|
||||
* Apply a v2beta1 Dashboard kind JSON (JSON string).
|
||||
*
|
||||
@@ -45,6 +53,15 @@ export function getCurrentDashboard(space = 2): string {
|
||||
return getDashboardSceneJsonApiV2().getCurrentDashboard(space);
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin-friendly helper to read aggregated query errors for the current dashboard (JSON string).
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function getCurrentDashboardErrors(space = 2): string {
|
||||
return getDashboardSceneJsonApiV2().getCurrentDashboardErrors(space);
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON-string helper to apply a v2 Dashboard kind JSON (spec-only enforcement happens in the implementation).
|
||||
*
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import { getCurrentDashboardErrors } from './currentDashboardErrors';
|
||||
|
||||
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('../utils/dashboardSceneGraph', () => ({
|
||||
dashboardSceneGraph: {
|
||||
getVizPanels: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../utils/utils', () => ({
|
||||
getPanelIdForVizPanel: jest.fn(),
|
||||
getQueryRunnerFor: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('getCurrentDashboardErrors', () => {
|
||||
const { getDashboardScenePageStateManager } = jest.requireMock('../pages/DashboardScenePageStateManager');
|
||||
const { dashboardSceneGraph } = jest.requireMock('../utils/dashboardSceneGraph');
|
||||
const { getPanelIdForVizPanel, getQueryRunnerFor } = jest.requireMock('../utils/utils');
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('returns an empty list when there are no query errors', () => {
|
||||
getDashboardScenePageStateManager.mockReturnValue({ state: { dashboard: {} } });
|
||||
dashboardSceneGraph.getVizPanels.mockReturnValue([{ state: { title: 'P1' } }]);
|
||||
getPanelIdForVizPanel.mockReturnValue(1);
|
||||
getQueryRunnerFor.mockReturnValue({ state: { data: { errors: [] } } });
|
||||
|
||||
expect(getCurrentDashboardErrors()).toEqual([]);
|
||||
});
|
||||
|
||||
it('returns per-panel error summaries with refId and datasource when available', () => {
|
||||
const panel = { state: { title: 'My panel' } };
|
||||
getDashboardScenePageStateManager.mockReturnValue({ state: { dashboard: {} } });
|
||||
dashboardSceneGraph.getVizPanels.mockReturnValue([panel]);
|
||||
getPanelIdForVizPanel.mockReturnValue(42);
|
||||
getQueryRunnerFor.mockReturnValue({
|
||||
state: {
|
||||
datasource: { uid: 'fallback-ds' },
|
||||
queries: [{ refId: 'A', datasource: { name: 'gdev-mysql' } }],
|
||||
data: { errors: [{ message: 'boom', refId: 'A' }] },
|
||||
},
|
||||
});
|
||||
|
||||
expect(getCurrentDashboardErrors()).toEqual([
|
||||
{
|
||||
panelId: 42,
|
||||
panelTitle: 'My panel',
|
||||
refId: 'A',
|
||||
datasource: 'gdev-mysql',
|
||||
message: 'boom',
|
||||
severity: 'error',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
import { DataQueryError, DataQueryErrorType } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import type { SceneDataQuery, SceneQueryRunner, VizPanel } from '@grafana/scenes';
|
||||
|
||||
import { dashboardSceneGraph } from '../utils/dashboardSceneGraph';
|
||||
import { getPanelIdForVizPanel, getQueryRunnerFor } from '../utils/utils';
|
||||
import { getDashboardScenePageStateManager } from '../pages/DashboardScenePageStateManager';
|
||||
|
||||
export type DashboardPanelErrorSeverity = 'error' | 'warning';
|
||||
|
||||
export interface DashboardPanelErrorSummary {
|
||||
panelId: number;
|
||||
panelTitle: string;
|
||||
refId?: string;
|
||||
datasource?: string;
|
||||
message: string;
|
||||
severity: DashboardPanelErrorSeverity;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
function toMessage(err: DataQueryError): string {
|
||||
return err.message || err.data?.message || err.data?.error || 'Query error';
|
||||
}
|
||||
|
||||
function toSeverity(err: DataQueryError): DashboardPanelErrorSeverity {
|
||||
// Treat cancellations as warnings; everything else is an error.
|
||||
if (err.type === DataQueryErrorType.Cancelled) {
|
||||
return 'warning';
|
||||
}
|
||||
return 'error';
|
||||
}
|
||||
|
||||
function formatDatasourceRef(ds: unknown): string | undefined {
|
||||
if (!ds || typeof ds !== 'object') {
|
||||
return undefined;
|
||||
}
|
||||
const obj = ds as Record<string, unknown>;
|
||||
const uid = obj.uid;
|
||||
const name = obj.name;
|
||||
const type = obj.type;
|
||||
if (typeof uid === 'string' && uid.length) {
|
||||
return uid;
|
||||
}
|
||||
if (typeof name === 'string' && name.length) {
|
||||
return name;
|
||||
}
|
||||
if (typeof type === 'string' && type.length) {
|
||||
return type;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getDatasourceForError(queryRunner: SceneQueryRunner, refId?: string): string | undefined {
|
||||
const queries = (queryRunner.state.queries ?? []) as SceneDataQuery[];
|
||||
const q = refId ? queries.find((qq) => qq.refId === refId) : undefined;
|
||||
const ds = q?.datasource ?? queryRunner.state.datasource;
|
||||
return formatDatasourceRef(ds);
|
||||
}
|
||||
|
||||
export function getCurrentDashboardErrors(): DashboardPanelErrorSummary[] {
|
||||
assertDashboardV2Enabled();
|
||||
|
||||
const dashboard = getCurrentDashboardScene();
|
||||
const panels = dashboardSceneGraph.getVizPanels(dashboard);
|
||||
|
||||
const out: DashboardPanelErrorSummary[] = [];
|
||||
|
||||
for (const panel of panels) {
|
||||
const queryRunner = getQueryRunnerFor(panel);
|
||||
const errors = queryRunner?.state.data?.errors ?? [];
|
||||
if (!queryRunner || errors.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const panelId = getPanelIdForVizPanel(panel);
|
||||
const panelTitle = (panel as VizPanel).state.title ?? '';
|
||||
|
||||
for (const err of errors) {
|
||||
const refId = err.refId;
|
||||
out.push({
|
||||
panelId,
|
||||
panelTitle,
|
||||
refId,
|
||||
datasource: getDatasourceForError(queryRunner, refId),
|
||||
message: toMessage(err),
|
||||
severity: toSeverity(err),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Stable ordering for LLM consumption.
|
||||
out.sort((a, b) => {
|
||||
if (a.panelId !== b.panelId) {
|
||||
return a.panelId - b.panelId;
|
||||
}
|
||||
return (a.refId ?? '').localeCompare(b.refId ?? '');
|
||||
});
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import { isEqual } from 'lodash';
|
||||
|
||||
import type { DashboardSceneJsonApiV2 } from '@grafana/runtime';
|
||||
|
||||
import { getCurrentDashboardErrors } from './currentDashboardErrors';
|
||||
import { getCurrentDashboardKindV2 as getCurrentDashboardResourceV2 } from './currentDashboardKindV2';
|
||||
import { applyCurrentDashboardSpecV2 } from './currentDashboardSpecApplyV2';
|
||||
|
||||
@@ -53,6 +54,10 @@ export const dashboardSceneJsonApiV2: DashboardSceneJsonApiV2 = {
|
||||
return JSON.stringify(resource, null, space);
|
||||
},
|
||||
|
||||
getCurrentDashboardErrors: (space = 2) => {
|
||||
return JSON.stringify(getCurrentDashboardErrors(), null, space);
|
||||
},
|
||||
|
||||
applyCurrentDashboard: (resourceJson: string) => {
|
||||
const resource = JSON.parse(resourceJson);
|
||||
let current: DashboardResourceV2 | undefined;
|
||||
|
||||
@@ -2,6 +2,7 @@ import { PanelData, RawTimeRange } from '@grafana/data';
|
||||
import {
|
||||
applyCurrentDashboard,
|
||||
getCurrentDashboard,
|
||||
getCurrentDashboardErrors,
|
||||
} from '@grafana/runtime';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
@@ -21,6 +22,7 @@ declare global {
|
||||
*/
|
||||
dashboardApi?: {
|
||||
getCurrentDashboard: (space?: number) => string;
|
||||
getCurrentDashboardErrors: (space?: number) => string;
|
||||
applyCurrentDashboard: (resourceJson: string) => void;
|
||||
};
|
||||
}
|
||||
@@ -70,6 +72,7 @@ export function initWindowRuntime() {
|
||||
// Expose the same API that plugins use via @grafana/runtime, but on `window` for easy console access.
|
||||
window.dashboardApi = {
|
||||
getCurrentDashboard,
|
||||
getCurrentDashboardErrors,
|
||||
applyCurrentDashboard,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user