mirror of
https://github.com/grafana/grafana.git
synced 2025-02-16 18:34:52 -06:00
DashboardScene: Viewer role only support (#76748)
* Allow starring DashboardScene
* Add feature toggle to enable dashbaord scene for viewers
* Basics for proxying viewers to a dashboard scene
* Removed isHomeDashboard flag
* Don't use proxy for rendering dashboard page
* Revert "Don't use proxy for rendering dashboard page"
This reverts commit 95836bdb2c
.
* Pre-fetch dashboard prior page rendering
* Depend only on dashboard permissions
* Update default home dashboard to proper model...
* Fix breadcrumbs
* Types update
* Dashboards page proxy tests
* Fix missing controls
* URLs generation
* DashboardScenePageStateManager caching test
* Tests updates
* Fix wrong import
* Test update
* Gen
This commit is contained in:
parent
03a7c65ead
commit
6e80a3d59b
@ -162,6 +162,7 @@ Experimental features might be changed or removed without prior notice.
|
||||
| `alertmanagerRemoteOnly` | Disable the internal Alertmanager and only use the external one defined. |
|
||||
| `annotationPermissionUpdate` | Separate annotation permissions from dashboard permissions to allow for more granular control. |
|
||||
| `extractFieldsNameDeduplication` | Make sure extracted field names are unique in the dataframe |
|
||||
| `dashboardSceneForViewers` | Enables dashboard rendering using Scenes for viewer roles |
|
||||
|
||||
## Development feature toggles
|
||||
|
||||
|
@ -156,4 +156,5 @@ export interface FeatureToggles {
|
||||
alertmanagerRemoteOnly?: boolean;
|
||||
annotationPermissionUpdate?: boolean;
|
||||
extractFieldsNameDeduplication?: boolean;
|
||||
dashboardSceneForViewers?: boolean;
|
||||
}
|
||||
|
@ -967,5 +967,12 @@ var (
|
||||
FrontendOnly: true,
|
||||
Owner: grafanaBiSquad,
|
||||
},
|
||||
{
|
||||
Name: "dashboardSceneForViewers",
|
||||
Description: "Enables dashboard rendering using Scenes for viewer roles",
|
||||
Stage: FeatureStageExperimental,
|
||||
FrontendOnly: true,
|
||||
Owner: grafanaDashboardsSquad,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
@ -137,3 +137,4 @@ alertmanagerRemotePrimary,experimental,@grafana/alerting-squad,false,false,false
|
||||
alertmanagerRemoteOnly,experimental,@grafana/alerting-squad,false,false,false,false
|
||||
annotationPermissionUpdate,experimental,@grafana/grafana-authnz-team,false,false,false,false
|
||||
extractFieldsNameDeduplication,experimental,@grafana/grafana-bi-squad,false,false,false,true
|
||||
dashboardSceneForViewers,experimental,@grafana/dashboards-squad,false,false,false,true
|
||||
|
|
@ -558,4 +558,8 @@ const (
|
||||
// FlagExtractFieldsNameDeduplication
|
||||
// Make sure extracted field names are unique in the dataframe
|
||||
FlagExtractFieldsNameDeduplication = "extractFieldsNameDeduplication"
|
||||
|
||||
// FlagDashboardSceneForViewers
|
||||
// Enables dashboard rendering using Scenes for viewer roles
|
||||
FlagDashboardSceneForViewers = "dashboardSceneForViewers"
|
||||
)
|
||||
|
@ -5,26 +5,32 @@ 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 { DashboardRoutes } from 'app/types';
|
||||
|
||||
import { getDashboardScenePageStateManager } from './DashboardScenePageStateManager';
|
||||
|
||||
export interface Props extends GrafanaRouteComponentProps<{ uid: string }> {}
|
||||
export interface Props extends GrafanaRouteComponentProps<DashboardPageRouteParams> {}
|
||||
|
||||
export function DashboardScenePage({ match }: Props) {
|
||||
export function DashboardScenePage({ match, route }: Props) {
|
||||
const stateManager = getDashboardScenePageStateManager();
|
||||
const { dashboard, isLoading, loadError } = stateManager.useState();
|
||||
|
||||
useEffect(() => {
|
||||
stateManager.loadDashboard(match.params.uid);
|
||||
if (route.routeName === DashboardRoutes.Home) {
|
||||
stateManager.loadDashboard(route.routeName);
|
||||
} else {
|
||||
stateManager.loadDashboard(match.params.uid!);
|
||||
}
|
||||
|
||||
return () => {
|
||||
stateManager.clearState();
|
||||
};
|
||||
}, [stateManager, match.params.uid]);
|
||||
}, [stateManager, match.params.uid, route.routeName]);
|
||||
|
||||
if (!dashboard) {
|
||||
return (
|
||||
<Page layout={PageLayoutType.Canvas}>
|
||||
<Page layout={PageLayoutType.Canvas} data-testid={'dashboard-scene-page'}>
|
||||
{isLoading && <PageLoader />}
|
||||
{loadError && <h2>{loadError}</h2>}
|
||||
</Page>
|
||||
|
@ -1,15 +1,17 @@
|
||||
import { advanceBy } from 'jest-date-mock';
|
||||
|
||||
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';
|
||||
import { DashboardScenePageStateManager, DASHBOARD_CACHE_TTL } 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 loadDashboardMock = setupLoadDashboardMock({ dashboard: { uid: 'fake-dash', editable: true }, meta: {} });
|
||||
|
||||
const loader = new DashboardScenePageStateManager({});
|
||||
await loader.loadDashboard('fake-dash');
|
||||
@ -74,5 +76,39 @@ describe('DashboardScenePageStateManager', () => {
|
||||
|
||||
expect(dash2!.state.$timeRange?.state.from).toEqual('now-10m');
|
||||
});
|
||||
|
||||
describe('caching', () => {
|
||||
it('should cache the dashboard DTO', async () => {
|
||||
setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} });
|
||||
|
||||
const loader = new DashboardScenePageStateManager({});
|
||||
|
||||
expect(loader.getFromCache('fake-dash')).toBeNull();
|
||||
|
||||
await loader.loadDashboard('fake-dash');
|
||||
|
||||
expect(loader.getFromCache('fake-dash')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should load dashboard DTO from cache if requested again within 2s', async () => {
|
||||
const loadDashSpy = jest.fn();
|
||||
setupLoadDashboardMock({ dashboard: { uid: 'fake-dash' }, meta: {} }, loadDashSpy);
|
||||
|
||||
const loader = new DashboardScenePageStateManager({});
|
||||
|
||||
expect(loader.getFromCache('fake-dash')).toBeNull();
|
||||
|
||||
await loader.fetchDashboard('fake-dash');
|
||||
expect(loadDashSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
advanceBy(DASHBOARD_CACHE_TTL / 2);
|
||||
await loader.fetchDashboard('fake-dash');
|
||||
expect(loadDashSpy).toHaveBeenCalledTimes(1);
|
||||
|
||||
advanceBy(DASHBOARD_CACHE_TTL / 2 + 1);
|
||||
await loader.fetchDashboard('fake-dash');
|
||||
expect(loadDashSpy).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,6 +1,14 @@
|
||||
import { locationUtil } from '@grafana/data';
|
||||
import { getBackendSrv, isFetchError, locationService } from '@grafana/runtime';
|
||||
import { updateNavIndex } from 'app/core/actions';
|
||||
import { StateManagerBase } from 'app/core/services/StateManagerBase';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { newBrowseDashboardsEnabled } from 'app/features/browse-dashboards/featureFlag';
|
||||
import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { buildNavModel } from 'app/features/folders/state/navModel';
|
||||
import { store } from 'app/store/store';
|
||||
import { DashboardDTO, DashboardMeta, DashboardRoutes } from 'app/types';
|
||||
|
||||
import { buildPanelEditScene, PanelEditor } from '../panel-edit/PanelEditor';
|
||||
import { DashboardScene } from '../scene/DashboardScene';
|
||||
@ -14,8 +22,69 @@ export interface DashboardScenePageState {
|
||||
loadError?: string;
|
||||
}
|
||||
|
||||
export const DASHBOARD_CACHE_TTL = 2000;
|
||||
|
||||
interface DashboardCacheEntry {
|
||||
dashboard: DashboardDTO;
|
||||
ts: number;
|
||||
}
|
||||
export class DashboardScenePageStateManager extends StateManagerBase<DashboardScenePageState> {
|
||||
private cache: Record<string, DashboardScene> = {};
|
||||
// This is a simplistic, short-term cache for DashboardDTOs to avoid fetching the same dashboard multiple times across a short time span.
|
||||
private dashboardCache: Map<string, DashboardCacheEntry> = new Map();
|
||||
|
||||
// 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: string) {
|
||||
const cachedDashboard = this.getFromCache(uid);
|
||||
|
||||
if (cachedDashboard) {
|
||||
return cachedDashboard;
|
||||
}
|
||||
|
||||
let rsp: DashboardDTO | undefined;
|
||||
|
||||
try {
|
||||
if (uid === 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 (rsp?.meta) {
|
||||
rsp.meta.canSave = false;
|
||||
rsp.meta.canShare = false;
|
||||
rsp.meta.canStar = false;
|
||||
}
|
||||
} else {
|
||||
rsp = await dashboardLoaderSrv.loadDashboard('db', '', uid);
|
||||
}
|
||||
|
||||
if (rsp) {
|
||||
// Fill in meta fields
|
||||
const dashboard = this.initDashboardMeta(rsp);
|
||||
|
||||
// Populate nav model in global store according to the folder
|
||||
await this.initNavModel(dashboard);
|
||||
|
||||
this.dashboardCache.set(uid, { dashboard, ts: Date.now() });
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore cancelled errors
|
||||
if (isFetchError(e) && e.cancelled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
console.error(e);
|
||||
throw e;
|
||||
}
|
||||
|
||||
return rsp;
|
||||
}
|
||||
|
||||
public async loadDashboard(uid: string) {
|
||||
try {
|
||||
@ -55,10 +124,11 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
|
||||
|
||||
this.setState({ isLoading: true });
|
||||
|
||||
const rsp = await dashboardLoaderSrv.loadDashboard('db', '', uid);
|
||||
const rsp = await this.fetchDashboard(uid);
|
||||
|
||||
if (rsp.dashboard) {
|
||||
if (rsp?.dashboard) {
|
||||
const scene = transformSaveModelToScene(rsp);
|
||||
|
||||
this.cache[uid] = scene;
|
||||
return scene;
|
||||
}
|
||||
@ -66,10 +136,49 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
|
||||
throw new Error('Dashboard not found');
|
||||
}
|
||||
|
||||
public getFromCache(uid: string) {
|
||||
const cachedDashboard = this.dashboardCache.get(uid);
|
||||
|
||||
if (cachedDashboard && !this.hasExpired(cachedDashboard)) {
|
||||
return cachedDashboard.dashboard;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private hasExpired(entry: DashboardCacheEntry) {
|
||||
return Date.now() - entry.ts > DASHBOARD_CACHE_TTL;
|
||||
}
|
||||
|
||||
private initDashboardMeta(dashboard: DashboardDTO): DashboardDTO {
|
||||
return {
|
||||
...dashboard,
|
||||
meta: initDashboardMeta(dashboard.meta, Boolean(dashboard.dashboard?.editable)),
|
||||
};
|
||||
}
|
||||
|
||||
private async initNavModel(dashboard: DashboardDTO) {
|
||||
// only the folder API has information about ancestors
|
||||
// get parent folder (if it exists) and put it in the store
|
||||
// this will be used to populate the full breadcrumb trail
|
||||
if (newBrowseDashboardsEnabled() && dashboard.meta.folderUid) {
|
||||
try {
|
||||
const folder = await backendSrv.getFolderByUid(dashboard.meta.folderUid);
|
||||
store.dispatch(updateNavIndex(buildNavModel(folder)));
|
||||
} catch (err) {
|
||||
console.warn('Error fetching parent folder', dashboard.meta.folderUid, 'for dashboard', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public clearState() {
|
||||
getDashboardSrv().setCurrent(undefined);
|
||||
this.setState({ dashboard: undefined, loadError: undefined, isLoading: false, panelEditor: undefined });
|
||||
}
|
||||
|
||||
public setDashboardCache(uid: string, dashboard: DashboardDTO) {
|
||||
this.dashboardCache.set(uid, { dashboard, ts: Date.now() });
|
||||
}
|
||||
}
|
||||
|
||||
let stateManager: DashboardScenePageStateManager | null = null;
|
||||
@ -81,3 +190,25 @@ export function getDashboardScenePageStateManager(): DashboardScenePageStateMana
|
||||
|
||||
return stateManager;
|
||||
}
|
||||
|
||||
function initDashboardMeta(source: DashboardMeta, isEditable: boolean) {
|
||||
const result = source ? { ...source } : {};
|
||||
|
||||
result.canShare = source.canShare !== false;
|
||||
result.canSave = source.canSave !== false;
|
||||
result.canStar = source.canStar !== false;
|
||||
result.canEdit = source.canEdit !== false;
|
||||
result.canDelete = source.canDelete !== false;
|
||||
|
||||
result.showSettings = source.canEdit;
|
||||
result.canMakeEditable = source.canSave && !isEditable;
|
||||
result.hasUnsavedFolderChange = false;
|
||||
|
||||
if (!isEditable) {
|
||||
result.canEdit = false;
|
||||
result.canDelete = false;
|
||||
result.canSave = false;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import * as H from 'history';
|
||||
|
||||
import { NavIndex } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import {
|
||||
getUrlSyncManager,
|
||||
@ -54,10 +55,10 @@ export class PanelEditor extends SceneObjectBase<PanelEditorState> {
|
||||
getUrlSyncManager().initSync(this);
|
||||
}
|
||||
|
||||
public getPageNav(location: H.Location) {
|
||||
public getPageNav(location: H.Location, navIndex: NavIndex) {
|
||||
return {
|
||||
text: 'Edit panel',
|
||||
parentItem: this.state.dashboardRef.resolve().getPageNav(location),
|
||||
parentItem: this.state.dashboardRef.resolve().getPageNav(location, navIndex),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -8,6 +8,7 @@ import { Button, useStyles2 } from '@grafana/ui';
|
||||
import { AppChromeUpdate } from 'app/core/components/AppChrome/AppChromeUpdate';
|
||||
import { NavToolbarSeparator } from 'app/core/components/AppChrome/NavToolbar/NavToolbarSeparator';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { useSelector } from 'app/types/store';
|
||||
|
||||
import { PanelEditor } from './PanelEditor';
|
||||
|
||||
@ -15,7 +16,8 @@ export function PanelEditorRenderer({ model }: SceneComponentProps<PanelEditor>)
|
||||
const { body, controls, drawer } = model.useState();
|
||||
const styles = useStyles2(getStyles);
|
||||
const location = useLocation();
|
||||
const pageNav = model.getPageNav(location);
|
||||
const navIndex = useSelector((state) => state.navIndex);
|
||||
const pageNav = model.getPageNav(location, navIndex);
|
||||
|
||||
return (
|
||||
<Page navId="scenes" pageNav={pageNav} layout={PageLayoutType.Custom}>
|
||||
|
@ -1,8 +1,8 @@
|
||||
import * as H from 'history';
|
||||
import { Unsubscribable } from 'rxjs';
|
||||
|
||||
import { CoreApp, DataQueryRequest, NavModelItem, UrlQueryMap } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { CoreApp, DataQueryRequest, NavIndex, NavModelItem, UrlQueryMap } from '@grafana/data';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import {
|
||||
getUrlSyncManager,
|
||||
SceneFlexLayout,
|
||||
@ -14,6 +14,8 @@ import {
|
||||
SceneObjectStateChangedEvent,
|
||||
sceneUtils,
|
||||
} from '@grafana/scenes';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { newBrowseDashboardsEnabled } from 'app/features/browse-dashboards/featureFlag';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
import { DashboardMeta } from 'app/types';
|
||||
|
||||
@ -155,16 +157,45 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
this.setState({ overlay: new SaveDashboardDrawer({ dashboardRef: this.getRef() }) });
|
||||
};
|
||||
|
||||
public getPageNav(location: H.Location) {
|
||||
public getPageNav(location: H.Location, navIndex: NavIndex) {
|
||||
const { meta } = this.state;
|
||||
|
||||
let pageNav: NavModelItem = {
|
||||
text: this.state.title,
|
||||
url: getDashboardUrl({
|
||||
uid: this.state.uid,
|
||||
currentQueryParams: location.search,
|
||||
updateQuery: { viewPanel: null, inspect: null },
|
||||
useExperimentalURL: Boolean(config.featureToggles.dashboardSceneForViewers && meta.canEdit),
|
||||
}),
|
||||
};
|
||||
|
||||
const { folderTitle, folderUid } = meta;
|
||||
|
||||
if (folderUid) {
|
||||
if (newBrowseDashboardsEnabled()) {
|
||||
const folderNavModel = getNavModel(navIndex, `folder-dashboards-${folderUid}`).main;
|
||||
// If the folder hasn't loaded (maybe user doesn't have permission on it?) then
|
||||
// don't show the "page not found" breadcrumb
|
||||
if (folderNavModel.id !== 'not-found') {
|
||||
pageNav = {
|
||||
...pageNav,
|
||||
parentItem: folderNavModel,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
if (folderTitle) {
|
||||
pageNav = {
|
||||
...pageNav,
|
||||
parentItem: {
|
||||
text: folderTitle,
|
||||
url: `/dashboards/f/${meta.folderUid}`,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.state.viewPanelKey) {
|
||||
pageNav = {
|
||||
text: 'View panel',
|
||||
@ -216,6 +247,25 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
this.setState({ overlay: undefined });
|
||||
}
|
||||
|
||||
public async onStarDashboard() {
|
||||
const { meta, uid } = this.state;
|
||||
if (!uid) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await getDashboardSrv().starDashboard(uid, Boolean(meta.isStarred));
|
||||
|
||||
this.setState({
|
||||
meta: {
|
||||
...meta,
|
||||
isStarred: result,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Failed to star dashboard', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by the SceneQueryRunner to privide contextural parameters (tracking) props for the request
|
||||
*/
|
||||
@ -230,6 +280,8 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
||||
}
|
||||
|
||||
canEditDashboard() {
|
||||
return Boolean(this.state.meta.canEdit || this.state.meta.canMakeEditable);
|
||||
const { meta } = this.state;
|
||||
|
||||
return Boolean(meta.canEdit || meta.canMakeEditable);
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,14 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { SceneComponentProps, SceneDebugger } from '@grafana/scenes';
|
||||
import { CustomScrollbar, useStyles2 } from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { useSelector } from 'app/types';
|
||||
|
||||
import { DashboardScene } from './DashboardScene';
|
||||
import { NavToolbarActions } from './NavToolbarActions';
|
||||
@ -14,14 +17,20 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
|
||||
const { controls, viewPanelKey: viewPanelId, overlay } = model.useState();
|
||||
const styles = useStyles2(getStyles);
|
||||
const location = useLocation();
|
||||
const pageNav = model.getPageNav(location);
|
||||
const navIndex = useSelector((state) => state.navIndex);
|
||||
const pageNav = model.getPageNav(location, navIndex);
|
||||
const bodyToRender = model.getBodyToRender(viewPanelId);
|
||||
|
||||
const navProps = config.featureToggles.dashboardSceneForViewers
|
||||
? { navModel: getNavModel(navIndex, 'dashboards/browse') }
|
||||
: { navId: 'scenes' };
|
||||
|
||||
return (
|
||||
<Page navId="scenes" pageNav={pageNav} layout={PageLayoutType.Custom}>
|
||||
<Page {...navProps} pageNav={pageNav} layout={PageLayoutType.Custom}>
|
||||
<CustomScrollbar autoHeightMin={'100%'}>
|
||||
<div className={styles.canvasContent}>
|
||||
<NavToolbarActions dashboard={model} />
|
||||
|
||||
{controls && (
|
||||
<div className={styles.controls}>
|
||||
{controls.map((control) => (
|
||||
@ -30,7 +39,7 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
|
||||
<SceneDebugger scene={model} key={'scene-debugger'} />
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.body}>
|
||||
<div className={cx(styles.body)}>
|
||||
<bodyToRender.Component model={bodyToRender} />
|
||||
</div>
|
||||
</div>
|
||||
@ -57,6 +66,7 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
gap: '8px',
|
||||
marginBottom: theme.spacing(2),
|
||||
}),
|
||||
|
||||
controls: css({
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
|
@ -16,10 +16,28 @@ interface Props {
|
||||
}
|
||||
|
||||
export const NavToolbarActions = React.memo<Props>(({ dashboard }) => {
|
||||
const { actions = [], isEditing, viewPanelKey, isDirty, uid } = dashboard.useState();
|
||||
const { actions = [], isEditing, viewPanelKey, isDirty, uid, meta } = dashboard.useState();
|
||||
const toolbarActions = (actions ?? []).map((action) => <action.Component key={action.state.key} model={action} />);
|
||||
|
||||
if (uid) {
|
||||
if (meta.canStar) {
|
||||
let desc = meta.isStarred
|
||||
? t('dashboard.toolbar.unmark-favorite', 'Unmark as favorite')
|
||||
: t('dashboard.toolbar.mark-favorite', 'Mark as favorite');
|
||||
|
||||
toolbarActions.push(
|
||||
<DashNavButton
|
||||
key="star-dashboard-button"
|
||||
tooltip={desc}
|
||||
icon={meta.isStarred ? 'favorite' : 'star'}
|
||||
iconType={meta.isStarred ? 'mono' : 'default'}
|
||||
iconSize="lg"
|
||||
onClick={() => {
|
||||
dashboard.onStarDashboard();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
toolbarActions.push(
|
||||
<DashNavButton
|
||||
key="share-dashboard-button"
|
||||
@ -61,36 +79,38 @@ export const NavToolbarActions = React.memo<Props>(({ dashboard }) => {
|
||||
}
|
||||
|
||||
if (!isEditing) {
|
||||
// TODO check permissions
|
||||
toolbarActions.push(
|
||||
<Button
|
||||
onClick={dashboard.onEnterEditMode}
|
||||
tooltip="Enter edit mode"
|
||||
key="edit"
|
||||
variant="primary"
|
||||
icon="pen"
|
||||
fill="text"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
);
|
||||
if (dashboard.canEditDashboard()) {
|
||||
toolbarActions.push(
|
||||
<Button
|
||||
onClick={dashboard.onEnterEditMode}
|
||||
tooltip="Enter edit mode"
|
||||
key="edit"
|
||||
variant="primary"
|
||||
icon="pen"
|
||||
fill="text"
|
||||
>
|
||||
Edit
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// TODO check permissions
|
||||
toolbarActions.push(
|
||||
<Button onClick={dashboard.onSave} tooltip="Save as copy" fill="text" key="save-as">
|
||||
Save as
|
||||
</Button>
|
||||
);
|
||||
toolbarActions.push(
|
||||
<Button onClick={dashboard.onDiscard} tooltip="Save changes" fill="text" key="discard" variant="destructive">
|
||||
Discard
|
||||
</Button>
|
||||
);
|
||||
toolbarActions.push(
|
||||
<Button onClick={dashboard.onSave} tooltip="Save changes" key="save" disabled={!isDirty}>
|
||||
Save
|
||||
</Button>
|
||||
);
|
||||
if (dashboard.canEditDashboard()) {
|
||||
toolbarActions.push(
|
||||
<Button onClick={dashboard.onSave} tooltip="Save as copy" fill="text" key="save-as">
|
||||
Save as
|
||||
</Button>
|
||||
);
|
||||
toolbarActions.push(
|
||||
<Button onClick={dashboard.onDiscard} tooltip="Save changes" fill="text" key="discard" variant="destructive">
|
||||
Discard
|
||||
</Button>
|
||||
);
|
||||
toolbarActions.push(
|
||||
<Button onClick={dashboard.onSave} tooltip="Save changes" key="save" disabled={!isDirty}>
|
||||
Save
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return <AppChromeUpdate actions={toolbarActions} />;
|
||||
|
@ -80,6 +80,9 @@ async function buildTestScene(options: SceneOptions) {
|
||||
const scene = new DashboardScene({
|
||||
title: 'hello',
|
||||
uid: 'dash-1',
|
||||
meta: {
|
||||
canEdit: true,
|
||||
},
|
||||
body: new SceneGridLayout({
|
||||
children: [
|
||||
new SceneGridItem({
|
||||
|
@ -35,19 +35,22 @@ export function panelMenuBehavior(menu: VizPanelMenu) {
|
||||
href: getViewPanelUrl(panel),
|
||||
});
|
||||
|
||||
// We could check isEditing here but I kind of think this should always be in the menu,
|
||||
// and going into panel edit should make the dashboard go into edit mode is it's not already
|
||||
items.push({
|
||||
text: t('panel.header-menu.edit', `Edit`),
|
||||
iconClassName: 'eye',
|
||||
shortcut: 'e',
|
||||
onClick: () => reportInteraction('dashboards_panelheader_menu', { item: 'edit' }),
|
||||
href: getDashboardUrl({
|
||||
uid: dashboard.state.uid,
|
||||
subPath: `/panel-edit/${panelId}`,
|
||||
currentQueryParams: location.search,
|
||||
}),
|
||||
});
|
||||
if (dashboard.canEditDashboard()) {
|
||||
// We could check isEditing here but I kind of think this should always be in the menu,
|
||||
// and going into panel edit should make the dashboard go into edit mode is it's not already
|
||||
items.push({
|
||||
text: t('panel.header-menu.edit', `Edit`),
|
||||
iconClassName: 'eye',
|
||||
shortcut: 'e',
|
||||
onClick: () => reportInteraction('dashboards_panelheader_menu', { item: 'edit' }),
|
||||
href: getDashboardUrl({
|
||||
uid: dashboard.state.uid,
|
||||
subPath: `/panel-edit/${panelId}`,
|
||||
currentQueryParams: location.search,
|
||||
useExperimentalURL: true,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
text: t('panel.header-menu.share', `Share`),
|
||||
|
@ -203,18 +203,23 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel)
|
||||
});
|
||||
}
|
||||
|
||||
const controls: SceneObject[] = [
|
||||
let controls: SceneObject[] = [
|
||||
new VariableValueSelectors({}),
|
||||
...filtersSets,
|
||||
new SceneDataLayerControls(),
|
||||
new SceneControlsSpacer(),
|
||||
new SceneTimePicker({}),
|
||||
new SceneRefreshPicker({
|
||||
refresh: oldModel.refresh,
|
||||
intervals: oldModel.timepicker.refresh_intervals,
|
||||
}),
|
||||
];
|
||||
|
||||
if (!Boolean(oldModel.timepicker.hidden)) {
|
||||
controls = controls.concat([
|
||||
new SceneTimePicker({}),
|
||||
new SceneRefreshPicker({
|
||||
refresh: oldModel.refresh,
|
||||
intervals: oldModel.timepicker.refresh_intervals,
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
||||
return new DashboardScene({
|
||||
title: oldModel.title,
|
||||
uid: oldModel.uid,
|
||||
|
@ -29,6 +29,7 @@ describe('ShareLinkTab', () => {
|
||||
config.appUrl = 'http://dashboards.grafana.com/grafana/';
|
||||
config.rendererAvailable = true;
|
||||
config.bootData.user.orgId = 1;
|
||||
config.featureToggles.dashboardSceneForViewers = true;
|
||||
locationService.push('/scenes/dashboard/dash-1?from=now-6h&to=now');
|
||||
});
|
||||
|
||||
@ -98,6 +99,9 @@ function buildAndRenderScenario(options: ScenarioOptions) {
|
||||
const dashboard = new DashboardScene({
|
||||
title: 'hello',
|
||||
uid: 'dash-1',
|
||||
meta: {
|
||||
canEdit: true,
|
||||
},
|
||||
$timeRange: new SceneTimeRange({}),
|
||||
body: new SceneGridLayout({
|
||||
children: [
|
||||
|
@ -74,6 +74,7 @@ export class ShareLinkTab extends SceneObjectBase<ShareLinkTabState> {
|
||||
currentQueryParams: location.search,
|
||||
updateQuery: urlParamsUpdate,
|
||||
absolute: true,
|
||||
useExperimentalURL: Boolean(config.featureToggles.dashboardSceneForViewers && dashboard.state.meta.canEdit),
|
||||
});
|
||||
|
||||
if (useShortUrl) {
|
||||
|
@ -18,8 +18,8 @@ import { DashboardDTO } from 'app/types';
|
||||
import { PanelRepeaterGridItem, RepeatDirection } from '../scene/PanelRepeaterGridItem';
|
||||
import { RowRepeaterBehavior } from '../scene/RowRepeaterBehavior';
|
||||
|
||||
export function setupLoadDashboardMock(rsp: DeepPartial<DashboardDTO>) {
|
||||
const loadDashboardMock = jest.fn().mockResolvedValue(rsp);
|
||||
export function setupLoadDashboardMock(rsp: DeepPartial<DashboardDTO>, spy?: jest.Mock) {
|
||||
const loadDashboardMock = (spy || jest.fn()).mockResolvedValue(rsp);
|
||||
setDashboardLoaderSrv({
|
||||
loadDashboard: loadDashboardMock,
|
||||
// disabling type checks since this is a test util
|
||||
|
@ -2,13 +2,18 @@ import { getDashboardUrl } from './urlBuilders';
|
||||
|
||||
describe('dashboard utils', () => {
|
||||
it('Can getUrl', () => {
|
||||
const url = getDashboardUrl({ uid: 'dash-1', currentQueryParams: '?orgId=1&filter=A' });
|
||||
const url = getDashboardUrl({ uid: 'dash-1', currentQueryParams: '?orgId=1&filter=A', useExperimentalURL: true });
|
||||
|
||||
expect(url).toBe('/scenes/dashboard/dash-1?orgId=1&filter=A');
|
||||
});
|
||||
|
||||
it('Can getUrl with subpath', () => {
|
||||
const url = getDashboardUrl({ uid: 'dash-1', subPath: '/panel-edit/2', currentQueryParams: '?orgId=1&filter=A' });
|
||||
const url = getDashboardUrl({
|
||||
uid: 'dash-1',
|
||||
subPath: '/panel-edit/2',
|
||||
currentQueryParams: '?orgId=1&filter=A',
|
||||
useExperimentalURL: true,
|
||||
});
|
||||
|
||||
expect(url).toBe('/scenes/dashboard/dash-1/panel-edit/2?orgId=1&filter=A');
|
||||
});
|
||||
@ -18,6 +23,7 @@ describe('dashboard utils', () => {
|
||||
uid: 'dash-1',
|
||||
currentQueryParams: '?orgId=1&filter=A',
|
||||
updateQuery: { filter: null, new: 'A' },
|
||||
useExperimentalURL: true,
|
||||
});
|
||||
|
||||
expect(url).toBe('/scenes/dashboard/dash-1?orgId=1&new=A');
|
||||
|
@ -20,10 +20,15 @@ export interface DashboardUrlOptions {
|
||||
absolute?: boolean;
|
||||
// Add tz to query params
|
||||
timeZone?: string;
|
||||
|
||||
// Add tz to query params
|
||||
useExperimentalURL?: boolean;
|
||||
}
|
||||
|
||||
export function getDashboardUrl(options: DashboardUrlOptions) {
|
||||
let path = `/scenes/dashboard/${options.uid}${options.subPath ?? ''}`;
|
||||
let path = options.useExperimentalURL
|
||||
? `/scenes/dashboard/${options.uid}${options.subPath ?? ''}`
|
||||
: `/d/${options.uid}${options.subPath ?? ''}`;
|
||||
|
||||
if (options.soloRoute) {
|
||||
path = `/d-solo/${options.uid}${options.subPath ?? ''}`;
|
||||
|
@ -40,27 +40,7 @@ import { cleanUpDashboardAndVariables } from '../state/actions';
|
||||
import { initDashboard } from '../state/initDashboard';
|
||||
import { calculateNewPanelGridPos } from '../utils/panel';
|
||||
|
||||
export interface DashboardPageRouteParams {
|
||||
uid?: string;
|
||||
type?: string;
|
||||
slug?: string;
|
||||
accessToken?: string;
|
||||
}
|
||||
|
||||
export type DashboardPageRouteSearchParams = {
|
||||
tab?: string;
|
||||
folderUid?: string;
|
||||
editPanel?: string;
|
||||
viewPanel?: string;
|
||||
editview?: string;
|
||||
addWidget?: boolean;
|
||||
panelType?: string;
|
||||
inspect?: string;
|
||||
from?: string;
|
||||
to?: string;
|
||||
refresh?: string;
|
||||
kiosk?: string | true;
|
||||
};
|
||||
import { DashboardPageRouteParams, DashboardPageRouteSearchParams } from './types';
|
||||
|
||||
export const mapStateToProps = (state: StoreState) => ({
|
||||
initPhase: state.dashboard.initPhase,
|
||||
|
@ -0,0 +1,171 @@
|
||||
import { render, screen, act, waitFor } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import { Router } from 'react-router-dom';
|
||||
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 { configureStore } from 'app/store/configureStore';
|
||||
import { DashboardDTO, DashboardRoutes } from 'app/types';
|
||||
|
||||
import DashboardPageProxy, { DashboardPageProxyProps } from './DashboardPageProxy';
|
||||
|
||||
const dashMock: DashboardDTO = {
|
||||
dashboard: {
|
||||
id: 1,
|
||||
annotations: {
|
||||
list: [],
|
||||
},
|
||||
uid: 'uid',
|
||||
title: 'title',
|
||||
panels: [],
|
||||
version: 1,
|
||||
schemaVersion: 1,
|
||||
},
|
||||
meta: {
|
||||
canEdit: false,
|
||||
},
|
||||
};
|
||||
|
||||
const dashMockEditable = {
|
||||
...dashMock,
|
||||
meta: {
|
||||
...dashMock.meta,
|
||||
canEdit: true,
|
||||
},
|
||||
};
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
...jest.requireActual('@grafana/runtime'),
|
||||
getDataSourceSrv: jest.fn().mockReturnValue({
|
||||
getInstanceSettings: () => {
|
||||
return { name: 'Grafana' };
|
||||
},
|
||||
get: jest.fn().mockResolvedValue({}),
|
||||
}),
|
||||
}));
|
||||
|
||||
function setup(props: Partial<DashboardPageProxyProps>) {
|
||||
const context = getGrafanaContextMock();
|
||||
const store = configureStore({});
|
||||
|
||||
return render(
|
||||
<GrafanaContext.Provider value={context}>
|
||||
<Provider store={store}>
|
||||
<Router history={locationService.getHistory()}>
|
||||
<DashboardPageProxy
|
||||
location={locationService.getLocation()}
|
||||
history={locationService.getHistory()}
|
||||
queryParams={{}}
|
||||
route={{ routeName: DashboardRoutes.Home, component: () => null, path: '/' }}
|
||||
match={{ params: {}, isExact: true, path: '/', url: '/' }}
|
||||
{...props}
|
||||
/>
|
||||
</Router>
|
||||
</Provider>
|
||||
</GrafanaContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
describe('DashboardPageProxy', () => {
|
||||
describe('when dashboardSceneForViewers feature toggle disabled', () => {
|
||||
beforeEach(() => {
|
||||
config.featureToggles.dashboardSceneForViewers = false;
|
||||
});
|
||||
|
||||
it('home dashboard', async () => {
|
||||
getDashboardScenePageStateManager().setDashboardCache(DashboardRoutes.Home, dashMock);
|
||||
act(() => {
|
||||
setup({
|
||||
route: { routeName: DashboardRoutes.Home, component: () => null, path: '/' },
|
||||
match: { params: {}, isExact: true, path: '/', url: '/' },
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId('dashboard-scene-page')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('uid dashboard', async () => {
|
||||
getDashboardScenePageStateManager().setDashboardCache('abc-def', dashMock);
|
||||
|
||||
act(() => {
|
||||
setup({
|
||||
route: { routeName: DashboardRoutes.Home, component: () => null, path: '/' },
|
||||
match: { params: {}, isExact: true, path: '/', url: '/' },
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId('dashboard-scene-page')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
act(() => {
|
||||
setup({
|
||||
route: { routeName: DashboardRoutes.Home, component: () => null, path: '/' },
|
||||
match: { params: {}, isExact: true, path: '/', url: '/' },
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId('dashboard-scene-page')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not render DashboardScenePage if route is Normal and has uid', async () => {
|
||||
getDashboardScenePageStateManager().setDashboardCache('abc-def', dashMockEditable);
|
||||
act(() => {
|
||||
setup({
|
||||
route: { routeName: DashboardRoutes.Normal, component: () => null, path: '/' },
|
||||
match: { params: { uid: 'abc-def' }, isExact: true, path: '/', url: '/' },
|
||||
});
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId('dashboard-scene-page')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when user can only view a dashboard ', () => {
|
||||
it('should render DashboardScenePage if route is Home', async () => {
|
||||
getDashboardScenePageStateManager().setDashboardCache(DashboardRoutes.Home, dashMock);
|
||||
act(() => {
|
||||
setup({
|
||||
route: { routeName: DashboardRoutes.Home, component: () => null, path: '/' },
|
||||
match: { params: {}, isExact: true, path: '/', url: '/' },
|
||||
});
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId('dashboard-scene-page')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render DashboardScenePage if route is Normal and has uid', async () => {
|
||||
getDashboardScenePageStateManager().setDashboardCache('abc-def', dashMock);
|
||||
act(() => {
|
||||
setup({
|
||||
route: { routeName: DashboardRoutes.Normal, component: () => null, path: '/' },
|
||||
match: { params: { uid: 'abc-def' }, isExact: true, path: '/', url: '/' },
|
||||
});
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId('dashboard-scene-page')).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,54 @@
|
||||
import React from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
import DashboardScenePage from 'app/features/dashboard-scene/pages/DashboardScenePage';
|
||||
import { getDashboardScenePageStateManager } from 'app/features/dashboard-scene/pages/DashboardScenePageStateManager';
|
||||
import { DashboardRoutes } from 'app/types';
|
||||
|
||||
import DashboardPage from './DashboardPage';
|
||||
import { DashboardPageRouteParams, DashboardPageRouteSearchParams } from './types';
|
||||
|
||||
export type DashboardPageProxyProps = GrafanaRouteComponentProps<
|
||||
DashboardPageRouteParams,
|
||||
DashboardPageRouteSearchParams
|
||||
>;
|
||||
|
||||
// This proxy component is used for Dashboard -> Scenes migration.
|
||||
// It will render DashboardScenePage if the user is only allowed to view the dashboard.
|
||||
function DashboardPageProxy(props: DashboardPageProxyProps) {
|
||||
const stateManager = getDashboardScenePageStateManager();
|
||||
const isScenesSupportedRoute = Boolean(
|
||||
props.route.routeName === DashboardRoutes.Home ||
|
||||
(props.route.routeName === DashboardRoutes.Normal && props.match.params.uid)
|
||||
);
|
||||
|
||||
// We pre-fetch dashboard to render dashboard page component depending on dashboard permissions.
|
||||
// To avoid querying single dashboard multiple times, stateManager.fetchDashboard uses a simple, short-lived cache.
|
||||
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(dashToFetch);
|
||||
}, [props.match.params.uid, props.route.routeName]);
|
||||
|
||||
if (!config.featureToggles.dashboardSceneForViewers) {
|
||||
return <DashboardPage {...props} />;
|
||||
}
|
||||
|
||||
if (dashboard.loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (dashboard.value && !dashboard.value.meta.canEdit && isScenesSupportedRoute) {
|
||||
return <DashboardScenePage {...props} />;
|
||||
} else {
|
||||
return <DashboardPage {...props} />;
|
||||
}
|
||||
}
|
||||
|
||||
export default DashboardPageProxy;
|
21
public/app/features/dashboard/containers/types.ts
Normal file
21
public/app/features/dashboard/containers/types.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export interface DashboardPageRouteParams {
|
||||
uid?: string;
|
||||
type?: string;
|
||||
slug?: string;
|
||||
accessToken?: string;
|
||||
}
|
||||
|
||||
export type DashboardPageRouteSearchParams = {
|
||||
tab?: string;
|
||||
folderUid?: string;
|
||||
editPanel?: string;
|
||||
viewPanel?: string;
|
||||
editview?: string;
|
||||
addWidget?: boolean;
|
||||
panelType?: string;
|
||||
inspect?: string;
|
||||
from?: string;
|
||||
to?: string;
|
||||
refresh?: string;
|
||||
kiosk?: string | true;
|
||||
};
|
@ -7,6 +7,7 @@ import { getBackendSrv, locationService } from '@grafana/runtime';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import impressionSrv from 'app/core/services/impression_srv';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
import { getDashboardScenePageStateManager } from 'app/features/dashboard-scene/pages/DashboardScenePageStateManager';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { DashboardDataDTO, DashboardDTO, DashboardMeta } from 'app/types';
|
||||
|
||||
@ -35,6 +36,7 @@ export class DashboardLoaderSrv {
|
||||
}
|
||||
|
||||
loadDashboard(type: UrlQueryValue, slug: string | undefined, uid: string | undefined): Promise<DashboardDTO> {
|
||||
const stateManager = getDashboardScenePageStateManager();
|
||||
let promise;
|
||||
|
||||
if (type === 'script' && slug) {
|
||||
@ -71,6 +73,11 @@ export class DashboardLoaderSrv {
|
||||
};
|
||||
});
|
||||
} else if (uid) {
|
||||
const cachedDashboard = stateManager.getFromCache(uid);
|
||||
if (cachedDashboard) {
|
||||
return Promise.resolve(cachedDashboard);
|
||||
}
|
||||
|
||||
promise = backendSrv
|
||||
.getDashboardByUid(uid)
|
||||
.then((result) => {
|
||||
|
@ -10,6 +10,7 @@ import { newBrowseDashboardsEnabled } from 'app/features/browse-dashboards/featu
|
||||
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 { getFolderByUid } from 'app/features/folders/state/actions';
|
||||
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
|
||||
import { playlistSrv } from 'app/features/playlist/PlaylistSrv';
|
||||
@ -62,6 +63,13 @@ async function fetchDashboard(
|
||||
try {
|
||||
switch (args.routeName) {
|
||||
case DashboardRoutes.Home: {
|
||||
const stateManager = getDashboardScenePageStateManager();
|
||||
const cachedDashboard = stateManager.getFromCache(DashboardRoutes.Home);
|
||||
|
||||
if (cachedDashboard) {
|
||||
return cachedDashboard;
|
||||
}
|
||||
|
||||
// load home dash
|
||||
const dashDTO: DashboardDTO = await backendSrv.get('/api/dashboards/home');
|
||||
|
||||
|
@ -36,7 +36,7 @@ export function getAppRoutes(): RouteDescriptor[] {
|
||||
pageClass: 'page-dashboard',
|
||||
routeName: DashboardRoutes.Home,
|
||||
component: SafeDynamicImport(
|
||||
() => import(/* webpackChunkName: "DashboardPage" */ '../features/dashboard/containers/DashboardPage')
|
||||
() => import(/* webpackChunkName: "DashboardPageProxy" */ '../features/dashboard/containers/DashboardPageProxy')
|
||||
),
|
||||
},
|
||||
{
|
||||
@ -44,7 +44,7 @@ export function getAppRoutes(): RouteDescriptor[] {
|
||||
pageClass: 'page-dashboard',
|
||||
routeName: DashboardRoutes.Normal,
|
||||
component: SafeDynamicImport(
|
||||
() => import(/* webpackChunkName: "DashboardPage" */ '../features/dashboard/containers/DashboardPage')
|
||||
() => import(/* webpackChunkName: "DashboardPageProxy" */ '../features/dashboard/containers/DashboardPageProxy')
|
||||
),
|
||||
},
|
||||
{
|
||||
@ -53,7 +53,7 @@ export function getAppRoutes(): RouteDescriptor[] {
|
||||
pageClass: 'page-dashboard',
|
||||
routeName: DashboardRoutes.New,
|
||||
component: SafeDynamicImport(
|
||||
() => import(/* webpackChunkName: "DashboardPage" */ '../features/dashboard/containers/DashboardPage')
|
||||
() => import(/* webpackChunkName: "DashboardPage" */ '../features/dashboard/containers/DashboardPageProxy')
|
||||
),
|
||||
},
|
||||
{
|
||||
@ -68,7 +68,7 @@ export function getAppRoutes(): RouteDescriptor[] {
|
||||
pageClass: 'page-dashboard',
|
||||
routeName: DashboardRoutes.Normal,
|
||||
component: SafeDynamicImport(
|
||||
() => import(/* webpackChunkName: "DashboardPage" */ '../features/dashboard/containers/DashboardPage')
|
||||
() => import(/* webpackChunkName: "DashboardPage" */ '../features/dashboard/containers/DashboardPageProxy')
|
||||
),
|
||||
},
|
||||
{
|
||||
|
@ -23,16 +23,18 @@
|
||||
"x": 0,
|
||||
"y": 4
|
||||
},
|
||||
"headings": true,
|
||||
"id": 3,
|
||||
"limit": 30,
|
||||
"links": [],
|
||||
"options": {},
|
||||
"query": "",
|
||||
"recent": true,
|
||||
"search": false,
|
||||
"starred": true,
|
||||
"tags": [],
|
||||
"options": {
|
||||
"showStarred": true,
|
||||
"showRecentlyViewed": true,
|
||||
"showSearch": false,
|
||||
"showHeadings": true,
|
||||
"folderId": 0,
|
||||
"maxItems": 30,
|
||||
"tags":[],
|
||||
"query": ""
|
||||
},
|
||||
"title": "Dashboards",
|
||||
"type": "dashlist"
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user