mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
DashboardScene: Initial work to support "new" dashboard route and creation logic (#81549)
* DashboardScene: Initial work to get new route to work * Update * remove caching of new dashboard * remove old new dashboard func * Update * Update public/app/features/dashboard-scene/scene/DashboardScene.tsx Co-authored-by: Ivan Ortega Alba <ivanortegaalba@gmail.com> * Fixing test * dam messy tests --------- Co-authored-by: Ivan Ortega Alba <ivanortegaalba@gmail.com>
This commit is contained in:
parent
39057552dc
commit
3b2352f066
@ -2379,6 +2379,9 @@ exports[`better eslint`] = {
|
||||
"public/app/features/dashboard-scene/inspect/InspectJsonTab.tsx:5381": [
|
||||
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/pages/DashboardScenePage.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/dashboard-scene/panel-edit/PanelDataPane/PanelDataPane.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
@ -2753,6 +2756,9 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "4"]
|
||||
],
|
||||
"public/app/features/dashboard/containers/DashboardPageProxy.tsx:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/dashboard/dashgrid/PanelStateWrapper.tsx:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
@ -2909,9 +2915,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"]
|
||||
],
|
||||
"public/app/features/dashboard/state/initDashboard.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
"public/app/features/dashboard/utils/getPanelMenu.test.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||
],
|
||||
|
@ -5,6 +5,7 @@ import { GrafanaTheme2, urlUtil } from '@grafana/data';
|
||||
import { EmbeddedDashboardProps } from '@grafana/runtime';
|
||||
import { SceneObjectStateChangedEvent, sceneUtils } from '@grafana/scenes';
|
||||
import { Spinner, Alert, useStyles2 } from '@grafana/ui';
|
||||
import { DashboardRoutes } from 'app/types';
|
||||
|
||||
import { getDashboardScenePageStateManager } from '../pages/DashboardScenePageStateManager';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
@ -14,7 +15,7 @@ export function EmbeddedDashboard(props: EmbeddedDashboardProps) {
|
||||
const { dashboard, loadError } = stateManager.useState();
|
||||
|
||||
useEffect(() => {
|
||||
stateManager.loadDashboard({ uid: props.uid!, isEmbedded: true });
|
||||
stateManager.loadDashboard({ uid: props.uid!, route: DashboardRoutes.Embedded });
|
||||
return () => {
|
||||
stateManager.clearState();
|
||||
};
|
||||
|
@ -5,28 +5,28 @@ 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 { DashboardPageRouteParams } from 'app/features/dashboard/containers/types';
|
||||
import { DashboardPageRouteParams, DashboardPageRouteSearchParams } from 'app/features/dashboard/containers/types';
|
||||
import { DashboardRoutes } from 'app/types';
|
||||
|
||||
import { getDashboardScenePageStateManager } from './DashboardScenePageStateManager';
|
||||
|
||||
export interface Props extends GrafanaRouteComponentProps<DashboardPageRouteParams> {}
|
||||
export interface Props extends GrafanaRouteComponentProps<DashboardPageRouteParams, DashboardPageRouteSearchParams> {}
|
||||
|
||||
export function DashboardScenePage({ match, route }: Props) {
|
||||
export function DashboardScenePage({ match, route, queryParams }: Props) {
|
||||
const stateManager = getDashboardScenePageStateManager();
|
||||
const { dashboard, isLoading, loadError } = stateManager.useState();
|
||||
|
||||
useEffect(() => {
|
||||
if (route.routeName === DashboardRoutes.Home) {
|
||||
stateManager.loadDashboard({ uid: route.routeName });
|
||||
} else {
|
||||
stateManager.loadDashboard({ uid: match.params.uid! });
|
||||
}
|
||||
stateManager.loadDashboard({
|
||||
uid: match.params.uid ?? '',
|
||||
route: route.routeName as DashboardRoutes,
|
||||
urlFolderUid: queryParams.folderUid,
|
||||
});
|
||||
|
||||
return () => {
|
||||
stateManager.clearState();
|
||||
};
|
||||
}, [stateManager, match.params.uid, route.routeName]);
|
||||
}, [stateManager, match.params.uid, route.routeName, queryParams.folderUid]);
|
||||
|
||||
if (!dashboard) {
|
||||
return (
|
||||
|
@ -2,6 +2,7 @@ import { advanceBy } from 'jest-date-mock';
|
||||
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { getUrlSyncManager } from '@grafana/scenes';
|
||||
import { DashboardRoutes } from 'app/types';
|
||||
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
import { setupLoadDashboardMock } from '../utils/test-utils';
|
||||
@ -14,12 +15,12 @@ describe('DashboardScenePageStateManager', () => {
|
||||
const loadDashboardMock = setupLoadDashboardMock({ dashboard: { uid: 'fake-dash', editable: true }, meta: {} });
|
||||
|
||||
const loader = new DashboardScenePageStateManager({});
|
||||
await loader.loadDashboard({ uid: 'fake-dash' });
|
||||
await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal });
|
||||
|
||||
expect(loadDashboardMock).toHaveBeenCalledWith('db', '', 'fake-dash');
|
||||
|
||||
// should use cache second time
|
||||
await loader.loadDashboard({ uid: 'fake-dash' });
|
||||
await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal });
|
||||
expect(loadDashboardMock.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
@ -27,7 +28,7 @@ describe('DashboardScenePageStateManager', () => {
|
||||
setupLoadDashboardMock({ dashboard: undefined, meta: {} });
|
||||
|
||||
const loader = new DashboardScenePageStateManager({});
|
||||
await loader.loadDashboard({ uid: 'fake-dash' });
|
||||
await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal });
|
||||
|
||||
expect(loader.state.dashboard).toBeUndefined();
|
||||
expect(loader.state.isLoading).toBe(false);
|
||||
@ -38,7 +39,7 @@ describe('DashboardScenePageStateManager', () => {
|
||||
setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} });
|
||||
|
||||
const loader = new DashboardScenePageStateManager({});
|
||||
await loader.loadDashboard({ uid: 'fake-dash' });
|
||||
await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal });
|
||||
|
||||
expect(loader.state.dashboard?.state.uid).toBe('fake-dash');
|
||||
expect(loader.state.loadError).toBe(undefined);
|
||||
@ -49,7 +50,7 @@ describe('DashboardScenePageStateManager', () => {
|
||||
setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} });
|
||||
|
||||
const loader = new DashboardScenePageStateManager({});
|
||||
await loader.loadDashboard({ uid: 'fake-dash' });
|
||||
await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal });
|
||||
|
||||
expect(loader.state.dashboard).toBeInstanceOf(DashboardScene);
|
||||
expect(loader.state.isLoading).toBe(false);
|
||||
@ -61,7 +62,7 @@ describe('DashboardScenePageStateManager', () => {
|
||||
locationService.partial({ from: 'now-5m', to: 'now' });
|
||||
|
||||
const loader = new DashboardScenePageStateManager({});
|
||||
await loader.loadDashboard({ uid: 'fake-dash' });
|
||||
await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal });
|
||||
const dash = loader.state.dashboard;
|
||||
|
||||
expect(dash!.state.$timeRange?.state.from).toEqual('now-5m');
|
||||
@ -71,7 +72,7 @@ describe('DashboardScenePageStateManager', () => {
|
||||
// try loading again (and hitting cache)
|
||||
locationService.partial({ from: 'now-10m', to: 'now' });
|
||||
|
||||
await loader.loadDashboard({ uid: 'fake-dash' });
|
||||
await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal });
|
||||
const dash2 = loader.state.dashboard;
|
||||
|
||||
expect(dash2!.state.$timeRange?.state.from).toEqual('now-10m');
|
||||
@ -83,12 +84,32 @@ describe('DashboardScenePageStateManager', () => {
|
||||
locationService.partial({ from: 'now-5m', to: 'now' });
|
||||
|
||||
const loader = new DashboardScenePageStateManager({});
|
||||
await loader.loadDashboard({ uid: 'fake-dash', isEmbedded: true });
|
||||
await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Embedded });
|
||||
const dash = loader.state.dashboard;
|
||||
|
||||
expect(dash!.state.$timeRange?.state.from).toEqual('now-6h');
|
||||
});
|
||||
|
||||
describe('New dashboards', () => {
|
||||
it('Should have new empty model with meta.isNew and should not be cached', async () => {
|
||||
const loader = new DashboardScenePageStateManager({});
|
||||
|
||||
await loader.loadDashboard({ uid: '', route: DashboardRoutes.New });
|
||||
const dashboard = loader.state.dashboard!;
|
||||
|
||||
expect(dashboard.state.meta.isNew).toBe(true);
|
||||
expect(dashboard.state.isEditing).toBe(true);
|
||||
expect(dashboard.state.isDirty).toBe(true);
|
||||
|
||||
dashboard.setState({ title: 'Changed' });
|
||||
|
||||
await loader.loadDashboard({ uid: '', route: DashboardRoutes.New });
|
||||
const dashboard2 = loader.state.dashboard!;
|
||||
|
||||
expect(dashboard2.state.title).toBe('New dashboard');
|
||||
});
|
||||
});
|
||||
|
||||
describe('caching', () => {
|
||||
it('should cache the dashboard DTO', async () => {
|
||||
setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} });
|
||||
@ -97,7 +118,7 @@ describe('DashboardScenePageStateManager', () => {
|
||||
|
||||
expect(loader.getFromCache('fake-dash')).toBeNull();
|
||||
|
||||
await loader.loadDashboard({ uid: 'fake-dash' });
|
||||
await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal });
|
||||
|
||||
expect(loader.getFromCache('fake-dash')).toBeDefined();
|
||||
});
|
||||
@ -110,15 +131,15 @@ describe('DashboardScenePageStateManager', () => {
|
||||
|
||||
expect(loader.getFromCache('fake-dash')).toBeNull();
|
||||
|
||||
await loader.fetchDashboard({ uid: 'fake-dash' });
|
||||
await loader.fetchDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal });
|
||||
expect(loadDashSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
advanceBy(DASHBOARD_CACHE_TTL / 2);
|
||||
await loader.fetchDashboard({ uid: 'fake-dash' });
|
||||
await loader.fetchDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal });
|
||||
expect(loadDashSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
advanceBy(DASHBOARD_CACHE_TTL / 2 + 1);
|
||||
await loader.fetchDashboard({ uid: 'fake-dash' });
|
||||
await loader.fetchDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal });
|
||||
expect(loadDashSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
@ -11,6 +11,7 @@ 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';
|
||||
|
||||
export interface DashboardScenePageState {
|
||||
@ -22,6 +23,9 @@ export interface DashboardScenePageState {
|
||||
|
||||
export const DASHBOARD_CACHE_TTL = 2000;
|
||||
|
||||
/** Only used by cache in loading home in DashboardPageProxy and initDashboard (Old arch), can remove this after old dashboard arch is gone */
|
||||
export const HOME_DASHBOARD_CACHE_KEY = '__grafana_home_uid__';
|
||||
|
||||
interface DashboardCacheEntry {
|
||||
dashboard: DashboardDTO;
|
||||
ts: number;
|
||||
@ -29,7 +33,8 @@ interface DashboardCacheEntry {
|
||||
|
||||
export interface LoadDashboardOptions {
|
||||
uid: string;
|
||||
isEmbedded?: boolean;
|
||||
route: DashboardRoutes;
|
||||
urlFolderUid?: string;
|
||||
}
|
||||
|
||||
export class DashboardScenePageStateManager extends StateManagerBase<DashboardScenePageState> {
|
||||
@ -39,8 +44,9 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
|
||||
|
||||
// To eventualy replace the fetchDashboard function from Dashboard redux state management.
|
||||
// For now it's a simplistic version to support Home and Normal dashboard routes.
|
||||
public async fetchDashboard({ uid, isEmbedded }: LoadDashboardOptions) {
|
||||
const cachedDashboard = this.getFromCache(uid);
|
||||
public async fetchDashboard({ uid, route, urlFolderUid }: LoadDashboardOptions) {
|
||||
const cacheKey = route === DashboardRoutes.Home ? HOME_DASHBOARD_CACHE_KEY : uid;
|
||||
const cachedDashboard = this.getFromCache(cacheKey);
|
||||
|
||||
if (cachedDashboard) {
|
||||
return cachedDashboard;
|
||||
@ -49,27 +55,37 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
|
||||
let rsp: DashboardDTO | undefined;
|
||||
|
||||
try {
|
||||
if (uid === DashboardRoutes.Home) {
|
||||
rsp = await getBackendSrv().get('/api/dashboards/home');
|
||||
switch (route) {
|
||||
case DashboardRoutes.New:
|
||||
rsp = buildNewDashboardSaveModel(urlFolderUid);
|
||||
break;
|
||||
case DashboardRoutes.Home:
|
||||
rsp = await getBackendSrv().get('/api/dashboards/home');
|
||||
|
||||
// If user specified a custom home dashboard redirect to that
|
||||
if (rsp?.redirectUri) {
|
||||
const newUrl = locationUtil.stripBaseFromUrl(rsp.redirectUri);
|
||||
locationService.replace(newUrl);
|
||||
return null;
|
||||
}
|
||||
// If user specified a custom home dashboard redirect to that
|
||||
if (rsp?.redirectUri) {
|
||||
const newUrl = locationUtil.stripBaseFromUrl(rsp.redirectUri);
|
||||
locationService.replace(newUrl);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (rsp?.meta) {
|
||||
rsp.meta.canSave = false;
|
||||
rsp.meta.canShare = false;
|
||||
rsp.meta.canStar = false;
|
||||
}
|
||||
} else {
|
||||
rsp = await dashboardLoaderSrv.loadDashboard('db', '', uid);
|
||||
if (rsp?.meta) {
|
||||
rsp.meta.canSave = false;
|
||||
rsp.meta.canShare = false;
|
||||
rsp.meta.canStar = false;
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
rsp = await dashboardLoaderSrv.loadDashboard('db', '', uid);
|
||||
|
||||
if (route === DashboardRoutes.Embedded) {
|
||||
rsp.meta.isEmbedded = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (rsp) {
|
||||
if (rsp.meta.url && !isEmbedded) {
|
||||
if (rsp.meta.url && route !== DashboardRoutes.Embedded) {
|
||||
const dashboardUrl = locationUtil.stripBaseFromUrl(rsp.meta.url);
|
||||
const currentPath = locationService.getLocation().pathname;
|
||||
if (dashboardUrl !== currentPath) {
|
||||
@ -85,7 +101,12 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
|
||||
// Populate nav model in global store according to the folder
|
||||
await this.initNavModel(rsp);
|
||||
|
||||
this.dashboardCache.set(uid, { dashboard: rsp, ts: Date.now() });
|
||||
// Do not cache new dashboards
|
||||
if (uid) {
|
||||
this.dashboardCache.set(uid, { dashboard: rsp, ts: Date.now() });
|
||||
} else if (route === DashboardRoutes.Home) {
|
||||
this.dashboardCache.set(HOME_DASHBOARD_CACHE_KEY, { dashboard: rsp, ts: Date.now() });
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore cancelled errors
|
||||
@ -103,10 +124,7 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
|
||||
public async loadDashboard(options: LoadDashboardOptions) {
|
||||
try {
|
||||
const dashboard = await this.loadScene(options);
|
||||
|
||||
if (!options.isEmbedded) {
|
||||
dashboard.startUrlSync();
|
||||
}
|
||||
dashboard.startUrlSync();
|
||||
|
||||
this.setState({ dashboard: dashboard, isLoading: false });
|
||||
} catch (err) {
|
||||
@ -117,8 +135,6 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
|
||||
private async loadScene(options: LoadDashboardOptions): Promise<DashboardScene> {
|
||||
const fromCache = this.cache[options.uid];
|
||||
if (fromCache) {
|
||||
// Need to update this in case we cached an embedded but now opening it standard mode
|
||||
fromCache.state.meta.isEmbedded = options.isEmbedded;
|
||||
return fromCache;
|
||||
}
|
||||
|
||||
@ -127,13 +143,12 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
|
||||
const rsp = await this.fetchDashboard(options);
|
||||
|
||||
if (rsp?.dashboard) {
|
||||
if (options.isEmbedded) {
|
||||
rsp.meta.isEmbedded = true;
|
||||
}
|
||||
|
||||
const scene = transformSaveModelToScene(rsp);
|
||||
|
||||
this.cache[options.uid] = scene;
|
||||
if (options.uid) {
|
||||
this.cache[options.uid] = scene;
|
||||
}
|
||||
|
||||
return scene;
|
||||
}
|
||||
|
||||
|
@ -153,7 +153,9 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
}
|
||||
|
||||
public startUrlSync() {
|
||||
getUrlSyncManager().initSync(this);
|
||||
if (!this.state.meta.isEmbedded) {
|
||||
getUrlSyncManager().initSync(this);
|
||||
}
|
||||
}
|
||||
|
||||
public stopUrlSync() {
|
||||
@ -206,6 +208,11 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
}
|
||||
|
||||
public onDiscard = () => {
|
||||
if (!this.canDiscard()) {
|
||||
console.error('Trying to discard back to a state that does not exist, initialState undefined');
|
||||
return;
|
||||
}
|
||||
|
||||
// No need to listen to changes anymore
|
||||
this.stopTrackingChanges();
|
||||
// Stop url sync before updating url
|
||||
@ -233,6 +240,10 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
this.propagateEditModeChange();
|
||||
};
|
||||
|
||||
public canDiscard() {
|
||||
return this._initialState !== undefined;
|
||||
}
|
||||
|
||||
public onRestore = async (version: DecoratedRevisionModel): Promise<boolean> => {
|
||||
const versionRsp = await historySrv.restoreDashboard(version.uid, version.version);
|
||||
|
||||
|
@ -128,33 +128,37 @@ export const NavToolbarActions = React.memo<Props>(({ dashboard }) => {
|
||||
}
|
||||
} else {
|
||||
if (dashboard.canEditDashboard()) {
|
||||
toolbarActions.push(
|
||||
<Button
|
||||
onClick={() => {
|
||||
dashboard.openSaveDrawer({ saveAsCopy: true });
|
||||
}}
|
||||
size="sm"
|
||||
tooltip="Save as copy"
|
||||
fill="text"
|
||||
key="save-as"
|
||||
>
|
||||
Save as
|
||||
</Button>
|
||||
);
|
||||
toolbarActions.push(
|
||||
<Button
|
||||
onClick={() => {
|
||||
dashboard.onDiscard();
|
||||
}}
|
||||
tooltip="Discard changes"
|
||||
fill="text"
|
||||
size="sm"
|
||||
key="discard"
|
||||
variant="destructive"
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
);
|
||||
if (!dashboard.state.meta.isNew) {
|
||||
toolbarActions.push(
|
||||
<Button
|
||||
onClick={() => {
|
||||
dashboard.openSaveDrawer({ saveAsCopy: true });
|
||||
}}
|
||||
size="sm"
|
||||
tooltip="Save as copy"
|
||||
fill="text"
|
||||
key="save-as"
|
||||
>
|
||||
Save as
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
if (dashboard.canDiscard()) {
|
||||
toolbarActions.push(
|
||||
<Button
|
||||
onClick={() => {
|
||||
dashboard.onDiscard();
|
||||
}}
|
||||
tooltip="Discard changes"
|
||||
fill="text"
|
||||
size="sm"
|
||||
key="discard"
|
||||
variant="destructive"
|
||||
>
|
||||
Discard
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
toolbarActions.push(
|
||||
<Button
|
||||
onClick={() => {
|
||||
|
@ -0,0 +1,26 @@
|
||||
import { defaultDashboard } from '@grafana/schema';
|
||||
import { DashboardDTO } from 'app/types';
|
||||
|
||||
export function buildNewDashboardSaveModel(urlFolderUid?: string): DashboardDTO {
|
||||
const data: DashboardDTO = {
|
||||
meta: {
|
||||
canStar: false,
|
||||
canShare: false,
|
||||
canDelete: false,
|
||||
isNew: true,
|
||||
folderUid: '',
|
||||
},
|
||||
dashboard: {
|
||||
...defaultDashboard,
|
||||
uid: '',
|
||||
title: 'New dashboard',
|
||||
panels: [],
|
||||
},
|
||||
};
|
||||
|
||||
if (urlFolderUid) {
|
||||
data.meta.folderUid = urlFolderUid;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
@ -48,6 +48,7 @@ import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
|
||||
import { NEW_LINK } from '../settings/links/utils';
|
||||
import { getQueryRunnerFor } from '../utils/utils';
|
||||
|
||||
import { buildNewDashboardSaveModel } from './buildNewDashboardSaveModel';
|
||||
import dashboard_to_load1 from './testfiles/dashboard_to_load1.json';
|
||||
import repeatingRowsAndPanelsDashboardJson from './testfiles/repeating_rows_and_panels.json';
|
||||
import {
|
||||
@ -167,6 +168,16 @@ describe('transformSaveModelToScene', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('When creating a new dashboard', () => {
|
||||
it('should initialize the DashboardScene in edit mode and dirty', () => {
|
||||
const rsp = buildNewDashboardSaveModel();
|
||||
const scene = transformSaveModelToScene(rsp);
|
||||
|
||||
expect(scene.state.isEditing).toBe(true);
|
||||
expect(scene.state.isDirty).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when organizing panels as scene children', () => {
|
||||
it('should create panels within collapsed rows', () => {
|
||||
const panel = createPanelSaveModel({
|
||||
|
@ -248,6 +248,8 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel)
|
||||
id: oldModel.id,
|
||||
description: oldModel.description,
|
||||
editable: oldModel.editable,
|
||||
isDirty: oldModel.meta.isNew,
|
||||
isEditing: oldModel.meta.isNew,
|
||||
meta: oldModel.meta,
|
||||
version: oldModel.version,
|
||||
body: new SceneGridLayout({
|
||||
|
@ -37,4 +37,13 @@ describe('dashboard utils', () => {
|
||||
|
||||
expect(url).toBe('/d/dash-1?orgId=1&new=A');
|
||||
});
|
||||
|
||||
it('Empty uid should be treated as a new dashboard', () => {
|
||||
const url = getDashboardUrl({
|
||||
uid: '',
|
||||
currentQueryParams: '?orgId=1&filter=A',
|
||||
});
|
||||
|
||||
expect(url).toBe('/dashboard/new?orgId=1&filter=A');
|
||||
});
|
||||
});
|
||||
|
@ -27,6 +27,10 @@ export interface DashboardUrlOptions {
|
||||
export function getDashboardUrl(options: DashboardUrlOptions) {
|
||||
let path = `/d/${options.uid}`;
|
||||
|
||||
if (!options.uid) {
|
||||
path = '/dashboard/new';
|
||||
}
|
||||
|
||||
if (options.soloRoute) {
|
||||
path = `/d-solo/${options.uid}`;
|
||||
}
|
||||
@ -34,6 +38,7 @@ export function getDashboardUrl(options: DashboardUrlOptions) {
|
||||
if (options.slug) {
|
||||
path += `/${options.slug}`;
|
||||
}
|
||||
|
||||
if (options.subPath) {
|
||||
path += options.subPath;
|
||||
}
|
||||
|
@ -7,7 +7,10 @@ import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
|
||||
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import { GrafanaContext } from 'app/core/context/GrafanaContext';
|
||||
import { getDashboardScenePageStateManager } from 'app/features/dashboard-scene/pages/DashboardScenePageStateManager';
|
||||
import {
|
||||
HOME_DASHBOARD_CACHE_KEY,
|
||||
getDashboardScenePageStateManager,
|
||||
} from 'app/features/dashboard-scene/pages/DashboardScenePageStateManager';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
import { DashboardDTO, DashboardRoutes } from 'app/types';
|
||||
|
||||
@ -87,7 +90,7 @@ describe('DashboardPageProxy', () => {
|
||||
});
|
||||
|
||||
it('home dashboard', async () => {
|
||||
getDashboardScenePageStateManager().setDashboardCache(DashboardRoutes.Home, dashMock);
|
||||
getDashboardScenePageStateManager().setDashboardCache(HOME_DASHBOARD_CACHE_KEY, dashMock);
|
||||
act(() => {
|
||||
setup({
|
||||
route: { routeName: DashboardRoutes.Home, component: () => null, path: '/' },
|
||||
@ -105,8 +108,8 @@ describe('DashboardPageProxy', () => {
|
||||
|
||||
act(() => {
|
||||
setup({
|
||||
route: { routeName: DashboardRoutes.Home, component: () => null, path: '/' },
|
||||
match: { params: {}, isExact: true, path: '/', url: '/' },
|
||||
route: { routeName: DashboardRoutes.Normal, component: () => null, path: '/' },
|
||||
match: { params: { uid: 'abc-def' }, isExact: true, path: '/', url: '/' },
|
||||
});
|
||||
});
|
||||
|
||||
@ -116,14 +119,14 @@ describe('DashboardPageProxy', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when dashboardSceneForViewers feature toggle enabled', () => {
|
||||
describe('when dashboardSceneForViewers feature toggle enabled', () => {
|
||||
beforeEach(() => {
|
||||
config.featureToggles.dashboardSceneForViewers = true;
|
||||
});
|
||||
|
||||
describe('when user can edit a dashboard ', () => {
|
||||
it('should not render DashboardScenePage if route is Home', async () => {
|
||||
getDashboardScenePageStateManager().setDashboardCache(DashboardRoutes.Home, dashMockEditable);
|
||||
getDashboardScenePageStateManager().setDashboardCache(HOME_DASHBOARD_CACHE_KEY, dashMockEditable);
|
||||
act(() => {
|
||||
setup({
|
||||
route: { routeName: DashboardRoutes.Home, component: () => null, path: '/' },
|
||||
@ -152,7 +155,7 @@ describe('DashboardPageProxy', () => {
|
||||
|
||||
describe('when user can only view a dashboard ', () => {
|
||||
it('should render DashboardScenePage if route is Home', async () => {
|
||||
getDashboardScenePageStateManager().setDashboardCache(DashboardRoutes.Home, dashMock);
|
||||
getDashboardScenePageStateManager().setDashboardCache(HOME_DASHBOARD_CACHE_KEY, dashMock);
|
||||
act(() => {
|
||||
setup({
|
||||
route: { routeName: DashboardRoutes.Home, component: () => null, path: '/' },
|
||||
|
@ -32,13 +32,10 @@ function DashboardPageProxy(props: DashboardPageProxyProps) {
|
||||
// To avoid querying single dashboard multiple times, stateManager.fetchDashboard uses a simple, short-lived cache.
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const dashboard = useAsync(async () => {
|
||||
const dashToFetch = props.route.routeName === DashboardRoutes.Home ? props.route.routeName : props.match.params.uid;
|
||||
|
||||
if (!dashToFetch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return stateManager.fetchDashboard({ uid: dashToFetch });
|
||||
return stateManager.fetchDashboard({
|
||||
route: props.route.routeName as DashboardRoutes,
|
||||
uid: props.match.params.uid ?? '',
|
||||
});
|
||||
}, [props.match.params.uid, props.route.routeName]);
|
||||
|
||||
if (!config.featureToggles.dashboardSceneForViewers) {
|
||||
|
@ -9,20 +9,16 @@ import store from 'app/core/store';
|
||||
import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
|
||||
import { DashboardSrv, getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||
import { getDashboardScenePageStateManager } from 'app/features/dashboard-scene/pages/DashboardScenePageStateManager';
|
||||
import {
|
||||
HOME_DASHBOARD_CACHE_KEY,
|
||||
getDashboardScenePageStateManager,
|
||||
} from 'app/features/dashboard-scene/pages/DashboardScenePageStateManager';
|
||||
import { buildNewDashboardSaveModel } from 'app/features/dashboard-scene/serialization/buildNewDashboardSaveModel';
|
||||
import { getFolderByUid } from 'app/features/folders/state/actions';
|
||||
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
|
||||
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
|
||||
import { toStateKey } from 'app/features/variables/utils';
|
||||
import {
|
||||
DashboardDTO,
|
||||
DashboardInitPhase,
|
||||
DashboardMeta,
|
||||
DashboardRoutes,
|
||||
StoreState,
|
||||
ThunkDispatch,
|
||||
ThunkResult,
|
||||
} from 'app/types';
|
||||
import { DashboardDTO, DashboardInitPhase, DashboardRoutes, StoreState, ThunkDispatch, ThunkResult } from 'app/types';
|
||||
|
||||
import { createDashboardQueryRunner } from '../../query/state/DashboardQueryRunner/DashboardQueryRunner';
|
||||
import { initVariablesTransaction } from '../../variables/state/actions';
|
||||
@ -44,7 +40,6 @@ export interface InitDashboardArgs {
|
||||
routeName?: string;
|
||||
fixUrl: boolean;
|
||||
keybindingSrv: KeybindingSrv;
|
||||
dashboardDto?: DashboardDTO;
|
||||
}
|
||||
|
||||
async function fetchDashboard(
|
||||
@ -63,7 +58,7 @@ async function fetchDashboard(
|
||||
switch (args.routeName) {
|
||||
case DashboardRoutes.Home: {
|
||||
const stateManager = getDashboardScenePageStateManager();
|
||||
const cachedDashboard = stateManager.getFromCache(DashboardRoutes.Home);
|
||||
const cachedDashboard = stateManager.getFromCache(HOME_DASHBOARD_CACHE_KEY);
|
||||
|
||||
if (cachedDashboard) {
|
||||
return cachedDashboard;
|
||||
@ -88,11 +83,6 @@ async function fetchDashboard(
|
||||
case DashboardRoutes.Public: {
|
||||
return await dashboardLoaderSrv.loadDashboard('public', args.urlSlug, args.accessToken);
|
||||
}
|
||||
case DashboardRoutes.Embedded: {
|
||||
if (args.dashboardDto) {
|
||||
return args.dashboardDto;
|
||||
}
|
||||
}
|
||||
case DashboardRoutes.Normal: {
|
||||
const dashDTO: DashboardDTO = await dashboardLoaderSrv.loadDashboard(args.urlType, args.urlSlug, args.urlUid);
|
||||
|
||||
@ -130,7 +120,7 @@ async function fetchDashboard(
|
||||
if (args.urlFolderUid) {
|
||||
await dispatch(getFolderByUid(args.urlFolderUid));
|
||||
}
|
||||
return getNewDashboardModelData(args.urlFolderUid);
|
||||
return buildNewDashboardSaveModel(args.urlFolderUid);
|
||||
}
|
||||
case DashboardRoutes.Path: {
|
||||
const path = args.urlSlug ?? '';
|
||||
@ -300,28 +290,6 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult<void> {
|
||||
};
|
||||
}
|
||||
|
||||
export function getNewDashboardModelData(urlFolderUid?: string): { dashboard: any; meta: DashboardMeta } {
|
||||
const data = {
|
||||
meta: {
|
||||
canStar: false,
|
||||
canShare: false,
|
||||
canDelete: false,
|
||||
isNew: true,
|
||||
folderUid: '',
|
||||
},
|
||||
dashboard: {
|
||||
title: 'New dashboard',
|
||||
panels: [],
|
||||
},
|
||||
};
|
||||
|
||||
if (urlFolderUid) {
|
||||
data.meta.folderUid = urlFolderUid;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
const DASHBOARD_FROM_LS_KEY = 'DASHBOARD_FROM_LS_KEY';
|
||||
|
||||
export function setDashboardToFetchFromLocalStorage(model: DashboardDTO) {
|
||||
|
@ -2,10 +2,8 @@ import { DataFrame, ExplorePanelsState } from '@grafana/data';
|
||||
import { Dashboard, DataQuery, DataSourceRef } from '@grafana/schema';
|
||||
import { DataTransformerConfig } from '@grafana/schema/dist/esm/raw/dashboard/x/dashboard_types.gen';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import {
|
||||
getNewDashboardModelData,
|
||||
setDashboardToFetchFromLocalStorage,
|
||||
} from 'app/features/dashboard/state/initDashboard';
|
||||
import { setDashboardToFetchFromLocalStorage } from 'app/features/dashboard/state/initDashboard';
|
||||
import { buildNewDashboardSaveModel } from 'app/features/dashboard-scene/serialization/buildNewDashboardSaveModel';
|
||||
import { DashboardDTO, ExplorePanelData } from 'app/types';
|
||||
|
||||
export enum AddToDashboardError {
|
||||
@ -87,7 +85,7 @@ export async function setDashboardInLocalStorage(options: AddPanelToDashboardOpt
|
||||
throw AddToDashboardError.FETCH_DASHBOARD;
|
||||
}
|
||||
} else {
|
||||
dto = getNewDashboardModelData();
|
||||
dto = buildNewDashboardSaveModel();
|
||||
}
|
||||
|
||||
dto.dashboard.panels = [panel, ...(dto.dashboard.panels ?? [])];
|
||||
|
@ -51,6 +51,7 @@ export interface DashboardMeta {
|
||||
publicDashboardEnabled?: boolean;
|
||||
dashboardNotFound?: boolean;
|
||||
isEmbedded?: boolean;
|
||||
isNew?: boolean;
|
||||
}
|
||||
|
||||
export interface AnnotationActions {
|
||||
|
Loading…
Reference in New Issue
Block a user