Basics for proxying viewers to a dashboard scene

This commit is contained in:
Dominik Prokop
2023-10-18 12:40:34 +02:00
parent 156eb7dfee
commit bbf7776505
10 changed files with 162 additions and 76 deletions

View File

@@ -5,22 +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 { 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 (

View File

@@ -1,6 +1,8 @@
import { getBackendSrv } from '@grafana/runtime';
import { StateManagerBase } from 'app/core/services/StateManagerBase';
import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { DashboardDTO, DashboardRoutes } from 'app/types';
import { buildPanelEditScene, PanelEditor } from '../panel-edit/PanelEditor';
import { DashboardScene } from '../scene/DashboardScene';
@@ -55,10 +57,34 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
this.setState({ isLoading: true });
const rsp = await dashboardLoaderSrv.loadDashboard('db', '', uid);
let rsp: DashboardDTO | undefined;
if (rsp.dashboard) {
if (uid === DashboardRoutes.Home) {
rsp = await getBackendSrv().get('/api/dashboards/home');
// TODO
// if user specified a custom home dashboard redirect to that
// if (rsp?.redirectUri) {
// const newUrl = locationUtil.stripBaseFromUrl(rsp.redirectUri);
// locationService.replace(newUrl);
// }
if (rsp?.meta) {
rsp.meta.canSave = false;
rsp.meta.canShare = false;
rsp.meta.canStar = false;
}
} else {
rsp = await dashboardLoaderSrv.loadDashboard('db', '', uid);
}
if (rsp?.dashboard) {
const scene = transformSaveModelToScene(rsp);
if (uid === DashboardRoutes.Home) {
scene.isHomeDashboard = true;
}
this.cache[uid] = scene;
return scene;
}

View File

@@ -14,8 +14,9 @@ import {
SceneObjectStateChangedEvent,
sceneUtils,
} from '@grafana/scenes';
import { contextSrv } from 'app/core/core';
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { DashboardMeta } from 'app/types';
import { AccessControlAction, DashboardMeta } from 'app/types';
import { DashboardSceneRenderer } from '../scene/DashboardSceneRenderer';
import { SaveDashboardDrawer } from '../serialization/SaveDashboardDrawer';
@@ -60,6 +61,7 @@ export interface DashboardSceneState extends SceneObjectState {
export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
static Component = DashboardSceneRenderer;
private _isHomeDashboard = false;
/**
* Handles url sync
*/
@@ -250,7 +252,20 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
};
}
public set isHomeDashboard(value: boolean) {
this._isHomeDashboard = value;
}
canEditDashboard() {
return Boolean(this.state.meta.canEdit || this.state.meta.canMakeEditable);
const { meta } = this.state;
// Default home dash is not editable.
if (this._isHomeDashboard) {
return false;
}
return (
contextSrv.hasPermission(AccessControlAction.DashboardsWrite) && Boolean(meta.canEdit || meta.canMakeEditable)
);
}
}

View File

@@ -1,4 +1,4 @@
import { css } from '@emotion/css';
import { css, cx } from '@emotion/css';
import React from 'react';
import { useLocation } from 'react-router-dom';
@@ -17,12 +17,15 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
const pageNav = model.getPageNav(location);
const bodyToRender = model.getBodyToRender(viewPanelId);
const hasControls = model.canEditDashboard() && controls;
return (
<Page navId="scenes" pageNav={pageNav} layout={PageLayoutType.Custom}>
<CustomScrollbar autoHeightMin={'100%'}>
<div className={styles.canvasContent}>
<NavToolbarActions dashboard={model} />
{controls && (
{hasControls && (
<div className={styles.controls}>
{controls.map((control) => (
<control.Component key={control.state.key} model={control} />
@@ -30,7 +33,7 @@ export function DashboardSceneRenderer({ model }: SceneComponentProps<DashboardS
<SceneDebugger scene={model} key={'scene-debugger'} />
</div>
)}
<div className={styles.body}>
<div className={cx(styles.body, !hasControls && styles.bodyNoControls)}>
<bodyToRender.Component model={bodyToRender} />
</div>
</div>
@@ -57,6 +60,9 @@ function getStyles(theme: GrafanaTheme2) {
gap: '8px',
marginBottom: theme.spacing(2),
}),
bodyNoControls: css({
paddingTop: theme.spacing(2),
}),
controls: css({
display: 'flex',
flexWrap: 'wrap',

View File

@@ -79,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} />;

View File

@@ -35,19 +35,21 @@ export function panelMenuBehavior(menu: VizPanelMenu) {
href: locationUtil.getUrlForPartial(location, { viewPanel: panel.state.key }),
});
// 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: 'v',
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: 'v',
onClick: () => reportInteraction('dashboards_panelheader_menu', { item: 'edit' }),
href: getDashboardUrl({
uid: dashboard.state.uid,
subPath: `/panel-edit/${panelId}`,
currentQueryParams: location.search,
}),
});
}
items.push({
text: t('panel.header-menu.share', `Share`),

View File

@@ -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,

View File

@@ -0,0 +1,28 @@
import React from 'react';
import { config } from '@grafana/runtime';
import { contextSrv } from 'app/core/core';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import DashboardScenePage from 'app/features/dashboard-scene/pages/DashboardScenePage';
import { AccessControlAction } from 'app/types';
import DashboardPage from './DashboardPage';
import { DashboardPageRouteParams, DashboardPageRouteSearchParams } from './types';
type Props = GrafanaRouteComponentProps<DashboardPageRouteParams, DashboardPageRouteSearchParams>;
// This proxy component is used for Dashboard -> Scenes migration.
// It will render DashboardScenePage if user does not have write permissions to a dashboard.
function DashboardPageProxy(props: Props) {
if (config.featureToggles.dashboardSceneForViewers) {
if (contextSrv.hasPermission(AccessControlAction.DashboardsWrite)) {
return <DashboardPage {...props} />;
} else {
return <DashboardScenePage {...props} />;
}
}
return <DashboardPage {...props} />;
}
export default DashboardPageProxy;

View 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;
};

View File

@@ -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')
),
},
{