mirror of
https://github.com/grafana/grafana.git
synced 2025-07-31 04:22:13 +08:00
DashList: Update links with time range and variables change (#77850)
* user essentials mob! 🔱
lastFile:public/app/plugins/panel/dashlist/DashList.tsx
* DashList: Update variables in URL when they change
Co-authored-by: eledobleefe <laura.fernandez@grafana.com>
Co-authored-by: Joao Silva <joao.silva@grafana.com>
Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
* add e2e test to check dashlist variables are always correctly passed
* add comment with link to issue
* Alternative solution
* Minor tweaks
* revert accidental change
* fix
* New solution
* Fix test
* refinement
* Added unit test
* Update devenv/dev-dashboards/panel-dashlist/dashlist.json
Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
---------
Co-authored-by: Joao Silva <joao.silva@grafana.com>
Co-authored-by: joshhunt <josh@trtr.co>
Co-authored-by: eledobleefe <laura.fernandez@grafana.com>
Co-authored-by: Ashley Harrison <ashley.harrison@grafana.com>
This commit is contained in:
86
devenv/dev-dashboards/panel-dashlist/dashlist.json
Normal file
86
devenv/dev-dashboards/panel-dashlist/dashlist.json
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
{
|
||||||
|
"annotations": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"builtIn": 1,
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana",
|
||||||
|
"uid": "-- Grafana --"
|
||||||
|
},
|
||||||
|
"enable": true,
|
||||||
|
"hide": true,
|
||||||
|
"iconColor": "rgba(0, 211, 255, 1)",
|
||||||
|
"name": "Annotations & Alerts",
|
||||||
|
"type": "dashboard"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"editable": true,
|
||||||
|
"fiscalYearStartMonth": 0,
|
||||||
|
"graphTooltip": 0,
|
||||||
|
"id": null,
|
||||||
|
"links": [],
|
||||||
|
"liveNow": false,
|
||||||
|
"panels": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "grafana-testdata-datasource",
|
||||||
|
"uid": "PD8C576611E62080A"
|
||||||
|
},
|
||||||
|
"gridPos": {
|
||||||
|
"h": 12,
|
||||||
|
"w": 12,
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
},
|
||||||
|
"id": 2,
|
||||||
|
"options": {
|
||||||
|
"folderUID": "",
|
||||||
|
"includeVars": true,
|
||||||
|
"keepTime": false,
|
||||||
|
"maxItems": 10,
|
||||||
|
"query": "",
|
||||||
|
"showHeadings": true,
|
||||||
|
"showRecentlyViewed": true,
|
||||||
|
"showSearch": false,
|
||||||
|
"showStarred": false,
|
||||||
|
"tags": []
|
||||||
|
},
|
||||||
|
"pluginVersion": "10.3.0-pre",
|
||||||
|
"title": "Include time range and variables enabled",
|
||||||
|
"type": "dashlist"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"refresh": "",
|
||||||
|
"schemaVersion": 39,
|
||||||
|
"tags": ["gdev", "panel-tests"],
|
||||||
|
"templating": {
|
||||||
|
"list": [
|
||||||
|
{
|
||||||
|
"current": {
|
||||||
|
"selected": true,
|
||||||
|
"text": "A",
|
||||||
|
"value": "A"
|
||||||
|
},
|
||||||
|
"hide": 0,
|
||||||
|
"includeAll": false,
|
||||||
|
"multi": true,
|
||||||
|
"name": "server",
|
||||||
|
"query": "A,B,C,D",
|
||||||
|
"queryValue": "",
|
||||||
|
"skipUrlSync": false,
|
||||||
|
"type": "custom"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"time": {
|
||||||
|
"from": "now-6h",
|
||||||
|
"to": "now"
|
||||||
|
},
|
||||||
|
"timepicker": {},
|
||||||
|
"timezone": "",
|
||||||
|
"title": "Panel Tests - DashList",
|
||||||
|
"uid": "a6801696-cc53-4196-b1f9-2403e3909185",
|
||||||
|
"version": 1,
|
||||||
|
"weekStart": ""
|
||||||
|
}
|
@ -149,6 +149,13 @@ local dashboard = grafana.dashboard;
|
|||||||
id: 0,
|
id: 0,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
dashboard.new('dashlist', import '../dev-dashboards/panel-dashlist/dashlist.json') +
|
||||||
|
resource.addMetadata('folder', 'dev-dashboards') +
|
||||||
|
{
|
||||||
|
spec+: {
|
||||||
|
id: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
dashboard.new('datadata-macros', import '../dev-dashboards/feature-templating/datadata-macros.json') +
|
dashboard.new('datadata-macros', import '../dev-dashboards/feature-templating/datadata-macros.json') +
|
||||||
resource.addMetadata('folder', 'dev-dashboards') +
|
resource.addMetadata('folder', 'dev-dashboards') +
|
||||||
{
|
{
|
||||||
|
37
e2e/panels-suite/dashlist.spec.ts
Normal file
37
e2e/panels-suite/dashlist.spec.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { e2e } from '../utils';
|
||||||
|
const PAGE_UNDER_TEST = 'a6801696-cc53-4196-b1f9-2403e3909185/panel-tests-dashlist-variables';
|
||||||
|
|
||||||
|
describe('DashList panel', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
e2e.flows.login(Cypress.env('USERNAME'), Cypress.env('PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
// this is to prevent the fix for https://github.com/grafana/grafana/issues/76800 from regressing
|
||||||
|
it('should pass current variable values correctly when `Include current template variable values` is set', () => {
|
||||||
|
e2e.flows.openDashboard({ uid: PAGE_UNDER_TEST });
|
||||||
|
|
||||||
|
// check the initial value of the urls contain the variable value correctly
|
||||||
|
e2e.components.Panels.Panel.title('Include time range and variables enabled')
|
||||||
|
.should('be.visible')
|
||||||
|
.within(() => {
|
||||||
|
cy.get('a').each(($el) => {
|
||||||
|
cy.wrap($el).should('have.attr', 'href').and('contain', 'var-server=A');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// update variable to b
|
||||||
|
e2e.pages.Dashboard.SubMenu.submenuItemLabels('server').click();
|
||||||
|
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B').click();
|
||||||
|
// blur the dropdown
|
||||||
|
cy.get('body').click();
|
||||||
|
|
||||||
|
// check the urls are updated with the new variable value
|
||||||
|
e2e.components.Panels.Panel.title('Include time range and variables enabled')
|
||||||
|
.should('be.visible')
|
||||||
|
.within(() => {
|
||||||
|
cy.get('a').each(($el) => {
|
||||||
|
cy.wrap($el).should('have.attr', 'href').and('contain', 'var-server=B');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,8 +1,18 @@
|
|||||||
import { CoreApp } from '@grafana/data';
|
import { CoreApp } from '@grafana/data';
|
||||||
import { sceneGraph, SceneGridItem, SceneGridLayout, SceneQueryRunner, VizPanel } from '@grafana/scenes';
|
import {
|
||||||
|
sceneGraph,
|
||||||
|
SceneGridItem,
|
||||||
|
SceneGridLayout,
|
||||||
|
SceneQueryRunner,
|
||||||
|
SceneVariableSet,
|
||||||
|
TestVariable,
|
||||||
|
VizPanel,
|
||||||
|
} from '@grafana/scenes';
|
||||||
|
import appEvents from 'app/core/app_events';
|
||||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||||
|
import { VariablesChanged } from 'app/features/variables/types';
|
||||||
|
|
||||||
import { DashboardScene } from './DashboardScene';
|
import { DashboardScene, DashboardSceneState } from './DashboardScene';
|
||||||
|
|
||||||
describe('DashboardScene', () => {
|
describe('DashboardScene', () => {
|
||||||
describe('DashboardSrv.getCurrent compatibility', () => {
|
describe('DashboardSrv.getCurrent compatibility', () => {
|
||||||
@ -59,9 +69,27 @@ describe('DashboardScene', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('When variables change', () => {
|
||||||
|
it('A change to griditem pos should set isDirty true', () => {
|
||||||
|
const varA = new TestVariable({ name: 'A', query: 'A.*', value: 'A.AA', text: '', options: [], delayMs: 0 });
|
||||||
|
const scene = buildTestScene({
|
||||||
|
$variables: new SceneVariableSet({ variables: [varA] }),
|
||||||
|
});
|
||||||
|
|
||||||
|
scene.activate();
|
||||||
|
|
||||||
|
const eventHandler = jest.fn();
|
||||||
|
appEvents.subscribe(VariablesChanged, eventHandler);
|
||||||
|
|
||||||
|
varA.changeValueTo('A.AB');
|
||||||
|
|
||||||
|
expect(eventHandler).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function buildTestScene() {
|
function buildTestScene(overrides?: Partial<DashboardSceneState>) {
|
||||||
const scene = new DashboardScene({
|
const scene = new DashboardScene({
|
||||||
title: 'hello',
|
title: 'hello',
|
||||||
uid: 'dash-1',
|
uid: 'dash-1',
|
||||||
@ -86,6 +114,7 @@ function buildTestScene() {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
...overrides,
|
||||||
});
|
});
|
||||||
|
|
||||||
return scene;
|
return scene;
|
||||||
|
@ -13,10 +13,14 @@ import {
|
|||||||
SceneObjectState,
|
SceneObjectState,
|
||||||
SceneObjectStateChangedEvent,
|
SceneObjectStateChangedEvent,
|
||||||
sceneUtils,
|
sceneUtils,
|
||||||
|
SceneVariable,
|
||||||
|
SceneVariableDependencyConfigLike,
|
||||||
} from '@grafana/scenes';
|
} from '@grafana/scenes';
|
||||||
|
import appEvents from 'app/core/app_events';
|
||||||
import { getNavModel } from 'app/core/selectors/navModel';
|
import { getNavModel } from 'app/core/selectors/navModel';
|
||||||
import { newBrowseDashboardsEnabled } from 'app/features/browse-dashboards/featureFlag';
|
import { newBrowseDashboardsEnabled } from 'app/features/browse-dashboards/featureFlag';
|
||||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||||
|
import { VariablesChanged } from 'app/features/variables/types';
|
||||||
import { DashboardMeta } from 'app/types';
|
import { DashboardMeta } from 'app/types';
|
||||||
|
|
||||||
import { DashboardSceneRenderer } from '../scene/DashboardSceneRenderer';
|
import { DashboardSceneRenderer } from '../scene/DashboardSceneRenderer';
|
||||||
@ -62,6 +66,11 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
|||||||
* Handles url sync
|
* Handles url sync
|
||||||
*/
|
*/
|
||||||
protected _urlSync = new DashboardSceneUrlSync(this);
|
protected _urlSync = new DashboardSceneUrlSync(this);
|
||||||
|
/**
|
||||||
|
* Get notified when variables change
|
||||||
|
*/
|
||||||
|
protected _variableDependency = new DashboardVariableDependency();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* State before editing started
|
* State before editing started
|
||||||
*/
|
*/
|
||||||
@ -285,3 +294,22 @@ export class DashboardScene extends SceneObjectBase<DashboardSceneState> {
|
|||||||
return Boolean(meta.canEdit || meta.canMakeEditable);
|
return Boolean(meta.canEdit || meta.canMakeEditable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class DashboardVariableDependency implements SceneVariableDependencyConfigLike {
|
||||||
|
private _emptySet = new Set<string>();
|
||||||
|
|
||||||
|
getNames(): Set<string> {
|
||||||
|
return this._emptySet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasDependencyOn(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public variableUpdatesCompleted(changedVars: Set<SceneVariable>) {
|
||||||
|
if (changedVars.size > 0) {
|
||||||
|
// Temp solution for some core panels (like dashlist) to know that variables have changed
|
||||||
|
appEvents.publish(new VariablesChanged({ refreshAll: true, panelIds: [] }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,16 +1,25 @@
|
|||||||
import { take } from 'lodash';
|
import { take } from 'lodash';
|
||||||
import React, { useEffect, useMemo, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { DateTime, InterpolateFunction, PanelProps, textUtil, UrlQueryValue, urlUtil } from '@grafana/data';
|
import {
|
||||||
|
DataLinkBuiltInVars,
|
||||||
|
DateTime,
|
||||||
|
InterpolateFunction,
|
||||||
|
PanelProps,
|
||||||
|
textUtil,
|
||||||
|
UrlQueryValue,
|
||||||
|
urlUtil,
|
||||||
|
} from '@grafana/data';
|
||||||
import { CustomScrollbar, useStyles2, IconButton } from '@grafana/ui';
|
import { CustomScrollbar, useStyles2, IconButton } from '@grafana/ui';
|
||||||
import { getConfig } from 'app/core/config';
|
import { getConfig } from 'app/core/config';
|
||||||
|
import { appEvents } from 'app/core/core';
|
||||||
|
import { useBusEvent } from 'app/core/hooks/useBusEvent';
|
||||||
import { setStarred } from 'app/core/reducers/navBarTree';
|
import { setStarred } from 'app/core/reducers/navBarTree';
|
||||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||||
import impressionSrv from 'app/core/services/impression_srv';
|
import impressionSrv from 'app/core/services/impression_srv';
|
||||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||||
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
|
||||||
import { DashboardSearchItem } from 'app/features/search/types';
|
import { DashboardSearchItem } from 'app/features/search/types';
|
||||||
import { getVariablesUrlParams } from 'app/features/variables/getAllVariableValuesForUrl';
|
import { VariablesChanged } from 'app/features/variables/types';
|
||||||
import { useDispatch } from 'app/types';
|
import { useDispatch } from 'app/types';
|
||||||
|
|
||||||
import { Options } from './panelcfg.gen';
|
import { Options } from './panelcfg.gen';
|
||||||
@ -26,6 +35,7 @@ interface DashboardGroup {
|
|||||||
|
|
||||||
async function fetchDashboards(options: Options, replaceVars: InterpolateFunction) {
|
async function fetchDashboards(options: Options, replaceVars: InterpolateFunction) {
|
||||||
let starredDashboards: Promise<DashboardSearchItem[]> = Promise.resolve([]);
|
let starredDashboards: Promise<DashboardSearchItem[]> = Promise.resolve([]);
|
||||||
|
|
||||||
if (options.showStarred) {
|
if (options.showStarred) {
|
||||||
const params = { limit: options.maxItems, starred: 'true' };
|
const params = { limit: options.maxItems, starred: 'true' };
|
||||||
starredDashboards = getBackendSrv().search(params);
|
starredDashboards = getBackendSrv().search(params);
|
||||||
@ -92,6 +102,7 @@ async function fetchDashboards(options: Options, replaceVars: InterpolateFunctio
|
|||||||
export function DashList(props: PanelProps<Options>) {
|
export function DashList(props: PanelProps<Options>) {
|
||||||
const [dashboards, setDashboards] = useState(new Map<string, Dashboard>());
|
const [dashboards, setDashboards] = useState(new Map<string, Dashboard>());
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchDashboards(props.options, props.replaceVariables).then((dashes) => {
|
fetchDashboards(props.options, props.replaceVariables).then((dashes) => {
|
||||||
setDashboards(dashes);
|
setDashboards(dashes);
|
||||||
@ -140,27 +151,14 @@ export function DashList(props: PanelProps<Options>) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const css = useStyles2(getStyles);
|
const css = useStyles2(getStyles);
|
||||||
|
const urlParams = useDashListUrlParams(props);
|
||||||
|
|
||||||
const renderList = (dashboards: Dashboard[]) => (
|
const renderList = (dashboards: Dashboard[]) => (
|
||||||
<ul>
|
<ul>
|
||||||
{dashboards.map((dash) => {
|
{dashboards.map((dash) => {
|
||||||
let url = dash.url;
|
let url = dash.url;
|
||||||
let params: { [key: string]: string | DateTime | UrlQueryValue } = {};
|
|
||||||
|
|
||||||
if (props.options.keepTime) {
|
url = urlUtil.appendQueryToUrl(url, urlParams);
|
||||||
const range = getTimeSrv().timeRangeForUrl();
|
|
||||||
params['from'] = range.from;
|
|
||||||
params['to'] = range.to;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (props.options.includeVars) {
|
|
||||||
params = {
|
|
||||||
...params,
|
|
||||||
...getVariablesUrlParams(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
url = urlUtil.appendQueryToUrl(url, urlUtil.toUrlParams(params));
|
|
||||||
url = getConfig().disableSanitizeHtml ? url : textUtil.sanitizeUrl(url);
|
url = getConfig().disableSanitizeHtml ? url : textUtil.sanitizeUrl(url);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -199,3 +197,22 @@ export function DashList(props: PanelProps<Options>) {
|
|||||||
</CustomScrollbar>
|
</CustomScrollbar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useDashListUrlParams(props: PanelProps<Options>) {
|
||||||
|
// We don't care about the payload just want to get re-render when this event is published
|
||||||
|
useBusEvent(appEvents, VariablesChanged);
|
||||||
|
|
||||||
|
let params: { [key: string]: string | DateTime | UrlQueryValue } = {};
|
||||||
|
|
||||||
|
if (props.options.keepTime) {
|
||||||
|
params[`\$${DataLinkBuiltInVars.keepTime}`] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.options.includeVars) {
|
||||||
|
params[`\$${DataLinkBuiltInVars.includeVars}`] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlParms = props.replaceVariables(urlUtil.toUrlParams(params));
|
||||||
|
|
||||||
|
return urlParms;
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user