PublicDashboards: Scene migration (#84409)

This commit is contained in:
Juan Cabanas
2024-03-22 11:48:21 -03:00
committed by GitHub
parent a0bcc44b63
commit 8d4ca72f2a
19 changed files with 571 additions and 21 deletions

View File

@@ -108,6 +108,7 @@ Experimental features might be changed or removed without prior notice.
| ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `live-service-web-worker` | This will use a webworker thread to processes events rather than the main thread |
| `queryOverLive` | Use Grafana Live WebSocket to execute backend queries |
| `publicDashboardsScene` | Enables public dashboard rendering using scenes |
| `lokiExperimentalStreaming` | Support new streaming approach for loki (prototype, needs special loki build) |
| `storage` | Configurable storage for dashboards, datasources, and resources |
| `datasourceQueryMultiStatus` | Introduce HTTP 207 Multi Status for api/ds/query |

View File

@@ -24,6 +24,7 @@ export interface FeatureToggles {
panelTitleSearch?: boolean;
publicDashboards?: boolean;
publicDashboardsEmailSharing?: boolean;
publicDashboardsScene?: boolean;
lokiExperimentalStreaming?: boolean;
featureHighlights?: boolean;
migrationLocking?: boolean;

View File

@@ -265,6 +265,12 @@ export const Pages = {
title: 'public-dashboard-title',
pausedDescription: 'public-dashboard-paused-description',
},
footer: 'public-dashboard-footer',
},
PublicDashboardScene: {
loadingPage: 'public-dashboard-scene-loading-page',
page: 'public-dashboard-scene-page',
controls: 'public-dashboard-controls',
},
RequestViewAccess: {
form: 'request-view-access-form',

View File

@@ -63,6 +63,13 @@ var (
HideFromDocs: true,
HideFromAdminPage: true,
},
{
Name: "publicDashboardsScene",
Description: "Enables public dashboard rendering using scenes",
Stage: FeatureStageExperimental,
FrontendOnly: true,
Owner: grafanaSharingSquad,
},
{
Name: "lokiExperimentalStreaming",
Description: "Support new streaming approach for loki (prototype, needs special loki build)",

View File

@@ -5,6 +5,7 @@ queryOverLive,experimental,@grafana/grafana-app-platform-squad,false,false,true
panelTitleSearch,preview,@grafana/grafana-app-platform-squad,false,false,false
publicDashboards,GA,@grafana/sharing-squad,false,false,false
publicDashboardsEmailSharing,preview,@grafana/sharing-squad,false,false,false
publicDashboardsScene,experimental,@grafana/sharing-squad,false,false,true
lokiExperimentalStreaming,experimental,@grafana/observability-logs,false,false,false
featureHighlights,GA,@grafana/grafana-as-code,false,false,false
migrationLocking,preview,@grafana/backend-platform,false,false,false
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
5 panelTitleSearch preview @grafana/grafana-app-platform-squad false false false
6 publicDashboards GA @grafana/sharing-squad false false false
7 publicDashboardsEmailSharing preview @grafana/sharing-squad false false false
8 publicDashboardsScene experimental @grafana/sharing-squad false false true
9 lokiExperimentalStreaming experimental @grafana/observability-logs false false false
10 featureHighlights GA @grafana/grafana-as-code false false false
11 migrationLocking preview @grafana/backend-platform false false false

View File

@@ -31,6 +31,10 @@ const (
// Enables public dashboard sharing to be restricted to only allowed emails
FlagPublicDashboardsEmailSharing = "publicDashboardsEmailSharing"
// FlagPublicDashboardsScene
// Enables public dashboard rendering using scenes
FlagPublicDashboardsScene = "publicDashboardsScene"
// FlagLokiExperimentalStreaming
// Support new streaming approach for loki (prototype, needs special loki build)
FlagLokiExperimentalStreaming = "lokiExperimentalStreaming"

View File

@@ -2164,6 +2164,19 @@
"hideFromAdminPage": true,
"hideFromDocs": true
}
},
{
"metadata": {
"name": "publicDashboardsScene",
"resourceVersion": "1710951139684",
"creationTimestamp": "2024-03-20T16:12:19Z"
},
"spec": {
"description": "Enables public dashboard rendering using scenes",
"stage": "experimental",
"codeowner": "@grafana/sharing-squad",
"frontend": true
}
}
]
}

View File

@@ -1,5 +1,5 @@
import { locationUtil } from '@grafana/data';
import { getBackendSrv, isFetchError, locationService } from '@grafana/runtime';
import { config, 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';
@@ -78,6 +78,9 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
}
break;
case DashboardRoutes.Public: {
return await dashboardLoaderSrv.loadDashboard('public', '', uid);
}
default:
rsp = await dashboardLoaderSrv.loadDashboard('db', '', uid);
@@ -142,7 +145,9 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
public async loadDashboard(options: LoadDashboardOptions) {
try {
const dashboard = await this.loadScene(options);
dashboard.startUrlSync();
if (!(config.publicDashboardAccessToken && dashboard.state.controls?.state.hideTimeControls)) {
dashboard.startUrlSync();
}
this.setState({ dashboard: dashboard, isLoading: false });
} catch (err) {

View File

@@ -0,0 +1,243 @@
import { render, screen, waitForElementToBeRemoved } from '@testing-library/react';
import React from 'react';
import { of } from 'rxjs';
import { TestProvider } from 'test/helpers/TestProvider';
import { getGrafanaContextMock } from 'test/mocks/getGrafanaContextMock';
import { getDefaultTimeRange, LoadingState, PanelData, PanelProps } from '@grafana/data';
import { getPanelPlugin } from '@grafana/data/test/__mocks__/pluginMocks';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { config, getPluginLinkExtensions, setPluginImportUtils, setRunRequest } from '@grafana/runtime';
import { Dashboard } from '@grafana/schema';
import { getRouteComponentProps } from 'app/core/navigation/__mocks__/routeProps';
import { DashboardRoutes } from 'app/types/dashboard';
import { setupLoadDashboardMock } from '../utils/test-utils';
import { getDashboardScenePageStateManager } from './DashboardScenePageStateManager';
import { PublicDashboardScenePage, Props as PublicDashboardSceneProps } from './PublicDashboardScenePage';
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
setPluginExtensionGetter: jest.fn(),
getPluginLinkExtensions: jest.fn(),
getDataSourceSrv: () => {
return {
get: jest.fn().mockResolvedValue({}),
getInstanceSettings: jest.fn().mockResolvedValue({ uid: 'ds1' }),
};
},
}));
const getPluginLinkExtensionsMock = jest.mocked(getPluginLinkExtensions);
function setup(props?: Partial<PublicDashboardSceneProps>) {
const context = getGrafanaContextMock();
const pubdashProps: PublicDashboardSceneProps = {
...getRouteComponentProps({
match: { params: { accessToken: 'an-access-token' }, isExact: true, url: '', path: '' },
route: {
routeName: DashboardRoutes.Public,
path: '/public-dashboards/:accessToken',
component: () => null,
},
}),
...props,
};
return render(
<TestProvider grafanaContext={context}>
<PublicDashboardScenePage {...pubdashProps} />
</TestProvider>
);
}
const simpleDashboard: Dashboard = {
title: 'My cool dashboard',
uid: 'my-dash-uid',
schemaVersion: 30,
version: 1,
timepicker: { hidden: false },
panels: [
{
id: 1,
type: 'custom-viz-panel',
title: 'Panel A',
options: {
content: `Content A`,
},
gridPos: {
x: 0,
y: 0,
w: 10,
h: 10,
},
targets: [],
},
{
id: 2,
type: 'custom-viz-panel',
title: 'Panel B',
options: {
content: `Content B`,
},
gridPos: {
x: 0,
y: 10,
w: 10,
h: 10,
},
targets: [],
},
],
};
const panelPlugin = getPanelPlugin(
{
skipDataQuery: true,
},
CustomVizPanel
);
config.panels['custom-viz-panel'] = panelPlugin.meta;
setPluginImportUtils({
importPanelPlugin: (id: string) => Promise.resolve(panelPlugin),
getPanelPluginFromCache: (id: string) => undefined,
});
const runRequestMock = jest.fn().mockReturnValue(
of<PanelData>({
state: LoadingState.Done,
series: [],
timeRange: getDefaultTimeRange(),
annotations: [],
})
);
setRunRequest(runRequestMock);
const componentsSelector = e2eSelectors.components;
const publicDashboardSelector = e2eSelectors.pages.PublicDashboard;
const publicDashboardSceneSelector = e2eSelectors.pages.PublicDashboardScene;
describe('PublicDashboardScenePage', () => {
beforeEach(() => {
config.publicDashboardAccessToken = 'an-access-token';
getDashboardScenePageStateManager().clearDashboardCache();
setupLoadDashboardMock({ dashboard: simpleDashboard, meta: {} });
// // hacky way because mocking autosizer does not work
Object.defineProperty(HTMLElement.prototype, 'offsetHeight', { configurable: true, value: 1000 });
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { configurable: true, value: 1000 });
getPluginLinkExtensionsMock.mockRestore();
getPluginLinkExtensionsMock.mockReturnValue({ extensions: [] });
});
it('can render public dashboard', async () => {
setup();
await waitForDashboardGridToRender();
expect(await screen.findByTitle('Panel A')).toBeInTheDocument();
expect(await screen.findByText('Content A')).toBeInTheDocument();
expect(await screen.findByTitle('Panel B')).toBeInTheDocument();
expect(await screen.findByText('Content B')).toBeInTheDocument();
expect(await screen.findByTestId(publicDashboardSelector.footer)).toBeInTheDocument();
});
it('cannot see menu panel', async () => {
setup();
await waitForDashboardGridToRender();
expect(screen.queryByTestId(componentsSelector.Panels.Panel.menu('Panel A'))).not.toBeInTheDocument();
expect(screen.queryByTestId(componentsSelector.Panels.Panel.menu('Panel B'))).not.toBeInTheDocument();
});
it('shows time controls when it is not hidden', async () => {
setup();
await waitForDashboardGridToRender();
expect(screen.queryByTestId(componentsSelector.TimePicker.openButton)).toBeInTheDocument();
expect(screen.queryByTestId(componentsSelector.RefreshPicker.runButtonV2)).toBeInTheDocument();
expect(screen.queryByTestId(componentsSelector.RefreshPicker.intervalButtonV2)).toBeInTheDocument();
});
it('does not render paused or deleted screen', async () => {
setup();
await waitForDashboardGridToRender();
expect(screen.queryByTestId(publicDashboardSelector.NotAvailable.container)).not.toBeInTheDocument();
});
it('does not show time controls when it is hidden', async () => {
const accessToken = 'hidden-time-picker-pubdash-access-token';
config.publicDashboardAccessToken = accessToken;
setupLoadDashboardMock({
dashboard: { ...simpleDashboard, timepicker: { hidden: true } },
meta: {},
});
setup({
match: { params: { accessToken }, isExact: true, url: '', path: '' },
});
await waitForDashboardGridToRender();
expect(screen.queryByTestId(componentsSelector.TimePicker.openButton)).not.toBeInTheDocument();
expect(screen.queryByTestId(componentsSelector.RefreshPicker.runButtonV2)).not.toBeInTheDocument();
expect(screen.queryByTestId(componentsSelector.RefreshPicker.intervalButtonV2)).not.toBeInTheDocument();
});
});
describe('given unavailable public dashboard', () => {
it('renders public dashboard paused screen when it is paused', async () => {
const accessToken = 'paused-pubdash-access-token';
config.publicDashboardAccessToken = accessToken;
setupLoadDashboardMock({
dashboard: simpleDashboard,
meta: { publicDashboardEnabled: false, dashboardNotFound: false },
});
setup({ match: { params: { accessToken }, isExact: true, url: '', path: '' } });
await waitForElementToBeRemoved(screen.getByTestId(publicDashboardSceneSelector.loadingPage));
expect(screen.queryByTestId(publicDashboardSceneSelector.page)).not.toBeInTheDocument();
expect(screen.getByTestId(publicDashboardSelector.NotAvailable.title)).toBeInTheDocument();
expect(screen.getByTestId(publicDashboardSelector.NotAvailable.pausedDescription)).toBeInTheDocument();
});
it('renders public dashboard not available screen when it is deleted', async () => {
const accessToken = 'deleted-pubdash-access-token';
config.publicDashboardAccessToken = accessToken;
setupLoadDashboardMock({
dashboard: simpleDashboard,
meta: { dashboardNotFound: true },
});
setup({ match: { params: { accessToken }, isExact: true, url: '', path: '' } });
await waitForElementToBeRemoved(screen.getByTestId(publicDashboardSceneSelector.loadingPage));
expect(screen.queryByTestId(publicDashboardSelector.page)).not.toBeInTheDocument();
expect(screen.queryByTestId(publicDashboardSelector.NotAvailable.pausedDescription)).not.toBeInTheDocument();
expect(screen.getByTestId(publicDashboardSelector.NotAvailable.title)).toBeInTheDocument();
});
});
interface VizOptions {
content: string;
}
interface VizProps extends PanelProps<VizOptions> {}
function CustomVizPanel(props: VizProps) {
return <div>{props.options.content}</div>;
}
async function waitForDashboardGridToRender() {
expect(await screen.findByTitle('Panel A')).toBeInTheDocument();
expect(await screen.findByTitle('Panel B')).toBeInTheDocument();
}

View File

@@ -0,0 +1,145 @@
import { css } from '@emotion/css';
import React, { useEffect, useState } from 'react';
import { GrafanaTheme2, PageLayoutType } from '@grafana/data';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { SceneComponentProps } from '@grafana/scenes';
import { Icon, Stack, useStyles2 } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
import PageLoader from 'app/core/components/PageLoader/PageLoader';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { PublicDashboardFooter } from 'app/features/dashboard/components/PublicDashboard/PublicDashboardsFooter';
import { PublicDashboardNotAvailable } from 'app/features/dashboard/components/PublicDashboardNotAvailable/PublicDashboardNotAvailable';
import {
PublicDashboardPageRouteParams,
PublicDashboardPageRouteSearchParams,
} from 'app/features/dashboard/containers/types';
import { DashboardRoutes } from 'app/types/dashboard';
import { DashboardScene } from '../scene/DashboardScene';
import { getDashboardScenePageStateManager } from './DashboardScenePageStateManager';
export interface Props
extends GrafanaRouteComponentProps<PublicDashboardPageRouteParams, PublicDashboardPageRouteSearchParams> {}
const selectors = e2eSelectors.pages.PublicDashboardScene;
export function PublicDashboardScenePage({ match, route }: Props) {
const stateManager = getDashboardScenePageStateManager();
const styles = useStyles2(getStyles);
const { dashboard, isLoading, loadError } = stateManager.useState();
useEffect(() => {
stateManager.loadDashboard({ uid: match.params.accessToken!, route: DashboardRoutes.Public });
return () => {
stateManager.clearState();
};
}, [stateManager, match.params.accessToken, route.routeName]);
if (!dashboard) {
return (
<Page layout={PageLayoutType.Custom} className={styles.loadingPage} data-testid={selectors.loadingPage}>
{isLoading && <PageLoader />}
{loadError && <h2>{loadError}</h2>}
</Page>
);
}
if (dashboard.state.meta.publicDashboardEnabled === false) {
return <PublicDashboardNotAvailable paused />;
}
if (dashboard.state.meta.dashboardNotFound) {
return <PublicDashboardNotAvailable />;
}
return <PublicDashboardSceneRenderer model={dashboard} />;
}
function PublicDashboardSceneRenderer({ model }: SceneComponentProps<DashboardScene>) {
const [isActive, setIsActive] = useState(false);
const { controls, title } = model.useState();
const { timePicker, refreshPicker, hideTimeControls } = controls!.useState();
const bodyToRender = model.getBodyToRender();
const styles = useStyles2(getStyles);
useEffect(() => {
setIsActive(true);
return model.activate();
}, [model]);
if (!isActive) {
return null;
}
return (
<Page layout={PageLayoutType.Custom} className={styles.page} data-testid={selectors.page}>
<div className={styles.controls}>
<Stack alignItems="center">
<div className={styles.iconTitle}>
<Icon name="grafana" size="lg" aria-hidden />
</div>
<span className={styles.title}>{title}</span>
</Stack>
{!hideTimeControls && (
<Stack>
<timePicker.Component model={timePicker} />
<refreshPicker.Component model={refreshPicker} />
</Stack>
)}
</div>
<div className={styles.body}>
<bodyToRender.Component model={bodyToRender} />
</div>
<PublicDashboardFooter />
</Page>
);
}
function getStyles(theme: GrafanaTheme2) {
return {
loadingPage: css({
justifyContent: 'center',
}),
page: css({
padding: theme.spacing(0, 2),
}),
controls: css({
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
position: 'sticky',
top: 0,
zIndex: theme.zIndex.navbarFixed,
background: theme.colors.background.canvas,
padding: theme.spacing(2, 0),
[theme.breakpoints.down('sm')]: {
flexDirection: 'column',
gap: theme.spacing(1),
alignItems: 'stretch',
},
}),
iconTitle: css({
display: 'none',
[theme.breakpoints.up('sm')]: {
display: 'flex',
alignItems: 'center',
},
}),
title: css({
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
display: 'flex',
fontSize: theme.typography.h4.fontSize,
margin: 0,
}),
body: css({
label: 'body',
flex: 1,
marginBottom: theme.spacing(3),
}),
};
}

View File

@@ -181,7 +181,10 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
dashboardWatcher.watch(this.state.uid);
}
const clearKeyBindings = setupKeyboardShortcuts(this);
let clearKeyBindings = () => {};
if (!config.publicDashboardAccessToken) {
clearKeyBindings = setupKeyboardShortcuts(this);
}
const oldDashboardWrapper = new DashboardModelCompatibilityWrapper(this);
// @ts-expect-error

View File

@@ -511,15 +511,18 @@ export function buildGridItemForPanel(panel: PanelModel): DashboardGridItem {
// To be replaced with it's own option persited option instead derived
hoverHeader: !panel.title && !panel.timeFrom && !panel.timeShift,
$data: createPanelDataProvider(panel),
menu: new VizPanelMenu({
$behaviors: [panelMenuBehavior],
}),
titleItems,
extendPanelContext: setDashboardPanelContext,
_UNSAFE_customMigrationHandler: getAngularPanelMigrationHandler(panel),
};
if (!config.publicDashboardAccessToken) {
vizPanelState.menu = new VizPanelMenu({
$behaviors: [panelMenuBehavior],
});
}
if (panel.timeFrom || panel.timeShift) {
vizPanelState.$timeRange = new PanelTimeRange({
timeFrom: panel.timeFrom,

View File

@@ -2,16 +2,19 @@ import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { useStyles2 } from '@grafana/ui';
import { useGetPublicDashboardConfig } from './usePublicDashboardConfig';
const selectors = e2eSelectors.pages.PublicDashboard;
export const PublicDashboardFooter = function () {
const styles = useStyles2(getStyles);
const conf = useGetPublicDashboardConfig();
return conf.footerHide ? null : (
<div className={styles.footer}>
<div className={styles.footer} data-testid={selectors.footer}>
<a className={styles.link} href={conf.footerLink} target="_blank" rel="noreferrer noopener">
{conf.footerText} <img className={styles.logoImg} alt="" src={conf.footerLogo} />
</a>
@@ -24,7 +27,11 @@ const getStyles = (theme: GrafanaTheme2) => ({
display: 'flex',
justifyContent: 'end',
height: '30px',
padding: theme.spacing(0, 2, 0, 1),
backgroundColor: theme.colors.background.canvas,
position: 'sticky',
bottom: 0,
zIndex: theme.zIndex.navbarFixed,
padding: theme.spacing(0.5, 0),
}),
link: css({
display: 'flex',

View File

@@ -8,6 +8,10 @@ import { PageToolbar, useStyles2 } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
import { useGrafana } from 'app/core/context/GrafanaContext';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import {
PublicDashboardPageRouteParams,
PublicDashboardPageRouteSearchParams,
} from 'app/features/dashboard/containers/types';
import { updateTimeZoneForSession } from 'app/features/profile/state/reducers';
import { useSelector, useDispatch } from 'app/types';
@@ -22,16 +26,6 @@ import { getTimeSrv } from '../services/TimeSrv';
import { DashboardModel } from '../state';
import { initDashboard } from '../state/initDashboard';
interface PublicDashboardPageRouteParams {
accessToken?: string;
}
interface PublicDashboardPageRouteSearchParams {
from?: string;
to?: string;
refresh?: string;
}
export type Props = GrafanaRouteComponentProps<PublicDashboardPageRouteParams, PublicDashboardPageRouteSearchParams>;
const selectors = e2eSelectors.pages.PublicDashboard;
@@ -116,7 +110,9 @@ const PublicDashboardPage = (props: Props) => {
<div className={styles.gridContainer}>
<DashboardGrid dashboard={dashboard} isEditable={false} viewPanel={null} editPanel={null} hidePanelMenus />
</div>
<PublicDashboardFooter />
<div className={styles.footer}>
<PublicDashboardFooter />
</div>
</Page>
);
};
@@ -127,6 +123,9 @@ const getStyles = (theme: GrafanaTheme2) => ({
padding: theme.spacing(2, 2, 2, 2),
overflow: 'auto',
}),
footer: css({
padding: theme.spacing(0, 2),
}),
});
export default PublicDashboardPage;

View File

@@ -0,0 +1,75 @@
import { render, screen, 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 { selectors as e2eSelectors } from '@grafana/e2e-selectors';
import { config, locationService } from '@grafana/runtime';
import { GrafanaContext } from 'app/core/context/GrafanaContext';
import { configureStore } from 'app/store/configureStore';
import { DashboardRoutes } from '../../../types';
import PublicDashboardPageProxy, { PublicDashboardPageProxyProps } from './PublicDashboardPageProxy';
const { PublicDashboardScene, PublicDashboard } = e2eSelectors.pages;
jest.mock('@grafana/runtime', () => ({
...jest.requireActual('@grafana/runtime'),
getDataSourceSrv: jest.fn().mockReturnValue({
getInstanceSettings: () => {
return { name: 'Grafana' };
},
get: jest.fn().mockResolvedValue({}),
}),
}));
function setup(props: Partial<PublicDashboardPageProxyProps>) {
const context = getGrafanaContextMock();
const store = configureStore({});
return render(
<GrafanaContext.Provider value={context}>
<Provider store={store}>
<Router history={locationService.getHistory()}>
<PublicDashboardPageProxy
location={locationService.getLocation()}
history={locationService.getHistory()}
queryParams={{}}
route={{ routeName: DashboardRoutes.Public, component: () => null, path: '/:accessToken' }}
match={{ params: { accessToken: 'an-access-token' }, isExact: true, path: '/', url: '/' }}
{...props}
/>
</Router>
</Provider>
</GrafanaContext.Provider>
);
}
describe('PublicDashboardPageProxy', () => {
beforeEach(() => {
config.featureToggles.publicDashboardsScene = false;
});
describe('when scene feature enabled', () => {
it('should render PublicDashboardScenePage if publicDashboardsScene is enabled', async () => {
config.featureToggles.publicDashboardsScene = true;
setup({});
await waitFor(() => {
expect(screen.queryByTestId(PublicDashboardScene.page)).toBeInTheDocument();
});
});
});
describe('when scene feature disabled', () => {
it('should render PublicDashboardPage if publicDashboardsScene is disabled', async () => {
setup({});
await waitFor(() => {
expect(screen.queryByTestId(PublicDashboard.page)).toBeInTheDocument();
});
});
});
});

View File

@@ -0,0 +1,24 @@
import React from 'react';
import { config } from '@grafana/runtime';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { PublicDashboardScenePage } from '../../dashboard-scene/pages/PublicDashboardScenePage';
import PublicDashboardPage from './PublicDashboardPage';
import { PublicDashboardPageRouteParams, PublicDashboardPageRouteSearchParams } from './types';
export type PublicDashboardPageProxyProps = GrafanaRouteComponentProps<
PublicDashboardPageRouteParams,
PublicDashboardPageRouteSearchParams
>;
function PublicDashboardPageProxy(props: PublicDashboardPageProxyProps) {
if (config.featureToggles.publicDashboardsScene) {
return <PublicDashboardScenePage {...props} />;
}
return <PublicDashboardPage {...props} />;
}
export default PublicDashboardPageProxy;

View File

@@ -21,3 +21,14 @@ export type DashboardPageRouteSearchParams = {
scenes?: boolean;
shareView?: string;
};
export type PublicDashboardPageRouteParams = {
accessToken?: string;
};
export type PublicDashboardPageRouteSearchParams = {
from?: string;
to?: string;
refresh?: string;
scenes?: boolean;
};

View File

@@ -28,7 +28,7 @@ export const getPublicDashboardRoutes = (): RouteDescriptor[] => {
component: SafeDynamicImport(
() =>
import(
/* webpackChunkName: "PublicDashboardPage" */ '../../features/dashboard/containers/PublicDashboardPage'
/* webpackChunkName: "PublicDashboardPage" */ '../../features/dashboard/containers/PublicDashboardPageProxy'
)
),
},

View File

@@ -249,7 +249,9 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult<void> {
dashboard.autoFitPanels(window.innerHeight, queryParams.kiosk);
}
args.keybindingSrv.setupDashboardBindings(dashboard);
if (!config.publicDashboardAccessToken) {
args.keybindingSrv.setupDashboardBindings(dashboard);
}
} catch (err) {
if (err instanceof Error) {
dispatch(notifyApp(createErrorNotification('Dashboard init failed', err)));