DashboardScene: Make Grafana usable when custom home dashboard is invalid (#89305)

* DashboardScene: Make Grafana usable when custom home dashboard is invalid

* Tests

* Remove console.error
This commit is contained in:
Dominik Prokop 2024-06-18 16:08:16 +02:00 committed by GitHub
parent 50dd95c09b
commit ae04580e5f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 87 additions and 17 deletions

View File

@ -19,7 +19,9 @@ export interface Props extends GrafanaRouteComponentProps<DashboardPageRoutePara
export function DashboardScenePage({ match, route, queryParams, history }: Props) {
const stateManager = getDashboardScenePageStateManager();
const { dashboard, isLoading, loadError } = stateManager.useState();
// After scene migration is complete and we get rid of old dashboard we should refactor dashboardWatcher so this route reload is not need
const routeReloadCounter = (history.location.state as any)?.routeReloadCounter;

View File

@ -35,21 +35,9 @@ describe('DashboardScenePageStateManager', () => {
const loader = new DashboardScenePageStateManager({});
await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal });
expect(loader.state.dashboard).toBeUndefined();
expect(loader.state.dashboard).toBeDefined();
expect(loader.state.isLoading).toBe(false);
expect(loader.state.loadError).toBe('Error: Dashboard not found');
});
it('should handle home dashboard redirect', async () => {
setBackendSrv({
get: () => Promise.resolve({ redirectUri: '/d/asd' }),
} as unknown as BackendSrv);
const loader = new DashboardScenePageStateManager({});
await loader.loadDashboard({ uid: '', route: DashboardRoutes.Home });
expect(loader.state.dashboard).toBeUndefined();
expect(loader.state.loadError).toBeUndefined();
expect(loader.state.loadError).toBe('Dashboard not found');
});
it('shoud fetch dashboard from local storage and remove it after if it exists', async () => {
@ -94,6 +82,37 @@ describe('DashboardScenePageStateManager', () => {
expect(loader.state.isLoading).toBe(false);
});
describe('Home dashboard', () => {
it('should handle home dashboard redirect', async () => {
setBackendSrv({
get: () => Promise.resolve({ redirectUri: '/d/asd' }),
} as unknown as BackendSrv);
const loader = new DashboardScenePageStateManager({});
await loader.loadDashboard({ uid: '', route: DashboardRoutes.Home });
expect(loader.state.dashboard).toBeUndefined();
expect(loader.state.loadError).toBeUndefined();
});
it('should handle invalid home dashboard request', async () => {
setBackendSrv({
get: () =>
Promise.reject({
status: 500,
data: { message: 'Failed to load home dashboard' },
}),
} as unknown as BackendSrv);
const loader = new DashboardScenePageStateManager({});
await loader.loadDashboard({ uid: '', route: DashboardRoutes.Home });
expect(loader.state.dashboard).toBeDefined();
expect(loader.state.dashboard?.state.title).toEqual('Failed to load home dashboard');
expect(loader.state.loadError).toEqual('Failed to load home dashboard');
});
});
describe('New dashboards', () => {
it('Should have new empty model with meta.isNew and should not be cached', async () => {
const loader = new DashboardScenePageStateManager({});

View File

@ -1,10 +1,13 @@
import { locationUtil } from '@grafana/data';
import { config, getBackendSrv, isFetchError, locationService } from '@grafana/runtime';
import { defaultDashboard } from '@grafana/schema';
import { StateManagerBase } from 'app/core/services/StateManagerBase';
import { default as localStorageStore } from 'app/core/store';
import { getMessageFromError } from 'app/core/utils/errors';
import { startMeasure, stopMeasure } from 'app/core/utils/metrics';
import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { DashboardModel } from 'app/features/dashboard/state';
import { emitDashboardViewEvent } from 'app/features/dashboard/state/analyticsProcessor';
import {
DASHBOARD_FROM_LS_KEY,
@ -16,7 +19,10 @@ import { DashboardDTO, DashboardRoutes } from 'app/types';
import { PanelEditor } from '../panel-edit/PanelEditor';
import { DashboardScene } from '../scene/DashboardScene';
import { buildNewDashboardSaveModel } from '../serialization/buildNewDashboardSaveModel';
import { transformSaveModelToScene } from '../serialization/transformSaveModelToScene';
import {
createDashboardSceneFromDashboardModel,
transformSaveModelToScene,
} from '../serialization/transformSaveModelToScene';
import { restoreDashboardStateFromLocalStorage } from '../utils/dashboardSessionState';
import { updateNavModel } from './utils';
@ -143,7 +149,6 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
return null;
}
console.error(e);
throw e;
}
@ -196,7 +201,12 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
});
}
} catch (err) {
this.setState({ isLoading: false, loadError: String(err) });
const msg = getMessageFromError(err);
this.setState({
isLoading: false,
loadError: msg,
dashboard: getErrorScene(msg),
});
}
}
@ -282,3 +292,42 @@ export function getDashboardScenePageStateManager(): DashboardScenePageStateMana
return stateManager;
}
function getErrorScene(msg: string) {
return createDashboardSceneFromDashboardModel(
new DashboardModel(
{
...defaultDashboard,
title: msg,
panels: [
{
fieldConfig: {
defaults: {},
overrides: [],
},
gridPos: {
h: 6,
w: 12,
x: 7,
y: 0,
},
id: 1,
options: {
code: {
language: 'plaintext',
showLineNumbers: false,
showMiniMap: false,
},
content: `<br/><br/><center><h1>${msg}</h1></center>`,
mode: 'html',
},
title: '',
transparent: true,
type: 'text',
},
],
},
{ canSave: false, canEdit: false }
)
);
}