From 8d4ca72f2a0e66c446d58d8bf13fadbc988fce11 Mon Sep 17 00:00:00 2001 From: Juan Cabanas Date: Fri, 22 Mar 2024 11:48:21 -0300 Subject: [PATCH] PublicDashboards: Scene migration (#84409) --- .../feature-toggles/index.md | 1 + .../src/types/featureToggles.gen.ts | 1 + .../src/selectors/pages.ts | 6 + pkg/services/featuremgmt/registry.go | 7 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + pkg/services/featuremgmt/toggles_gen.json | 13 + .../pages/DashboardScenePageStateManager.ts | 9 +- .../pages/PublicDashboardScenePage.test.tsx | 243 ++++++++++++++++++ .../pages/PublicDashboardScenePage.tsx | 145 +++++++++++ .../dashboard-scene/scene/DashboardScene.tsx | 5 +- .../transformSaveModelToScene.ts | 9 +- .../PublicDashboardsFooter.tsx | 11 +- .../containers/PublicDashboardPage.tsx | 21 +- .../PublicDashboardPageProxy.test.tsx | 75 ++++++ .../containers/PublicDashboardPageProxy.tsx | 24 ++ .../features/dashboard/containers/types.ts | 11 + public/app/features/dashboard/routes.ts | 2 +- .../features/dashboard/state/initDashboard.ts | 4 +- 19 files changed, 571 insertions(+), 21 deletions(-) create mode 100644 public/app/features/dashboard-scene/pages/PublicDashboardScenePage.test.tsx create mode 100644 public/app/features/dashboard-scene/pages/PublicDashboardScenePage.tsx create mode 100644 public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx create mode 100644 public/app/features/dashboard/containers/PublicDashboardPageProxy.tsx diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 540cba2af06..35081c71992 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -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 | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index fc29cd63dc4..6aceeb5f066 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -24,6 +24,7 @@ export interface FeatureToggles { panelTitleSearch?: boolean; publicDashboards?: boolean; publicDashboardsEmailSharing?: boolean; + publicDashboardsScene?: boolean; lokiExperimentalStreaming?: boolean; featureHighlights?: boolean; migrationLocking?: boolean; diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index 7b820ee07fc..f40a8709f8e 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -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', diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index e31c61a3b2f..f526d249cf5 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -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)", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 5787afcccf8..bf0a27769c2 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -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 diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index a92e4e8a6ea..7ec1fcd6166 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -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" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 75faf7053a1..68cde1b4819 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -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 + } } ] } \ No newline at end of file diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index 8b79f18fb4d..6abb169ee8e 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -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 ({ + ...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) { + 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( + + + + ); +} + +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({ + 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 {} + +function CustomVizPanel(props: VizProps) { + return
{props.options.content}
; +} + +async function waitForDashboardGridToRender() { + expect(await screen.findByTitle('Panel A')).toBeInTheDocument(); + expect(await screen.findByTitle('Panel B')).toBeInTheDocument(); +} diff --git a/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.tsx b/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.tsx new file mode 100644 index 00000000000..f6c44a0f036 --- /dev/null +++ b/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.tsx @@ -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 {} + +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 ( + + {isLoading && } + {loadError &&

{loadError}

} +
+ ); + } + + if (dashboard.state.meta.publicDashboardEnabled === false) { + return ; + } + + if (dashboard.state.meta.dashboardNotFound) { + return ; + } + + return ; +} + +function PublicDashboardSceneRenderer({ model }: SceneComponentProps) { + 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 ( + +
+ +
+ +
+ {title} +
+ {!hideTimeControls && ( + + + + + )} +
+
+ +
+ +
+ ); +} + +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), + }), + }; +} diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index a3ad85e0913..1e786eeceec 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -181,7 +181,10 @@ export class DashboardScene extends SceneObjectBase { 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 diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index ae68c88821f..60553ea9188 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -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, diff --git a/public/app/features/dashboard/components/PublicDashboard/PublicDashboardsFooter.tsx b/public/app/features/dashboard/components/PublicDashboard/PublicDashboardsFooter.tsx index c2b9f72dd9f..8bfb8ba3469 100644 --- a/public/app/features/dashboard/components/PublicDashboard/PublicDashboardsFooter.tsx +++ b/public/app/features/dashboard/components/PublicDashboard/PublicDashboardsFooter.tsx @@ -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 : ( -
+
{conf.footerText} @@ -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', diff --git a/public/app/features/dashboard/containers/PublicDashboardPage.tsx b/public/app/features/dashboard/containers/PublicDashboardPage.tsx index a4af9556c90..ffdacffcbfd 100644 --- a/public/app/features/dashboard/containers/PublicDashboardPage.tsx +++ b/public/app/features/dashboard/containers/PublicDashboardPage.tsx @@ -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; const selectors = e2eSelectors.pages.PublicDashboard; @@ -116,7 +110,9 @@ const PublicDashboardPage = (props: Props) => {
- +
+ +
); }; @@ -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; diff --git a/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx b/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx new file mode 100644 index 00000000000..897c5df9678 --- /dev/null +++ b/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx @@ -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) { + const context = getGrafanaContextMock(); + const store = configureStore({}); + + return render( + + + + null, path: '/:accessToken' }} + match={{ params: { accessToken: 'an-access-token' }, isExact: true, path: '/', url: '/' }} + {...props} + /> + + + + ); +} + +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(); + }); + }); + }); +}); diff --git a/public/app/features/dashboard/containers/PublicDashboardPageProxy.tsx b/public/app/features/dashboard/containers/PublicDashboardPageProxy.tsx new file mode 100644 index 00000000000..f7820295318 --- /dev/null +++ b/public/app/features/dashboard/containers/PublicDashboardPageProxy.tsx @@ -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 ; + } + + return ; +} + +export default PublicDashboardPageProxy; diff --git a/public/app/features/dashboard/containers/types.ts b/public/app/features/dashboard/containers/types.ts index 35c8411516b..4c2b3278eab 100644 --- a/public/app/features/dashboard/containers/types.ts +++ b/public/app/features/dashboard/containers/types.ts @@ -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; +}; diff --git a/public/app/features/dashboard/routes.ts b/public/app/features/dashboard/routes.ts index 1ee8a7a88b4..127d57b118e 100644 --- a/public/app/features/dashboard/routes.ts +++ b/public/app/features/dashboard/routes.ts @@ -28,7 +28,7 @@ export const getPublicDashboardRoutes = (): RouteDescriptor[] => { component: SafeDynamicImport( () => import( - /* webpackChunkName: "PublicDashboardPage" */ '../../features/dashboard/containers/PublicDashboardPage' + /* webpackChunkName: "PublicDashboardPage" */ '../../features/dashboard/containers/PublicDashboardPageProxy' ) ), }, diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 6d1e0c70312..712e7ec5967 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -249,7 +249,9 @@ export function initDashboard(args: InitDashboardArgs): ThunkResult { 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)));