From 97c0ff2ae467452aa2ea1619b6e014bc005e09fa Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Fri, 25 Oct 2024 15:56:54 +0300 Subject: [PATCH] Dashboards: Reload the dashboard based on time range and filters changes (#94190) --- .../src/types/featureToggles.gen.ts | 2 +- pkg/services/featuremgmt/registry.go | 4 +- pkg/services/featuremgmt/toggles_gen.csv | 2 +- pkg/services/featuremgmt/toggles_gen.go | 6 +- pkg/services/featuremgmt/toggles_gen.json | 19 ++++- .../DashboardScenePageStateManager.test.ts | 2 +- .../pages/DashboardScenePageStateManager.ts | 85 +++++++++++++------ .../scene/DashboardReloadBehavior.ts | 79 +++++++++++++++++ .../scene/DashboardScopesFacade.ts | 9 +- .../transformSaveModelToScene.ts | 10 ++- .../features/dashboard/api/dashboard_api.ts | 11 +-- .../dashboard/services/DashboardLoaderSrv.ts | 19 +++-- .../scopes/tests/dashboardReload.test.ts | 78 +++++++++++++++-- .../scopes/tests/dashboardsApi.test.ts | 48 ----------- .../features/scopes/tests/utils/actions.ts | 31 +++++-- .../features/scopes/tests/utils/assertions.ts | 11 +-- .../app/features/scopes/tests/utils/mocks.ts | 70 +++++++-------- .../features/scopes/tests/utils/render.tsx | 44 ++++++++++ public/app/types/dashboard.ts | 4 +- 19 files changed, 372 insertions(+), 162 deletions(-) create mode 100644 public/app/features/dashboard-scene/scene/DashboardReloadBehavior.ts delete mode 100644 public/app/features/scopes/tests/dashboardsApi.test.ts diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index a0673322b10..6aea6f88b69 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -198,7 +198,7 @@ export interface FeatureToggles { ssoSettingsLDAP?: boolean; failWrongDSUID?: boolean; zanzana?: boolean; - passScopeToDashboardApi?: boolean; + reloadDashboardsOnParamsChange?: boolean; alertingApiServer?: boolean; cloudWatchRoundUpEndTime?: boolean; cloudwatchMetricInsightsCrossAccount?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 5de1807b66a..a238f51db5f 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -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, diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 99efc0e76cd..291ad8e6b80 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -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 diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 9b24818435b..0e95c9173cf 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -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 diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index 65906e21cb7..220b06d2e8c 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -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 @@ } } ] -} \ No newline at end of file +} diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts index 124bc7b27e8..b23e6884b0b 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.test.ts @@ -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 }); diff --git a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts index b954cdd3ecc..7c8d9399940 100644 --- a/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts +++ b/public/app/features/dashboard-scene/pages/DashboardScenePageStateManager.ts @@ -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 { @@ -52,12 +55,20 @@ export class DashboardScenePageStateManager extends StateManagerBase { + public async fetchDashboard({ + uid, + route, + urlFolderUid, + queryParams, + }: LoadDashboardOptions): Promise { 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 { this.setState({ dashboard: undefined, isLoading: true }); @@ -209,7 +261,6 @@ export class DashboardScenePageStateManager extends StateManagerBase { + 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( + (acc, variable) => ({ + ...acc, + ...variable.urlSync?.getUrlState(), + }), + params + ); + + getDashboardScenePageStateManager().reloadDashboard(params); + } + } +} diff --git a/public/app/features/dashboard-scene/scene/DashboardScopesFacade.ts b/public/app/features/dashboard-scene/scene/DashboardScopesFacade.ts index 0b6267cde58..93b81e735ce 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScopesFacade.ts +++ b/public/app/features/dashboard-scene/scene/DashboardScopesFacade.ts @@ -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(); } }, diff --git a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts index f16d3f3b6db..cb8ab241061 100644 --- a/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts +++ b/public/app/features/dashboard-scene/serialization/transformSaveModelToScene.ts @@ -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({ diff --git a/public/app/features/dashboard/api/dashboard_api.ts b/public/app/features/dashboard/api/dashboard_api.ts index 88ca1d3b01c..84c6f2e98f3 100644 --- a/public/app/features/dashboard/api/dashboard_api.ts +++ b/public/app/features/dashboard/api/dashboard_api.ts @@ -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; + getDashboardDTO(uid: string, params?: UrlQueryMap): Promise; /** Save dashboard */ saveDashboard(options: SaveDashboardCommand): Promise; /** Delete a dashboard */ @@ -41,11 +41,8 @@ class LegacyDashboardAPI implements DashboardAPI { return getBackendSrv().delete(`/api/dashboards/uid/${uid}`, { showSuccessAlert }); } - getDashboardDTO(uid: string): Promise { - const scopes = config.featureToggles.passScopeToDashboardApi ? getSelectedScopesNames() : []; - const queryParams = scopes.length > 0 ? { scopes } : undefined; - - return getBackendSrv().get(`/api/dashboards/uid/${uid}`, queryParams); + getDashboardDTO(uid: string, params?: UrlQueryMap): Promise { + return getBackendSrv().get(`/api/dashboards/uid/${uid}`, params); } } diff --git a/public/app/features/dashboard/services/DashboardLoaderSrv.ts b/public/app/features/dashboard/services/DashboardLoaderSrv.ts index ba06819bc6c..0037fcb0e8e 100644 --- a/public/app/features/dashboard/services/DashboardLoaderSrv.ts +++ b/public/app/features/dashboard/services/DashboardLoaderSrv.ts @@ -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 { + loadDashboard( + type: UrlQueryValue, + slug: string | undefined, + uid: string | undefined, + params?: UrlQueryMap + ): Promise { 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']); diff --git a/public/app/features/scopes/tests/dashboardReload.test.ts b/public/app/features/scopes/tests/dashboardReload.test.ts index 161dcf58283..84856e82aec 100644 --- a/public/app/features/scopes/tests/dashboardReload.test.ts +++ b/public/app/features/scopes/tests/dashboardReload.test.ts @@ -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)); + }); + }); }); }); diff --git a/public/app/features/scopes/tests/dashboardsApi.test.ts b/public/app/features/scopes/tests/dashboardsApi.test.ts deleted file mode 100644 index f9af59eb92f..00000000000 --- a/public/app/features/scopes/tests/dashboardsApi.test.ts +++ /dev/null @@ -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)); -}); diff --git a/public/app/features/scopes/tests/utils/actions.ts b/public/app/features/scopes/tests/utils/actions.ts index 77ae7fb0086..9781fee2b6d 100644 --- a/public/app/features/scopes/tests/utils/actions.ts +++ b/public/app/features/scopes/tests/utils/actions.ts @@ -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); diff --git a/public/app/features/scopes/tests/utils/assertions.ts b/public/app/features/scopes/tests/utils/assertions.ts index cfd54c4ad78..eefc65dbade 100644 --- a/public/app/features/scopes/tests/utils/assertions.ts +++ b/public/app/features/scopes/tests/utils/assertions.ts @@ -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); diff --git a/public/app/features/scopes/tests/utils/mocks.ts b/public/app/features/scopes/tests/utils/mocks.ts index 924729c07cc..0ac0a785237 100644 --- a/public/app/features/scopes/tests/utils/mocks.ts +++ b/public/app/features/scopes/tests/utils/mocks.ts @@ -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) => { + 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('/apis/dashboard.grafana.app/v0alpha1/namespaces/default/dashboards/')) { + return { + metadata: { + name: '1', + }, + }; + } - if (url.startsWith('/api/dashboards/uid/')) { return {}; } - - if (url.startsWith('/apis/dashboard.grafana.app/v0alpha1/namespaces/default/dashboards/')) { - return { - metadata: { - name: '1', - }, - }; - } - - return {}; - }); + ); const generateScopeDashboardBinding = (dashboardTitle: string, groups?: string[], dashboardId?: string) => ({ metadata: { name: `${dashboardTitle}-name` }, diff --git a/public/app/features/scopes/tests/utils/render.tsx b/public/app/features/scopes/tests/utils/render.tsx index 71d60ff6492..32ff0010d48 100644 --- a/public/app/features/scopes/tests/utils/render.tsx +++ b/public/app/features/scopes/tests/utils/render.tsx @@ -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: [ diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index c6713fa0865..396642594a9 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -74,9 +74,9 @@ export interface DashboardMeta { // until we use the resource as the main container k8s?: Partial; - // 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 {