diff --git a/devenv/dev-dashboards/panel-dashlist/dashlist.json b/devenv/dev-dashboards/panel-dashlist/dashlist.json new file mode 100644 index 00000000000..8db5b616acd --- /dev/null +++ b/devenv/dev-dashboards/panel-dashlist/dashlist.json @@ -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": "" +} diff --git a/devenv/jsonnet/dev-dashboards.libsonnet b/devenv/jsonnet/dev-dashboards.libsonnet index 959a28a7dad..02aceb37b7f 100644 --- a/devenv/jsonnet/dev-dashboards.libsonnet +++ b/devenv/jsonnet/dev-dashboards.libsonnet @@ -149,6 +149,13 @@ local dashboard = grafana.dashboard; 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') + resource.addMetadata('folder', 'dev-dashboards') + { diff --git a/e2e/panels-suite/dashlist.spec.ts b/e2e/panels-suite/dashlist.spec.ts new file mode 100644 index 00000000000..ec053d3e5e4 --- /dev/null +++ b/e2e/panels-suite/dashlist.spec.ts @@ -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'); + }); + }); + }); +}); diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx index 4ee82614482..e34bab0ca21 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.test.tsx @@ -1,8 +1,18 @@ 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 { VariablesChanged } from 'app/features/variables/types'; -import { DashboardScene } from './DashboardScene'; +import { DashboardScene, DashboardSceneState } from './DashboardScene'; describe('DashboardScene', () => { 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) { const scene = new DashboardScene({ title: 'hello', uid: 'dash-1', @@ -86,6 +114,7 @@ function buildTestScene() { }), ], }), + ...overrides, }); return scene; diff --git a/public/app/features/dashboard-scene/scene/DashboardScene.tsx b/public/app/features/dashboard-scene/scene/DashboardScene.tsx index e380d2da55e..0f95d8456af 100644 --- a/public/app/features/dashboard-scene/scene/DashboardScene.tsx +++ b/public/app/features/dashboard-scene/scene/DashboardScene.tsx @@ -13,10 +13,14 @@ import { SceneObjectState, SceneObjectStateChangedEvent, sceneUtils, + SceneVariable, + SceneVariableDependencyConfigLike, } from '@grafana/scenes'; +import appEvents from 'app/core/app_events'; import { getNavModel } from 'app/core/selectors/navModel'; import { newBrowseDashboardsEnabled } from 'app/features/browse-dashboards/featureFlag'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; +import { VariablesChanged } from 'app/features/variables/types'; import { DashboardMeta } from 'app/types'; import { DashboardSceneRenderer } from '../scene/DashboardSceneRenderer'; @@ -62,6 +66,11 @@ export class DashboardScene extends SceneObjectBase { * Handles url sync */ protected _urlSync = new DashboardSceneUrlSync(this); + /** + * Get notified when variables change + */ + protected _variableDependency = new DashboardVariableDependency(); + /** * State before editing started */ @@ -285,3 +294,22 @@ export class DashboardScene extends SceneObjectBase { return Boolean(meta.canEdit || meta.canMakeEditable); } } + +export class DashboardVariableDependency implements SceneVariableDependencyConfigLike { + private _emptySet = new Set(); + + getNames(): Set { + return this._emptySet; + } + + public hasDependencyOn(): boolean { + return false; + } + + public variableUpdatesCompleted(changedVars: Set) { + 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: [] })); + } + } +} diff --git a/public/app/plugins/panel/dashlist/DashList.tsx b/public/app/plugins/panel/dashlist/DashList.tsx index 9f773eb0099..e0a75e7e33b 100644 --- a/public/app/plugins/panel/dashlist/DashList.tsx +++ b/public/app/plugins/panel/dashlist/DashList.tsx @@ -1,16 +1,25 @@ import { take } from 'lodash'; 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 { 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 { getBackendSrv } from 'app/core/services/backend_srv'; import impressionSrv from 'app/core/services/impression_srv'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; -import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; 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 { Options } from './panelcfg.gen'; @@ -26,6 +35,7 @@ interface DashboardGroup { async function fetchDashboards(options: Options, replaceVars: InterpolateFunction) { let starredDashboards: Promise = Promise.resolve([]); + if (options.showStarred) { const params = { limit: options.maxItems, starred: 'true' }; starredDashboards = getBackendSrv().search(params); @@ -92,6 +102,7 @@ async function fetchDashboards(options: Options, replaceVars: InterpolateFunctio export function DashList(props: PanelProps) { const [dashboards, setDashboards] = useState(new Map()); const dispatch = useDispatch(); + useEffect(() => { fetchDashboards(props.options, props.replaceVariables).then((dashes) => { setDashboards(dashes); @@ -140,27 +151,14 @@ export function DashList(props: PanelProps) { ]; const css = useStyles2(getStyles); + const urlParams = useDashListUrlParams(props); const renderList = (dashboards: Dashboard[]) => (
    {dashboards.map((dash) => { let url = dash.url; - let params: { [key: string]: string | DateTime | UrlQueryValue } = {}; - if (props.options.keepTime) { - 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 = urlUtil.appendQueryToUrl(url, urlParams); url = getConfig().disableSanitizeHtml ? url : textUtil.sanitizeUrl(url); return ( @@ -199,3 +197,22 @@ export function DashList(props: PanelProps) { ); } + +function useDashListUrlParams(props: PanelProps) { + // 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; +}