mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Dashboards: Reload the dashboard based on time range and filters changes (#94190)
This commit is contained in:
@@ -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
|
||||
|
||||
|
@@ -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",
|
||||
|
||||
@@ -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('/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` },
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user