From 66177e946350ada3d83809f88db2f6be76a3207f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 15 Mar 2021 08:44:13 +0100 Subject: [PATCH] PanelInspect: Interpolates variables in CSV file name (#31936) * PanelInspect: Variables in CSV file name are interpolated * Chore: fixes failing test --- .../components/Inspector/InspectDataTab.tsx | 6 +- .../dashboard/dashgrid/PanelChrome.test.tsx | 1 + .../dashgrid/PanelHeader/PanelHeader.tsx | 2 +- .../dashboard/state/PanelModel.test.ts | 71 ++++++++++--------- .../features/dashboard/state/PanelModel.ts | 10 +++ 5 files changed, 51 insertions(+), 39 deletions(-) diff --git a/public/app/features/dashboard/components/Inspector/InspectDataTab.tsx b/public/app/features/dashboard/components/Inspector/InspectDataTab.tsx index e188aabfc40..6ad0e8b8257 100644 --- a/public/app/features/dashboard/components/Inspector/InspectDataTab.tsx +++ b/public/app/features/dashboard/components/Inspector/InspectDataTab.tsx @@ -3,6 +3,7 @@ import AutoSizer from 'react-virtualized-auto-sizer'; import { applyFieldOverrides, applyRawFieldOverrides, + CSVConfig, DataFrame, DataTransformerID, dateTimeFormat, @@ -10,9 +11,8 @@ import { SelectableValue, toCSV, transformDataFrame, - CSVConfig, } from '@grafana/data'; -import { Button, Container, Field, HorizontalGroup, Spinner, Select, Switch, Table, VerticalGroup } from '@grafana/ui'; +import { Button, Container, Field, HorizontalGroup, Select, Spinner, Switch, Table, VerticalGroup } from '@grafana/ui'; import { selectors } from '@grafana/e2e-selectors'; import { getPanelInspectorStyles } from './styles'; @@ -94,7 +94,7 @@ export class InspectDataTab extends PureComponent { type: 'text/csv;charset=utf-8', }); const transformation = transformId !== DataTransformerID.noop ? '-as-' + transformId.toLocaleLowerCase() : ''; - const fileName = `${panel.title}-data${transformation}-${dateTimeFormat(new Date())}.csv`; + const fileName = `${panel.getDisplayTitle()}-data${transformation}-${dateTimeFormat(new Date())}.csv`; saveAs(blob, fileName); }; diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.test.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.test.tsx index a2bbbe4aad1..fef5ef8c719 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.test.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.test.tsx @@ -37,6 +37,7 @@ function setupTestContext(options: Partial) { events: { subscribe: jest.fn() }, getQueryRunner: () => panelQueryRunner, getOptions: jest.fn(), + getDisplayTitle: jest.fn(), } as unknown) as PanelModel, dashboard: ({ panelInitialized: jest.fn(), diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx index 4527711259b..787399ceaea 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx @@ -28,7 +28,7 @@ export interface Props { export const PanelHeader: FC = ({ panel, error, isViewing, isEditing, data, alertState, dashboard }) => { const onCancelQuery = () => panel.getQueryRunner().cancelQuery(); - const title = panel.replaceVariables(panel.title, {}, 'text'); + const title = panel.getDisplayTitle(); const className = cx('panel-header', !(isViewing || isEditing) ? 'grid-drag-handle' : ''); return ( diff --git a/public/app/features/dashboard/state/PanelModel.test.ts b/public/app/features/dashboard/state/PanelModel.test.ts index 2f38ac5920d..6315ac68371 100644 --- a/public/app/features/dashboard/state/PanelModel.test.ts +++ b/public/app/features/dashboard/state/PanelModel.test.ts @@ -1,13 +1,12 @@ import { PanelModel } from './PanelModel'; import { getPanelPlugin } from '../../plugins/__mocks__/pluginMocks'; import { + DataLinkBuiltInVars, FieldConfigProperty, + PanelData, PanelProps, standardEditorsRegistry, standardFieldConfigEditorRegistry, - PanelData, - DataLinkBuiltInVars, - VariableModel, } from '@grafana/data'; import { ComponentClass } from 'react'; import { PanelQueryRunner } from '../../query/state/PanelQueryRunner'; @@ -17,6 +16,7 @@ import { setTemplateSrv } from '@grafana/runtime'; import { variableAdapters } from '../../variables/adapters'; import { createQueryVariableAdapter } from '../../variables/query/adapter'; import { mockStandardFieldConfigOptions } from '../../../../test/helpers/fieldConfig'; +import { queryBuilder } from 'app/features/variables/shared/testing/builders'; standardFieldConfigEditorRegistry.setInit(() => mockStandardFieldConfigOptions()); standardEditorsRegistry.setInit(() => mockStandardFieldConfigOptions()); @@ -28,16 +28,15 @@ setTimeSrv({ }), } as any); +const getVariables = () => variablesMock; +const getVariableWithName = (name: string) => variablesMock.filter((v) => v.name === name)[0]; +const getFilteredVariables = jest.fn(); + setTemplateSrv( new TemplateSrv({ - // @ts-ignore - getVariables: () => { - return variablesMock; - }, - // @ts-ignore - getVariableWithName: (name: string) => { - return variablesMock.filter((v) => v.name === name)[0]; - }, + getVariables, + getVariableWithName, + getFilteredVariables, }) ); @@ -223,7 +222,7 @@ describe('PanelModel', () => { it('should interpolate $__all_variables variable', () => { const out = model.replaceVariables(`/d/1?$${DataLinkBuiltInVars.includeVars}`); - expect(out).toBe('/d/1?var-test1=val1&var-test2=val2'); + expect(out).toBe('/d/1?var-test1=val1&var-test2=val2&var-test3=Value%203&var-test4=A&var-test4=B'); }); it('should prefer the local variable value', () => { @@ -328,7 +327,7 @@ describe('PanelModel', () => { expect(model.showColumns).toBe(true); }); - it('should restore custom field config to what it was and preseve standard options', () => { + it('should restore custom field config to what it was and preserve standard options', () => { model.changePlugin(tablePlugin); expect(model.fieldConfig.defaults.custom.customProp).toBe(true); }); @@ -438,30 +437,32 @@ describe('PanelModel', () => { expect(model.getQueryRunner().getLastResult()).toBeDefined(); }); }); + + describe('getDisplayTitle', () => { + it('when called then it should interpolate singe value variables in title', () => { + const model = new PanelModel({ + title: 'Single value variable [[test3]] ${test3} ${test3:percentencode}', + }); + const title = model.getDisplayTitle(); + + expect(title).toEqual('Single value variable Value 3 Value 3 Value%203'); + }); + + it('when called then it should interpolate multi value variables in title', () => { + const model = new PanelModel({ + title: 'Multi value variable [[test4]] ${test4} ${test4:percentencode}', + }); + const title = model.getDisplayTitle(); + + expect(title).toEqual('Multi value variable A + B A + B %7BA%2CB%7D'); + }); + }); }); }); const variablesMock = [ - { - type: 'query', - name: 'test1', - label: 'Test1', - hide: false, - current: { value: 'val1' }, - skipUrlSync: false, - getValueForUrl: function () { - return 'val1'; - }, - } as VariableModel, - { - type: 'query', - name: 'test2', - label: 'Test2', - hide: false, - current: { value: 'val2' }, - skipUrlSync: false, - getValueForUrl: function () { - return 'val2'; - }, - } as VariableModel, + queryBuilder().withId('test1').withName('test1').withCurrent('val1').build(), + queryBuilder().withId('test2').withName('test2').withCurrent('val2').build(), + queryBuilder().withId('test3').withName('test3').withCurrent('Value 3').build(), + queryBuilder().withId('test4').withName('test4').withCurrent(['A', 'B']).build(), ]; diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index 1c18cff8c41..ccb175be9f5 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -59,6 +59,7 @@ const notPersistedProperties: { [str: string]: boolean } = { replaceVariables: true, editSourceId: true, hasChanged: true, + getDisplayTitle: true, }; // For angular panels we need to clean up properties when changing type @@ -102,6 +103,7 @@ const mustKeepProps: { [str: string]: boolean } = { interval: true, replaceVariables: true, libraryPanel: true, + getDisplayTitle: true, }; const defaults: any = { @@ -551,6 +553,14 @@ export class PanelModel implements DataConfigSource { getSavedId(): number { return this.editSourceId ?? this.id; } + + /* + * This is the title used when displaying the title in the UI so it will include any interpolated variables. + * If you need the raw title without interpolation use title property instead. + * */ + getDisplayTitle(): string { + return this.replaceVariables(this.title, {}, 'text'); + } } function getPluginVersion(plugin: PanelPlugin): string {