Dashboards: Reload the dashboard based on time range and filters changes (#94190)

pull/95407/head
Bogdan Matei 8 months ago committed by GitHub
parent bdab0bc8c0
commit 97c0ff2ae4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      packages/grafana-data/src/types/featureToggles.gen.ts
  2. 4
      pkg/services/featuremgmt/registry.go
  3. 2
      pkg/services/featuremgmt/toggles_gen.csv
  4. 6
      pkg/services/featuremgmt/toggles_gen.go
  5. 19
      pkg/services/featuremgmt/toggles_gen.json
  6. 2
      public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts
  7. 85
      public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts
  8. 79
      public/app/features/dashboard-scene/scene/DashboardReloadBehavior.ts
  9. 9
      public/app/features/dashboard-scene/scene/DashboardScopesFacade.ts
  10. 10
      public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts
  11. 11
      public/app/features/dashboard/api/dashboard_api.ts
  12. 19
      public/app/features/dashboard/services/DashboardLoaderSrv.ts
  13. 78
      public/app/features/scopes/tests/dashboardReload.test.ts
  14. 48
      public/app/features/scopes/tests/dashboardsApi.test.ts
  15. 31
      public/app/features/scopes/tests/utils/actions.ts
  16. 11
      public/app/features/scopes/tests/utils/assertions.ts
  17. 70
      public/app/features/scopes/tests/utils/mocks.ts
  18. 44
      public/app/features/scopes/tests/utils/render.tsx
  19. 4
      public/app/types/dashboard.ts

@ -198,7 +198,7 @@ export interface FeatureToggles {
ssoSettingsLDAP?: boolean;
failWrongDSUID?: boolean;
zanzana?: boolean;
passScopeToDashboardApi?: boolean;
reloadDashboardsOnParamsChange?: boolean;
alertingApiServer?: boolean;
cloudWatchRoundUpEndTime?: boolean;
cloudwatchMetricInsightsCrossAccount?: boolean;

@ -1363,8 +1363,8 @@ var (
HideFromAdminPage: true,
},
{
Name: "passScopeToDashboardApi",
Description: "Enables the passing of scopes to dashboards fetching in Grafana",
Name: "reloadDashboardsOnParamsChange",
Description: "Enables reload of dashboards on scopes, time range and variables changes",
FrontendOnly: false,
Stage: FeatureStageExperimental,
Owner: grafanaDashboardsSquad,

@ -179,7 +179,7 @@ openSearchBackendFlowEnabled,GA,@grafana/aws-datasources,false,false,false
ssoSettingsLDAP,preview,@grafana/identity-access-team,false,true,false
failWrongDSUID,experimental,@grafana/plugins-platform-backend,false,false,false
zanzana,experimental,@grafana/identity-access-team,false,false,false
passScopeToDashboardApi,experimental,@grafana/dashboards-squad,false,false,false
reloadDashboardsOnParamsChange,experimental,@grafana/dashboards-squad,false,false,false
alertingApiServer,experimental,@grafana/alerting-squad,false,true,false
cloudWatchRoundUpEndTime,GA,@grafana/aws-datasources,false,false,false
cloudwatchMetricInsightsCrossAccount,preview,@grafana/aws-datasources,false,false,true

1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
179 ssoSettingsLDAP preview @grafana/identity-access-team false true false
180 failWrongDSUID experimental @grafana/plugins-platform-backend false false false
181 zanzana experimental @grafana/identity-access-team false false false
182 passScopeToDashboardApi reloadDashboardsOnParamsChange experimental @grafana/dashboards-squad false false false
183 alertingApiServer experimental @grafana/alerting-squad false true false
184 cloudWatchRoundUpEndTime GA @grafana/aws-datasources false false false
185 cloudwatchMetricInsightsCrossAccount preview @grafana/aws-datasources false false true

@ -727,9 +727,9 @@ const (
// Use openFGA as authorization engine.
FlagZanzana = "zanzana"
// FlagPassScopeToDashboardApi
// Enables the passing of scopes to dashboards fetching in Grafana
FlagPassScopeToDashboardApi = "passScopeToDashboardApi"
// FlagReloadDashboardsOnParamsChange
// Enables reload of dashboards on scopes, time range and variables changes
FlagReloadDashboardsOnParamsChange = "reloadDashboardsOnParamsChange"
// FlagAlertingApiServer
// Register Alerting APIs with the K8s API server

@ -2347,11 +2347,26 @@
"requiresDevMode": true
}
},
{
"metadata": {
"name": "reloadDashboardsOnParamsChange",
"resourceVersion": "1728903221522",
"creationTimestamp": "2024-10-14T10:53:41Z"
},
"spec": {
"description": "Enables reload of dashboards on scopes, time range and variables changes",
"stage": "experimental",
"codeowner": "@grafana/dashboards-squad",
"hideFromAdminPage": true,
"hideFromDocs": true
}
},
{
"metadata": {
"name": "passScopeToDashboardApi",
"resourceVersion": "1718290335877",
"creationTimestamp": "2024-06-20T15:49:19Z"
"creationTimestamp": "2024-06-20T15:49:19Z",
"deletionTimestamp": "2024-10-14T10:53:41Z"
},
"spec": {
"description": "Enables the passing of scopes to dashboards fetching in Grafana",
@ -3305,4 +3320,4 @@
}
}
]
}
}

@ -21,7 +21,7 @@ describe('DashboardScenePageStateManager', () => {
const loader = new DashboardScenePageStateManager({});
await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal });
expect(loadDashboardMock).toHaveBeenCalledWith('db', '', 'fake-dash');
expect(loadDashboardMock).toHaveBeenCalledWith('db', '', 'fake-dash', undefined);
// should use cache second time
await loader.loadDashboard({ uid: 'fake-dash', route: DashboardRoutes.Normal });

@ -1,4 +1,6 @@
import { locationUtil } from '@grafana/data';
import { isEqual } from 'lodash';
import { locationUtil, UrlQueryMap } from '@grafana/data';
import { config, getBackendSrv, isFetchError, locationService } from '@grafana/runtime';
import { StateManagerBase } from 'app/core/services/StateManagerBase';
import { getMessageFromError } from 'app/core/utils/errors';
@ -7,7 +9,6 @@ import { dashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoa
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
import { emitDashboardViewEvent } from 'app/features/dashboard/state/analyticsProcessor';
import { trackDashboardSceneLoaded } from 'app/features/dashboard/utils/tracking';
import { getSelectedScopesNames } from 'app/features/scopes';
import { DashboardDTO, DashboardRoutes } from 'app/types';
import { PanelEditor } from '../panel-edit/PanelEditor';
@ -20,6 +21,7 @@ import { updateNavModel } from './utils';
export interface DashboardScenePageState {
dashboard?: DashboardScene;
options?: LoadDashboardOptions;
panelEditor?: PanelEditor;
isLoading?: boolean;
loadError?: string;
@ -42,6 +44,7 @@ export interface LoadDashboardOptions {
uid: string;
route: DashboardRoutes;
urlFolderUid?: string;
queryParams?: UrlQueryMap;
}
export class DashboardScenePageStateManager extends StateManagerBase<DashboardScenePageState> {
@ -52,12 +55,20 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
// To eventualy replace the fetchDashboard function from Dashboard redux state management.
// For now it's a simplistic version to support Home and Normal dashboard routes.
public async fetchDashboard({ uid, route, urlFolderUid }: LoadDashboardOptions): Promise<DashboardDTO | null> {
public async fetchDashboard({
uid,
route,
urlFolderUid,
queryParams,
}: LoadDashboardOptions): Promise<DashboardDTO | null> {
const cacheKey = route === DashboardRoutes.Home ? HOME_DASHBOARD_CACHE_KEY : uid;
const cachedDashboard = this.getDashboardFromCache(cacheKey);
if (cachedDashboard) {
return cachedDashboard;
if (!queryParams) {
const cachedDashboard = this.getDashboardFromCache(cacheKey);
if (cachedDashboard) {
return cachedDashboard;
}
}
let rsp: DashboardDTO;
@ -86,7 +97,7 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
return await dashboardLoaderSrv.loadDashboard('public', '', uid);
}
default:
rsp = await dashboardLoaderSrv.loadDashboard('db', '', uid);
rsp = await dashboardLoaderSrv.loadDashboard('db', '', uid, queryParams);
if (route === DashboardRoutes.Embedded) {
rsp.meta.isEmbedded = true;
@ -159,7 +170,7 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
restoreDashboardStateFromLocalStorage(dashboard);
}
this.setState({ dashboard: dashboard, isLoading: false });
this.setState({ dashboard: dashboard, isLoading: false, options });
const measure = stopMeasure(LOAD_SCENE_MEASUREMENT);
trackDashboardSceneLoaded(dashboard, measure?.duration);
@ -177,6 +188,47 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
}
}
public async reloadDashboard(queryParams?: LoadDashboardOptions['queryParams'] | undefined) {
if (!this.state.options) {
return;
}
const options = {
...this.state.options,
queryParams,
};
if (isEqual(options, this.state.options)) {
return;
}
try {
this.setState({ isLoading: true });
const rsp = await this.fetchDashboard(options);
const fromCache = this.getSceneFromCache(options.uid);
if (fromCache && fromCache.state.version === rsp?.dashboard.version) {
this.setState({ isLoading: false });
return;
}
if (!rsp?.dashboard) {
this.setState({ isLoading: false, loadError: 'Dashboard not found' });
return;
}
const scene = transformSaveModelToScene(rsp);
this.setSceneCache(options.uid, scene);
this.setState({ dashboard: scene, isLoading: false, options });
} catch (err) {
const msg = getMessageFromError(err);
this.setState({ isLoading: false, loadError: msg });
}
}
private async loadScene(options: LoadDashboardOptions): Promise<DashboardScene | null> {
this.setState({ dashboard: undefined, isLoading: true });
@ -209,7 +261,6 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
public getDashboardFromCache(cacheKey: string) {
const cachedDashboard = this.dashboardCache;
cacheKey = this.getCacheKey(cacheKey);
if (
cachedDashboard &&
@ -234,8 +285,6 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
}
public setDashboardCache(cacheKey: string, dashboard: DashboardDTO) {
cacheKey = this.getCacheKey(cacheKey);
this.dashboardCache = { dashboard, ts: Date.now(), cacheKey };
}
@ -244,30 +293,16 @@ export class DashboardScenePageStateManager extends StateManagerBase<DashboardSc
}
public getSceneFromCache(cacheKey: string) {
cacheKey = this.getCacheKey(cacheKey);
return this.cache[cacheKey];
}
public setSceneCache(cacheKey: string, scene: DashboardScene) {
cacheKey = this.getCacheKey(cacheKey);
this.cache[cacheKey] = scene;
}
public clearSceneCache() {
this.cache = {};
}
public getCacheKey(cacheKey: string): string {
const scopesCacheKey = getSelectedScopesNames().sort().join('__scp__');
if (!scopesCacheKey) {
return cacheKey;
}
return `${cacheKey}__scp__${scopesCacheKey}`;
}
}
let stateManager: DashboardScenePageStateManager | null = null;

@ -0,0 +1,79 @@
import { isEqual } from 'lodash';
import { UrlQueryMap } from '@grafana/data';
import { sceneGraph, SceneObjectBase, SceneObjectState, VariableDependencyConfig } from '@grafana/scenes';
import { getClosestScopesFacade, ScopesFacade } from 'app/features/scopes';
import { getDashboardScenePageStateManager } from '../pages/DashboardScenePageStateManager';
export interface DashboardReloadBehaviorState extends SceneObjectState {
reloadOnParamsChange?: boolean;
uid?: string;
version?: number;
}
export class DashboardReloadBehavior extends SceneObjectBase<DashboardReloadBehaviorState> {
private _scopesFacade: ScopesFacade | null = null;
constructor(state: DashboardReloadBehaviorState) {
const shouldReload = state.reloadOnParamsChange && state.uid;
super(state);
this.reloadDashboard = this.reloadDashboard.bind(this);
if (shouldReload) {
this.addActivationHandler(() => {
this._scopesFacade = getClosestScopesFacade(this);
this._variableDependency = new VariableDependencyConfig(this, {
onAnyVariableChanged: this.reloadDashboard,
});
this._scopesFacade?.setState({
handler: this.reloadDashboard,
});
this._subs.add(
sceneGraph.getTimeRange(this).subscribeToState((newState, prevState) => {
if (!isEqual(newState.value, prevState.value)) {
this.reloadDashboard();
}
})
);
});
}
}
private isEditing() {
return this.parent && 'isEditing' in this.parent.state && this.parent.state.isEditing;
}
private isWaitingForVariables() {
const varSet = sceneGraph.getVariables(this.parent!);
return varSet.state.variables.some((variable) => varSet.isVariableLoadingOrWaitingToUpdate(variable));
}
private reloadDashboard() {
if (!this.isEditing() && !this.isWaitingForVariables()) {
const timeRange = sceneGraph.getTimeRange(this);
let params: UrlQueryMap = {
version: this.state.version,
scopes: this._scopesFacade?.value.map((scope) => scope.metadata.name),
...timeRange.urlSync?.getUrlState(),
};
params = sceneGraph.getVariables(this).state.variables.reduce<UrlQueryMap>(
(acc, variable) => ({
...acc,
...variable.urlSync?.getUrlState(),
}),
params
);
getDashboardScenePageStateManager().reloadDashboard(params);
}
}
}

@ -1,19 +1,16 @@
import { locationService } from '@grafana/runtime';
import { sceneGraph } from '@grafana/scenes';
import { ScopesFacade } from 'app/features/scopes';
export interface DashboardScopesFacadeState {
reloadOnScopesChange?: boolean;
reloadOnParamsChange?: boolean;
uid?: string;
}
export class DashboardScopesFacade extends ScopesFacade {
constructor({ reloadOnScopesChange, uid }: DashboardScopesFacadeState) {
constructor({ reloadOnParamsChange, uid }: DashboardScopesFacadeState) {
super({
handler: (facade) => {
if (reloadOnScopesChange && uid) {
locationService.reload();
} else {
if (!reloadOnParamsChange || !uid) {
sceneGraph.getTimeRange(facade).onRefresh();
}
},

@ -30,6 +30,7 @@ import { DashboardControls } from '../scene/DashboardControls';
import { DashboardDataLayerSet } from '../scene/DashboardDataLayerSet';
import { DashboardGridItem, RepeatDirection } from '../scene/DashboardGridItem';
import { registerDashboardMacro } from '../scene/DashboardMacro';
import { DashboardReloadBehavior } from '../scene/DashboardReloadBehavior';
import { DashboardScene } from '../scene/DashboardScene';
import { DashboardScopesFacade } from '../scene/DashboardScopesFacade';
import { LibraryPanelBehavior } from '../scene/LibraryPanelBehavior';
@ -251,9 +252,16 @@ export function createDashboardSceneFromDashboardModel(oldModel: DashboardModel,
preserveDashboardSceneStateInLocalStorage,
addPanelsOnLoadBehavior,
new DashboardScopesFacade({
reloadOnScopesChange: oldModel.meta.reloadOnScopesChange,
reloadOnParamsChange:
config.featureToggles.reloadDashboardsOnParamsChange && oldModel.meta.reloadOnParamsChange,
uid: oldModel.uid,
}),
new DashboardReloadBehavior({
reloadOnParamsChange:
config.featureToggles.reloadDashboardsOnParamsChange && oldModel.meta.reloadOnParamsChange,
uid: oldModel.uid,
version: oldModel.version,
}),
],
$data: new DashboardDataLayerSet({ annotationLayers, alertStatesLayer }),
controls: new DashboardControls({

@ -1,3 +1,4 @@
import { UrlQueryMap } from '@grafana/data';
import { config, getBackendSrv } from '@grafana/runtime';
import { ScopedResourceClient } from 'app/features/apiserver/client';
import {
@ -10,12 +11,11 @@ import {
import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types';
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
import { DeleteDashboardResponse } from 'app/features/manage-dashboards/types';
import { getSelectedScopesNames } from 'app/features/scopes';
import { DashboardDTO, DashboardDataDTO, SaveDashboardResponseDTO } from 'app/types';
export interface DashboardAPI {
/** Get a dashboard with the access control metadata */
getDashboardDTO(uid: string): Promise<DashboardDTO>;
getDashboardDTO(uid: string, params?: UrlQueryMap): Promise<DashboardDTO>;
/** Save dashboard */
saveDashboard(options: SaveDashboardCommand): Promise<SaveDashboardResponseDTO>;
/** Delete a dashboard */
@ -41,11 +41,8 @@ class LegacyDashboardAPI implements DashboardAPI {
return getBackendSrv().delete<DeleteDashboardResponse>(`/api/dashboards/uid/${uid}`, { showSuccessAlert });
}
getDashboardDTO(uid: string): Promise<DashboardDTO> {
const scopes = config.featureToggles.passScopeToDashboardApi ? getSelectedScopesNames() : [];
const queryParams = scopes.length > 0 ? { scopes } : undefined;
return getBackendSrv().get<DashboardDTO>(`/api/dashboards/uid/${uid}`, queryParams);
getDashboardDTO(uid: string, params?: UrlQueryMap): Promise<DashboardDTO> {
return getBackendSrv().get<DashboardDTO>(`/api/dashboards/uid/${uid}`, params);
}
}

@ -2,7 +2,7 @@ import $ from 'jquery';
import _, { isFunction } from 'lodash'; // eslint-disable-line lodash/import-scope
import moment from 'moment'; // eslint-disable-line no-restricted-imports
import { AppEvents, dateMath, UrlQueryValue } from '@grafana/data';
import { AppEvents, dateMath, UrlQueryMap, UrlQueryValue } from '@grafana/data';
import { getBackendSrv, locationService } from '@grafana/runtime';
import { backendSrv } from 'app/core/services/backend_srv';
import impressionSrv from 'app/core/services/impression_srv';
@ -35,7 +35,12 @@ export class DashboardLoaderSrv {
};
}
loadDashboard(type: UrlQueryValue, slug: string | undefined, uid: string | undefined): Promise<DashboardDTO> {
loadDashboard(
type: UrlQueryValue,
slug: string | undefined,
uid: string | undefined,
params?: UrlQueryMap
): Promise<DashboardDTO> {
const stateManager = getDashboardScenePageStateManager();
let promise;
@ -77,13 +82,15 @@ export class DashboardLoaderSrv {
};
});
} else if (uid) {
const cachedDashboard = stateManager.getDashboardFromCache(uid);
if (cachedDashboard) {
return Promise.resolve(cachedDashboard);
if (!params) {
const cachedDashboard = stateManager.getDashboardFromCache(uid);
if (cachedDashboard) {
return Promise.resolve(cachedDashboard);
}
}
promise = getDashboardAPI()
.getDashboardDTO(uid)
.getDashboardDTO(uid, params)
.then((result) => {
if (result.meta.isFolder) {
appEvents.emit(AppEvents.alertError, ['Dashboard not found']);

@ -1,6 +1,7 @@
import { config } from '@grafana/runtime';
import { setDashboardAPI } from 'app/features/dashboard/api/dashboard_api';
import { updateScopes } from './utils/actions';
import { enterEditMode, updateMyVar, updateScopes, updateTimeRange } from './utils/actions';
import { expectDashboardReload, expectNotDashboardReload } from './utils/assertions';
import { getDatasource, getInstanceSettings, getMock } from './utils/mocks';
import { renderDashboard, resetScenes } from './utils/render';
@ -14,6 +15,45 @@ jest.mock('@grafana/runtime', () => ({
usePluginLinks: jest.fn().mockReturnValue({ links: [] }),
}));
const runTest = async (
reloadDashboardsOnParamsChange: boolean,
reloadOnParamsChange: boolean,
withUid: boolean,
editMode: boolean
) => {
config.featureToggles.reloadDashboardsOnParamsChange = reloadDashboardsOnParamsChange;
setDashboardAPI(undefined);
const uid = 'dash-1';
const dashboardScene = renderDashboard({ uid: withUid ? uid : undefined }, { reloadOnParamsChange });
if (editMode) {
await enterEditMode(dashboardScene);
}
const shouldReload = reloadDashboardsOnParamsChange && reloadOnParamsChange && withUid && !editMode;
await updateTimeRange(dashboardScene);
if (!shouldReload) {
expectNotDashboardReload();
} else {
expectDashboardReload();
}
await updateMyVar(dashboardScene, '2');
if (!shouldReload) {
expectNotDashboardReload();
} else {
expectDashboardReload();
}
await updateScopes(['grafana']);
if (!shouldReload) {
expectNotDashboardReload();
} else {
expectDashboardReload();
}
};
describe('Dashboard reload', () => {
beforeAll(() => {
config.featureToggles.scopeFilters = true;
@ -21,18 +61,38 @@ describe('Dashboard reload', () => {
});
afterEach(async () => {
setDashboardAPI(undefined);
await resetScenes();
});
it('Does not reload the dashboard without UID', async () => {
renderDashboard({ uid: undefined }, { reloadOnScopesChange: true });
await updateScopes(['grafana']);
expectNotDashboardReload();
describe('reloadDashboardsOnParamsChange off', () => {
describe('reloadOnParamsChange off', () => {
it('with UID - no reload', () => runTest(false, false, true, false));
it('without UID - no reload', () => runTest(false, false, false, false));
});
describe('reloadOnParamsChange on', () => {
it('with UID - no reload', () => runTest(false, true, true, false));
it('without UID - no reload', () => runTest(false, true, false, false));
});
});
it('Reloads the dashboard with UID', async () => {
renderDashboard({}, { reloadOnScopesChange: true });
await updateScopes(['grafana']);
expectDashboardReload();
describe('reloadDashboardsOnParamsChange on', () => {
describe('reloadOnParamsChange off', () => {
it('with UID - no reload', () => runTest(true, false, true, false));
it('without UID - no reload', () => runTest(true, false, false, false));
});
describe('reloadOnParamsChange on', () => {
describe('edit mode on', () => {
it('with UID - no reload', () => runTest(true, true, true, true));
it('without UID - no reload', () => runTest(true, true, false, true));
});
describe('edit mode off', () => {
it('with UID - reload', () => runTest(true, true, true, false));
it('without UID - no reload', () => runTest(true, true, false, false));
});
});
});
});

@ -1,48 +0,0 @@
import { config } from '@grafana/runtime';
import { setDashboardAPI } from 'app/features/dashboard/api/dashboard_api';
import { getDashboardDTO, updateScopes } from './utils/actions';
import { expectNewDashboardDTO, expectOldDashboardDTO } from './utils/assertions';
import { getDatasource, getInstanceSettings, getMock } from './utils/mocks';
import { renderDashboard, resetScenes } from './utils/render';
jest.mock('@grafana/runtime', () => ({
__esModule: true,
...jest.requireActual('@grafana/runtime'),
useChromeHeaderHeight: jest.fn(),
getBackendSrv: () => ({ get: getMock }),
getDataSourceSrv: () => ({ get: getDatasource, getInstanceSettings }),
usePluginLinks: jest.fn().mockReturnValue({ links: [] }),
}));
const runTest = async (passScopes: boolean, kubernetesApi: boolean) => {
config.featureToggles.scopeFilters = true;
config.featureToggles.passScopeToDashboardApi = passScopes;
config.featureToggles.kubernetesDashboards = kubernetesApi;
setDashboardAPI(undefined);
renderDashboard({}, { reloadOnScopesChange: true });
await updateScopes(['grafana', 'mimir']);
await getDashboardDTO();
if (kubernetesApi) {
return expectNewDashboardDTO();
}
if (passScopes) {
return expectOldDashboardDTO(['grafana', 'mimir']);
}
return expectOldDashboardDTO();
};
describe('Dashboards API', () => {
afterEach(async () => {
setDashboardAPI(undefined);
await resetScenes();
});
it('Legacy API should not pass the scopes with feature flag off', async () => runTest(false, false));
it('K8s API should not pass the scopes with feature flag off', async () => runTest(false, true));
it('Legacy API should pass the scopes with feature flag on', async () => runTest(true, false));
it('K8s API should not pass the scopes with feature flag on', async () => runTest(true, true));
});

@ -1,17 +1,19 @@
import { act, fireEvent } from '@testing-library/react';
import { getDashboardAPI, setDashboardAPI } from 'app/features/dashboard/api/dashboard_api';
import { DateTime, makeTimeRange, dateMath } from '@grafana/data';
import { MultiValueVariable, sceneGraph, VariableValue } from '@grafana/scenes';
import { defaultTimeZone, TimeZone } from '@grafana/schema';
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
import { scopesSelectorScene } from '../../instance';
import {
dashboardReloadSpy,
fetchDashboardsSpy,
fetchNodesSpy,
fetchScopeSpy,
fetchSelectedScopesSpy,
getMock,
locationReloadSpy,
} from './mocks';
import {
getDashboardFolderExpand,
@ -40,7 +42,7 @@ export const clearMocks = () => {
fetchScopeSpy.mockClear();
fetchSelectedScopesSpy.mockClear();
fetchDashboardsSpy.mockClear();
locationReloadSpy.mockClear();
dashboardReloadSpy.mockClear();
getMock.mockClear();
};
@ -87,7 +89,24 @@ export const expandDashboardFolder = (folder: string) => click(() => getDashboar
export const enterEditMode = async (dashboardScene: DashboardScene) =>
act(async () => dashboardScene.onEnterEditMode());
export const getDashboardDTO = async () => {
setDashboardAPI(undefined);
await getDashboardAPI().getDashboardDTO('1');
export const updateTimeRange = async (
dashboardScene: DashboardScene,
from: DateTime | string = 'now-6h',
to: DateTime | string = 'now',
timeZone: TimeZone = defaultTimeZone
) =>
act(async () =>
sceneGraph
.getTimeRange(dashboardScene)
.onTimeRangeChange(makeTimeRange(dateMath.parse(from, false, timeZone)!, dateMath.parse(to, false, timeZone)!))
);
export const updateVariable = async (dashboardScene: DashboardScene, name: string, value: VariableValue) => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const variable = sceneGraph.lookupVariable(name, dashboardScene) as MultiValueVariable;
return act(async () => variable.changeValueTo(value));
};
export const updateMyVar = async (dashboardScene: DashboardScene, value: '1' | '2') =>
updateVariable(dashboardScene, 'myVar', value);

@ -1,4 +1,4 @@
import { getMock, locationReloadSpy } from './mocks';
import { dashboardReloadSpy } from './mocks';
import {
getDashboard,
getDashboardsContainer,
@ -77,13 +77,8 @@ export const expectDashboardNotInDocument = (uid: string) => expectNotInDocument
export const expectDashboardLength = (uid: string, length: number) =>
expect(queryAllDashboard(uid)).toHaveLength(length);
export const expectNotDashboardReload = () => expect(locationReloadSpy).not.toHaveBeenCalled();
export const expectDashboardReload = () => expect(locationReloadSpy).toHaveBeenCalled();
export const expectOldDashboardDTO = (scopes?: string[]) =>
expect(getMock).toHaveBeenCalledWith('/api/dashboards/uid/1', scopes ? { scopes } : undefined);
export const expectNewDashboardDTO = () =>
expect(getMock).toHaveBeenCalledWith('/apis/dashboard.grafana.app/v0alpha1/namespaces/default/dashboards/1/dto');
export const expectNotDashboardReload = () => expect(dashboardReloadSpy).not.toHaveBeenCalled();
export const expectDashboardReload = () => expect(dashboardReloadSpy).toHaveBeenCalled();
export const expectSelectedScopePath = (name: string, path: string[] | undefined) =>
expect(getSelectedScope(name)?.path).toEqual(path);

@ -1,6 +1,6 @@
import { Scope, ScopeDashboardBinding, ScopeNode } from '@grafana/data';
import { locationService } from '@grafana/runtime';
import { DataSourceRef } from '@grafana/schema/dist/esm/common/common.gen';
import { getDashboardScenePageStateManager } from 'app/features/dashboard-scene/pages/DashboardScenePageStateManager';
import * as api from '../../internal/api';
@ -373,48 +373,50 @@ export const fetchNodesSpy = jest.spyOn(api, 'fetchNodes');
export const fetchScopeSpy = jest.spyOn(api, 'fetchScope');
export const fetchSelectedScopesSpy = jest.spyOn(api, 'fetchSelectedScopes');
export const fetchDashboardsSpy = jest.spyOn(api, 'fetchDashboards');
export const locationReloadSpy = jest.spyOn(locationService, 'reload');
export const dashboardReloadSpy = jest.spyOn(getDashboardScenePageStateManager(), 'reloadDashboard');
export const getMock = jest
.fn()
.mockImplementation((url: string, params: { parent: string; scope: string[]; query?: string }) => {
if (url.startsWith('/apis/scope.grafana.app/v0alpha1/namespaces/default/find/scope_node_children')) {
return {
items: mocksNodes.filter(
({ parent, spec: { title } }) =>
parent === params.parent && title.toLowerCase().includes((params.query ?? '').toLowerCase())
),
};
}
.mockImplementation(
(url: string, params: { parent: string; scope: string[]; query?: string } & Record<string, string | string[]>) => {
if (url.startsWith('/apis/scope.grafana.app/v0alpha1/namespaces/default/find/scope_node_children')) {
return {
items: mocksNodes.filter(
({ parent, spec: { title } }) =>
parent === params.parent && title.toLowerCase().includes((params.query ?? '').toLowerCase())
),
};
}
if (url.startsWith('/apis/scope.grafana.app/v0alpha1/namespaces/default/scopes/')) {
const name = url.replace('/apis/scope.grafana.app/v0alpha1/namespaces/default/scopes/', '');
if (url.startsWith('/apis/scope.grafana.app/v0alpha1/namespaces/default/scopes/')) {
const name = url.replace('/apis/scope.grafana.app/v0alpha1/namespaces/default/scopes/', '');
return mocksScopes.find((scope) => scope.metadata.name.toLowerCase() === name.toLowerCase()) ?? {};
}
return mocksScopes.find((scope) => scope.metadata.name.toLowerCase() === name.toLowerCase()) ?? {};
}
if (url.startsWith('/apis/scope.grafana.app/v0alpha1/namespaces/default/find/scope_dashboard_bindings')) {
return {
items: mocksScopeDashboardBindings.filter(({ spec: { scope: bindingScope } }) =>
params.scope.includes(bindingScope)
),
};
}
if (url.startsWith('/apis/scope.grafana.app/v0alpha1/namespaces/default/find/scope_dashboard_bindings')) {
return {
items: mocksScopeDashboardBindings.filter(({ spec: { scope: bindingScope } }) =>
params.scope.includes(bindingScope)
),
};
}
if (url.startsWith('/api/dashboards/uid/')) {
return {};
}
if (url.startsWith('/api/dashboards/uid/')) {
return {};
}
if (url.startsWith('/apis/dashboard.grafana.app/v0alpha1/namespaces/default/dashboards/')) {
return {
metadata: {
name: '1',
},
};
}
if (url.startsWith('/apis/dashboard.grafana.app/v0alpha1/namespaces/default/dashboards/')) {
return {
metadata: {
name: '1',
},
};
}
return {};
});
return {};
}
);
const generateScopeDashboardBinding = (dashboardTitle: string, groups?: string[], dashboardId?: string) => ({
metadata: { name: `${dashboardTitle}-name` },

@ -49,6 +49,50 @@ const getDashboardDTO: (
name: 'groupBy',
type: 'groupby',
},
{
current: {
text: ['1'],
value: ['1'],
},
multi: true,
name: 'myVar',
options: [
{
selected: true,
text: '1',
value: '1',
},
{
selected: false,
text: '2',
value: '2',
},
],
query: '1, 2',
type: 'custom',
},
{
current: {
text: ['1'],
value: ['1'],
},
multi: true,
name: 'myVar2',
options: [
{
selected: true,
text: '1',
value: '1',
},
{
selected: false,
text: '2',
value: '2',
},
],
query: '1, 2',
type: 'custom',
},
],
},
panels: [

@ -74,9 +74,9 @@ export interface DashboardMeta {
// until we use the resource as the main container
k8s?: Partial<ObjectMeta>;
// This is a property added specifically for edge cases where dashboards should be reloaded on scopes changes
// This is a property added specifically for edge cases where dashboards should be reloaded on scopes, time range or variables changes
// This property is not persisted in the DB but its existence is controlled by the API
reloadOnScopesChange?: boolean;
reloadOnParamsChange?: boolean;
}
export interface AnnotationActions {

Loading…
Cancel
Save