Code Diff
diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts
index e8110cdd08c8e..6e15eaea033b6 100644
--- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts
+++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts
@@ -544,6 +544,41 @@ describe('DashboardScenePageStateManager v1', () => {
expect(loader.getSceneFromCache('fake-dash').state.title).toBe('Dashboard 2');
});
+ it('public dashboard: should build scene from API and not reuse or keep stale scene cache', () => {
+ const loader = new DashboardScenePageStateManager({});
+ const accessToken = 'public-access-token';
+ const staleScene = new DashboardScene(
+ {
+ title: 'Stale cached title',
+ uid: 'underlying-dash-uid',
+ meta: { created: 'same-created' },
+ version: 2,
+ },
+ 'v1'
+ );
+
+ loader.setSceneCache(accessToken, staleScene);
+
+ const rsp: DashboardDTO = {
+ meta: { created: 'same-created' },
+ dashboard: {
+ uid: 'underlying-dash-uid',
+ title: 'Fresh from API',
+ schemaVersion: 38,
+ version: 2,
+ } as DashboardDataDTO,
+ };
+
+ const scene = loader.transformResponseToScene(rsp, {
+ uid: accessToken,
+ route: DashboardRoutes.Public,
+ });
+
+ expect(rsp.dashboard?.title).toBe('Fresh from API');
+ expect(scene).not.toBe(staleScene);
+ expect(scene?.state.title).toBe('Fresh from API');
+ });
+
it('should take scene from cache if it exists', async () => {
setupLoadDashboardMock({ dashboard: { uid: 'fake-dash', version: 10 }, meta: {} });
@@ -763,6 +798,45 @@ describe('DashboardScenePageStateManager v2', () => {
expect(getDashSpy).toHaveBeenCalledTimes(1);
});
+ it('public dashboard: should build scene from API and not reuse or keep stale scene cache', () => {
+ const loader = new DashboardScenePageStateManagerV2({});
+ const accessToken = 'public-access-token';
+ const generation = 4;
+ const staleScene = new DashboardScene(
+ {
+ title: 'Stale cached title',
+ uid: 'dash-uid',
+ meta: {},
+ version: generation,
+ },
+ 'v2'
+ );
+
+ loader.setSceneCache(accessToken, staleScene);
+
+ const rsp: DashboardWithAccessInfo<DashboardV2Spec> = {
+ access: {},
+ apiVersion: 'v2beta1',
+ kind: 'DashboardWithAccessInfo',
+ metadata: {
+ name: 'dash',
+ creationTimestamp: '',
+ resourceVersion: '1',
+ generation,
+ },
+ spec: { ...defaultDashboardV2Spec(), title: 'Fresh from API' },
+ };
+
+ const scene = loader.transformResponseToScene(rsp, {
+ uid: accessToken,
+ route: DashboardRoutes.Public,
+ });
+
+ expect(rsp.spec?.title).toBe('Fresh from API');
+ expect(scene).not.toBe(staleScene);
+ expect(scene?.state.title).toBe('Fresh from API');
+ });
+
it('should register report render readiness observer for render-authenticated normal route', async () => {
const getDashSpy = jest.fn();
setupDashboardAPI(
diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts
index f21be346eaddd..27d0e4e855f2f 100644
--- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts
+++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts
@@ -522,19 +522,24 @@ abstract class DashboardScenePageStateManagerBase<T>
export class DashboardScenePageStateManager extends DashboardScenePageStateManagerBase<DashboardDTO> {
transformResponseToScene(rsp: DashboardDTO | null, options: LoadDashboardOptions): DashboardScene | null {
- const fromCache = this.getSceneFromCache(options.uid);
+ // Public dashboards are not part of a session and therefore should not use the cache
+ const skipSceneCache = options.route === DashboardRoutes.Public;
- if (
- fromCache &&
- fromCache.state.version === rsp?.dashboard.version &&
- fromCache.state.meta.created === rsp?.meta.created
- ) {
- const profiler = getDashboardSceneProfiler();
- profiler.setMetadata({
- dashboardUID: fromCache.state.uid,
- dashboardTitle: fromCache.state.title,
- });
- return fromCache;
+ if (!skipSceneCache) {
+ const fromCache = this.getSceneFromCache(options.uid);
+
+ if (
+ fromCache &&
+ fromCache.state.version === rsp?.dashboard.version &&
+ fromCache.state.meta.created === rsp?.meta.created
+ ) {
+ const profiler = getDashboardSceneProfiler();
+ profiler.setMetadata({
+ dashboardUID: fromCache.state.uid,
+ dashboardTitle: fromCache.state.title,
+ });
+ return fromCache;
+ }
}
if (rsp?.dashboard) {
@@ -553,8 +558,8 @@ export class DashboardScenePageStateManager extends DashboardScenePageStateManag
scene.setState({ isDirty: true });
}
- // Cache scene only if not coming from Explore, we don't want to cache temporary dashboard
- if (options.uid) {
+ // We don't want to cache temporary dashboards (e.g from Explore) or public dashboards
+ if (options.uid && !skipSceneCache) {
this.setSceneCache(options.uid, scene);
}
@@ -918,22 +923,27 @@ export class DashboardScenePageStateManagerV2 extends DashboardScenePageStateMan
rsp: DashboardWithAccessInfo<DashboardV2Spec> | null,
options: LoadDashboardOptions
): DashboardScene | null {
- const fromCache = this.getSceneFromCache(options.uid);
+ // Public dashboards are not part of a session and therefore should not use the cache
+ const skipSceneCache = options.route === DashboardRoutes.Public;
- if (fromCache && fromCache.state.version === rsp?.metadata.generation) {
- const profiler = getDashboardSceneProfiler();
- profiler.setMetadata({
- dashboardUID: fromCache.state.uid,
- dashboardTitle: fromCache.state.title,
- });
- return fromCache;
+ if (!skipSceneCache) {
+ const fromCache = this.getSceneFromCache(options.uid);
+
+ if (fromCache && fromCache.state.version === rsp?.metadata.generation) {
+ const profiler = getDashboardSceneProfiler();
+ profiler.setMetadata({
+ dashboardUID: fromCache.state.uid,
+ dashboardTitle: fromCache.state.title,
+ });
+ return fromCache;
+ }
}
if (rsp) {
const scene = transformSaveModelSchemaV2ToScene(rsp, options);
- // Cache scene only if not coming from Explore, we don't want to cache temporary dashboard
- if (options.uid) {
+ // We don't want to cache temporary dashboards (e.g from Explore) or public dashboards
+ if (options.uid && !skipSceneCache) {
this.setSceneCache(options.uid, scene);
}
diff --git a/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.test.tsx b/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.test.tsx
index 3de00b2fadfea..46ead1f28579f 100644
--- a/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.test.tsx
+++ b/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.test.tsx
@@ -1,4 +1,4 @@
-import { screen, waitForElementToBeRemoved } from '@testing-library/react';
+import { screen, waitFor, waitForElementToBeRemoved } from '@testing-library/react';
import { Route, Routes } from 'react-router-dom-v5-compat';
import { of } from 'rxjs';
import { render } from 'test/test-utils';
@@ -131,6 +131,19 @@ describe('PublicDashboardScenePage', () => {
Object.defineProperty(HTMLElement.prototype, 'offsetWidth', { configurable: true, value: 1000 });
});
+ it('syncs config.publicDashboardAccessToken from the URL and restores the previous value on unmount', async () => {
+ config.publicDashboardAccessToken = 'token-before-navigation';
+ const view = setup('token-from-route');
+
+ await waitFor(() => {
+ expect(config.publicDashboardAccessToken).toBe('token-from-route');
+ });
+
+ view.unmount();
+
+ expect(config.publicDashboardAccessToken).toBe('token-before-navigation');
+ });
+
it('can render public dashboard', async () => {
setup();
diff --git a/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.tsx b/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.tsx
index 4996f7f88cb32..81000e48f7925 100644
--- a/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.tsx
+++ b/public/app/features/dashboard-scene/pages/PublicDashboardScenePage.tsx
@@ -4,6 +4,7 @@ import { useParams } from 'react-router-dom-v5-compat';
import { type GrafanaTheme2, PageLayoutType } from '@grafana/data';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
+import { config } from '@grafana/runtime';
import { type SceneComponentProps, UrlSyncContextProvider } from '@grafana/scenes';
import { Alert, Box, Icon, Stack, useStyles2 } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
@@ -37,10 +38,17 @@ export function PublicDashboardScenePage({ route }: Props) {
const { dashboard, isLoading, loadError } = stateManager.useState();
useEffect(() => {
+ // Full page loads set this via boot data, but client-side navigation must set it here or panel queries will not use /api/public/dashboards/{token}/...
+ const previousToken = config.publicDashboardAccessToken;
+ if (accessToken) {
+ config.publicDashboardAccessToken = accessToken;
+ }
+
stateManager.loadDashboard({ uid: accessToken, route: DashboardRoutes.Public });
return () => {
stateManager.clearState();
+ config.publicDashboardAccessToken = previousToken;
};
}, [stateManager, accessToken, route.routeName]);
diff --git a/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx b/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx
index 2c24e926c9a9d..93a04d4438152 100644
--- a/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx
+++ b/public/app/features/dashboard/containers/PublicDashboardPageProxy.test.tsx
@@ -1,9 +1,11 @@
import { screen, waitFor } from '@testing-library/react';
import { Routes, Route } from 'react-router-dom-v5-compat';
+import { of } from 'rxjs';
import { render } from 'test/test-utils';
+import { getDefaultTimeRange, LoadingState, type PanelData } from '@grafana/data';
import { selectors as e2eSelectors } from '@grafana/e2e-selectors';
-import { locationService } from '@grafana/runtime';
+import { locationService, setRunRequest } from '@grafana/runtime';
import { backendSrv } from 'app/core/services/backend_srv';
import { type DashboardDTO, DashboardRoutes } from 'app/types/dashboard';
@@ -26,6 +28,16 @@ jest.mock('react-router-dom-v5-compat', () => ({
useParams: () => ({ accessToken: 'an-access-token' }),
}));
+const runRequestMock = jest.fn().mockReturnValue(
+ of<PanelData>({
+ state: LoadingState.Done,
+ series: [],
+ timeRange: getDefaultTimeRange(),
+ annotations: [],
+ })
+);
+setRunRequest(runRequestMock);
+
function setup(props: Partial<PublicDashboardPageProxyProps>) {
return render(
<Routes>