mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
PublicDashboards: Scene migration (#84409)
This commit is contained in:
@@ -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 |
|
||||
|
||||
@@ -24,6 +24,7 @@ export interface FeatureToggles {
|
||||
panelTitleSearch?: boolean;
|
||||
publicDashboards?: boolean;
|
||||
publicDashboardsEmailSharing?: boolean;
|
||||
publicDashboardsScene?: boolean;
|
||||
lokiExperimentalStreaming?: boolean;
|
||||
featureHighlights?: boolean;
|
||||
migrationLocking?: boolean;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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"
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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),
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -28,7 +28,7 @@ export const getPublicDashboardRoutes = (): RouteDescriptor[] => {
|
||||
component: SafeDynamicImport(
|
||||
() =>
|
||||
import(
|
||||
/* webpackChunkName: "PublicDashboardPage" */ '../../features/dashboard/containers/PublicDashboardPage'
|
||||
/* webpackChunkName: "PublicDashboardPage" */ '../../features/dashboard/containers/PublicDashboardPageProxy'
|
||||
)
|
||||
),
|
||||
},
|
||||
|
||||
@@ -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)));
|
||||
|
||||
Reference in New Issue
Block a user