From 3dcfe54d8d921c79b0d319e42441070a24b60028 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Wed, 2 Dec 2020 14:08:35 +0100 Subject: [PATCH] Variables: Fixes Textbox current value persistence (#29481) * Variables: Fixes savequery for Constant and TextBox variables * Refactor: reverts textbox changes * Refactor: Fixes dashboard export and tests * Refactor: hides or migrates Constant variables * Tests: fixes snapshots * Variables: Fixes Textbox current value persistance * Refactor: fixes PR comments and adds e2e tests --- .../templating-textbox-e2e-scenarios.json | 116 +++++++ .../specs/variables/textbox-variables.ts | 299 ++++++++++++++++++ .../src/selectors/components.ts | 3 + .../src/selectors/pages.ts | 3 + .../DashboardSettings/template.html | 4 +- .../SaveDashboard/SaveDashboardButton.tsx | 3 + .../dashboard/state/DashboardMigrator.test.ts | 2 +- .../dashboard/state/DashboardModel.ts | 8 +- public/app/features/variables/adapters.ts | 2 +- .../variables/shared/testing/builders.ts | 3 +- .../shared/testing/textboxVariableBuilder.ts | 9 + .../textbox/TextBoxVariableEditor.tsx | 58 ++-- .../textbox/TextBoxVariablePicker.tsx | 60 ++-- .../variables/textbox/adapter.test.ts | 84 +++++ .../app/features/variables/textbox/adapter.ts | 14 +- .../app/features/variables/textbox/reducer.ts | 1 + public/app/features/variables/types.ts | 4 +- 17 files changed, 606 insertions(+), 67 deletions(-) create mode 100644 devenv/dev-dashboards/feature-templating/templating-textbox-e2e-scenarios.json create mode 100644 e2e/suite1/specs/variables/textbox-variables.ts create mode 100644 public/app/features/variables/shared/testing/textboxVariableBuilder.ts create mode 100644 public/app/features/variables/textbox/adapter.test.ts diff --git a/devenv/dev-dashboards/feature-templating/templating-textbox-e2e-scenarios.json b/devenv/dev-dashboards/feature-templating/templating-textbox-e2e-scenarios.json new file mode 100644 index 00000000000..37c853cafcc --- /dev/null +++ b/devenv/dev-dashboards/feature-templating/templating-textbox-e2e-scenarios.json @@ -0,0 +1,116 @@ +{ + "__inputs": [ + { + "name": "DS_GDEV-TESTDATA", + "label": "gdev-testdata", + "description": "", + "type": "datasource", + "pluginId": "testdata", + "pluginName": "TestData DB" + } + ], + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "7.4.0-pre" + }, + { + "type": "datasource", + "id": "testdata", + "name": "TestData DB", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "text", + "name": "Text", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "id": null, + "iteration": 1606804991052, + "links": [], + "panels": [ + { + "datasource": "${DS_GDEV-TESTDATA}", + "fieldConfig": { + "defaults": { + "custom": {} + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "content": "# variable: ${text}\n ", + "mode": "markdown" + }, + "pluginVersion": "7.4.0-pre", + "timeFrom": null, + "timeShift": null, + "title": "Panel Title", + "type": "text" + } + ], + "schemaVersion": 27, + "style": "dark", + "tags": [], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "default value", + "value": "default value" + }, + "description": null, + "error": null, + "hide": 0, + "label": null, + "name": "text", + "options": [ + { + "selected": true, + "text": "default value", + "value": "default value" + } + ], + "query": "default value", + "skipUrlSync": false, + "type": "textbox" + } + ] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "Templating - Textbox e2e scenarios", + "uid": "AejrN1AMz", + "version": 1 +} diff --git a/e2e/suite1/specs/variables/textbox-variables.ts b/e2e/suite1/specs/variables/textbox-variables.ts new file mode 100644 index 00000000000..f85c5809d88 --- /dev/null +++ b/e2e/suite1/specs/variables/textbox-variables.ts @@ -0,0 +1,299 @@ +import { e2e } from '@grafana/e2e'; + +const PAGE_UNDER_TEST = 'AejrN1AMz'; + +describe('TextBox - load options scenarios', function() { + it('default options should be correct', function() { + e2e.flows.login('admin', 'admin'); + e2e.flows.openDashboard({ uid: PAGE_UNDER_TEST }); + e2e().server(); + e2e() + .route({ + method: 'GET', + url: `/api/dashboards/uid/${PAGE_UNDER_TEST}`, + }) + .as('dash'); + + e2e().wait('@dash'); + + validateTextboxAndMarkup('default value'); + }); + + it('loading variable from url should be correct', function() { + e2e.flows.login('admin', 'admin'); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?var-text=not default value` }); + e2e().server(); + e2e() + .route({ + method: 'GET', + url: `/api/dashboards/uid/${PAGE_UNDER_TEST}`, + }) + .as('dash'); + + e2e().wait('@dash'); + + validateTextboxAndMarkup('not default value'); + }); +}); + +describe('TextBox - change query scenarios', function() { + it('when changing the query value and not saving current as default should revert query value', function() { + copyExistingDashboard(); + + changeQueryInput(); + + e2e.components.BackButton.backArrow() + .should('be.visible') + .click({ force: true }); + + validateTextboxAndMarkup('changed value'); + + saveDashboard(false); + + e2e() + .get('@dashuid') + .then((dashuid: any) => { + expect(dashuid).not.to.eq(PAGE_UNDER_TEST); + + e2e.flows.openDashboard({ uid: dashuid }); + + e2e().wait('@load-dash'); + + validateTextboxAndMarkup('default value'); + + validateVariable('changed value'); + }); + }); + + it('when changing the query value and saving current as default should change query value', function() { + copyExistingDashboard(); + + changeQueryInput(); + + e2e.components.BackButton.backArrow() + .should('be.visible') + .click({ force: true }); + + validateTextboxAndMarkup('changed value'); + + saveDashboard(true); + + e2e() + .get('@dashuid') + .then((dashuid: any) => { + expect(dashuid).not.to.eq(PAGE_UNDER_TEST); + + e2e.flows.openDashboard({ uid: dashuid }); + + e2e().wait('@load-dash'); + + validateTextboxAndMarkup('changed value'); + + validateVariable('changed value'); + }); + }); +}); + +describe('TextBox - change picker value scenarios', function() { + it('when changing the input value and not saving current as default should revert query value', function() { + copyExistingDashboard(); + + changeTextBoxInput(); + + validateTextboxAndMarkup('changed value'); + + saveDashboard(false); + + e2e() + .get('@dashuid') + .then((dashuid: any) => { + expect(dashuid).not.to.eq(PAGE_UNDER_TEST); + + e2e.flows.openDashboard({ uid: dashuid }); + + e2e().wait('@load-dash'); + + validateTextboxAndMarkup('default value'); + validateVariable('default value'); + }); + }); + + it('when changing the input value and saving current as default should change query value', function() { + copyExistingDashboard(); + + changeTextBoxInput(); + + validateTextboxAndMarkup('changed value'); + + saveDashboard(true); + + e2e() + .get('@dashuid') + .then((dashuid: any) => { + expect(dashuid).not.to.eq(PAGE_UNDER_TEST); + + e2e.flows.openDashboard({ uid: dashuid }); + + e2e().wait('@load-dash'); + + validateTextboxAndMarkup('changed value'); + validateVariable('changed value'); + }); + }); +}); + +function copyExistingDashboard() { + e2e.flows.login('admin', 'admin'); + e2e().server(); + e2e() + .route({ + method: 'GET', + url: '/api/search?query=&type=dash-folder&permission=Edit', + }) + .as('dash-settings'); + e2e() + .route({ + method: 'POST', + url: '/api/dashboards/db/', + }) + .as('save-dash'); + e2e() + .route({ + method: 'GET', + url: /\/api\/dashboards\/uid\/(?!AejrN1AMz)\w+/, + }) + .as('load-dash'); + e2e.flows.openDashboard({ uid: `${PAGE_UNDER_TEST}?editview=settings&orgId=1` }); + + e2e().wait('@dash-settings'); + + e2e.pages.Dashboard.Settings.General.saveAsDashBoard() + .should('be.visible') + .click(); + + e2e.pages.SaveDashboardAsModal.newName() + .should('be.visible') + .type(`${Date.now()}`); + + e2e.pages.SaveDashboardAsModal.save() + .should('be.visible') + .click(); + + e2e().wait('@save-dash'); + e2e().wait('@load-dash'); + + e2e.pages.Dashboard.SubMenu.submenuItem().should('be.visible'); + + e2e() + .location() + .then(loc => { + const dashuid = /\/d\/(\w+)\//.exec(loc.href)![1]; + e2e() + .wrap(dashuid) + .as('dashuid'); + }); + + e2e().wait(500); +} + +function saveDashboard(saveVariables: boolean) { + e2e.pages.Dashboard.Toolbar.toolbarItems('Save dashboard') + .should('be.visible') + .click(); + + if (saveVariables) { + e2e.pages.SaveDashboardModal.saveVariables() + .should('exist') + .click({ force: true }); + } + + e2e.pages.SaveDashboardModal.save() + .should('be.visible') + .click(); + + e2e().wait('@save-dash'); +} + +function validateTextboxAndMarkup(value: string) { + e2e.pages.Dashboard.SubMenu.submenuItem() + .should('be.visible') + .within(() => { + e2e.pages.Dashboard.SubMenu.submenuItemLabels('text').should('be.visible'); + e2e() + .get('input') + .should('be.visible') + .should('have.value', value); + }); + + e2e.components.Panels.Visualization.Text.container() + .should('be.visible') + .within(() => { + e2e() + .get('h1') + .should('be.visible') + .should('have.text', `variable: ${value}`); + }); +} + +function validateVariable(value: string) { + e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings') + .should('be.visible') + .click(); + + e2e.pages.Dashboard.Settings.General.sectionItems('Variables') + .should('be.visible') + .click(); + + e2e.pages.Dashboard.Settings.Variables.List.tableRowNameFields('text') + .should('be.visible') + .click(); + + e2e.pages.Dashboard.Settings.Variables.Edit.TextBoxVariable.textBoxOptionsQueryInput() + .should('be.visible') + .should('have.value', value); +} + +function changeTextBoxInput() { + e2e.pages.Dashboard.SubMenu.submenuItemLabels('text').should('be.visible'); + e2e.pages.Dashboard.SubMenu.submenuItem() + .should('be.visible') + .within(() => { + e2e() + .get('input') + .should('be.visible') + .should('have.value', 'default value') + .clear() + .type('changed value') + .type('{enter}'); + }); + + e2e() + .location() + .should(loc => { + expect(loc.search).to.contain('var-text=changed%20value'); + }); +} + +function changeQueryInput() { + e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings') + .should('be.visible') + .click(); + + e2e.pages.Dashboard.Settings.General.sectionItems('Variables') + .should('be.visible') + .click(); + + e2e.pages.Dashboard.Settings.Variables.List.tableRowNameFields('text') + .should('be.visible') + .click(); + + e2e.pages.Dashboard.Settings.Variables.Edit.TextBoxVariable.textBoxOptionsQueryInput() + .should('be.visible') + .clear() + .type('changed value') + .blur(); + + e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption() + .should('have.length', 1) + .should('have.text', 'changed value'); +} diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index 73deb45ab09..ebd777e28d4 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -37,6 +37,9 @@ export const Components = { BarGauge: { value: 'Bar gauge value', }, + Text: { + container: () => '.markdown-html', + }, }, }, Drawer: { diff --git a/packages/grafana-e2e-selectors/src/selectors/pages.ts b/packages/grafana-e2e-selectors/src/selectors/pages.ts index ff00865f431..2c753972131 100644 --- a/packages/grafana-e2e-selectors/src/selectors/pages.ts +++ b/packages/grafana-e2e-selectors/src/selectors/pages.ts @@ -99,6 +99,9 @@ export const Pages = { ConstantVariable: { constantOptionsQueryInput: 'Variable editor Form Constant Query field', }, + TextBoxVariable: { + textBoxOptionsQueryInput: 'Variable editor Form TextBox Query field', + }, }, }, }, diff --git a/public/app/features/dashboard/components/DashboardSettings/template.html b/public/app/features/dashboard/components/DashboardSettings/template.html index 6fb87f257f5..b951945faba 100644 --- a/public/app/features/dashboard/components/DashboardSettings/template.html +++ b/public/app/features/dashboard/components/DashboardSettings/template.html @@ -10,10 +10,10 @@
- +
- +
diff --git a/public/app/features/dashboard/components/SaveDashboard/SaveDashboardButton.tsx b/public/app/features/dashboard/components/SaveDashboard/SaveDashboardButton.tsx index da7bf2bae57..7a07d4132fd 100644 --- a/public/app/features/dashboard/components/SaveDashboard/SaveDashboardButton.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/SaveDashboardButton.tsx @@ -5,6 +5,7 @@ import { connectWithProvider } from 'app/core/utils/connectWithReduxStore'; import { provideModalsContext } from 'app/routes/ReactContainer'; import { SaveDashboardAsModal } from './SaveDashboardAsModal'; import { SaveDashboardModalProxy } from './SaveDashboardModalProxy'; +import { selectors } from '@grafana/e2e-selectors'; interface SaveDashboardButtonProps { dashboard: DashboardModel; @@ -30,6 +31,7 @@ export const SaveDashboardButton: React.FC = ({ dashbo onDismiss: hideModal, }); }} + aria-label={selectors.pages.Dashboard.Settings.General.saveDashBoard} > Save dashboard @@ -63,6 +65,7 @@ export const SaveDashboardAsButton: React.FC Save As... diff --git a/public/app/features/dashboard/state/DashboardMigrator.test.ts b/public/app/features/dashboard/state/DashboardMigrator.test.ts index ee477e60c59..bdea27b2765 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.test.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.test.ts @@ -857,7 +857,7 @@ describe('DashboardModel', () => { }); }); - it('should have three variables after migration', () => { + it('should have six variables after migration', () => { expect(model.templating.list.length).toBe(6); }); diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index 429c4a5ea8b..947933bb9bd 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -14,12 +14,12 @@ import { dateTimeFormat, dateTimeFormatTimeAgo, DateTimeInput, + EventBusExtended, + EventBusSrv, PanelEvents, TimeRange, TimeZone, UrlQueryValue, - EventBusSrv, - EventBusExtended, } from '@grafana/data'; import { CoreEvents, DashboardMeta, KIOSK_MODE_TV } from 'app/types'; import { GetVariables, getVariables } from 'app/features/variables/state/selectors'; @@ -236,7 +236,9 @@ export class DashboardModel { const currentVariables = this.getVariablesFromState(); copy.templating = { - list: currentVariables.map(variable => variableAdapters.get(variable.type).getSaveModel(variable)), + list: currentVariables.map(variable => + variableAdapters.get(variable.type).getSaveModel(variable, defaults.saveVariables) + ), }; if (!defaults.saveVariables) { diff --git a/public/app/features/variables/adapters.ts b/public/app/features/variables/adapters.ts index f83fb0e50e8..6bfba8bd37c 100644 --- a/public/app/features/variables/adapters.ts +++ b/public/app/features/variables/adapters.ts @@ -34,7 +34,7 @@ export interface VariableAdapter { setValue: (variable: Model, option: VariableOption, emitChanges?: boolean) => Promise; setValueFromUrl: (variable: Model, urlValue: UrlQueryValue) => Promise; updateOptions: (variable: Model, searchFilter?: string) => Promise; - getSaveModel: (variable: Model) => Partial; + getSaveModel: (variable: Model, saveCurrentAsDefault?: boolean) => Partial; getValueForUrl: (variable: Model) => string | string[]; picker: ComponentType; editor: ComponentType; diff --git a/public/app/features/variables/shared/testing/builders.ts b/public/app/features/variables/shared/testing/builders.ts index 3a5e813ef56..c2351663973 100644 --- a/public/app/features/variables/shared/testing/builders.ts +++ b/public/app/features/variables/shared/testing/builders.ts @@ -11,11 +11,12 @@ import { initialCustomVariableModelState } from '../../custom/reducer'; import { MultiVariableBuilder } from './multiVariableBuilder'; import { initialConstantVariableModelState } from '../../constant/reducer'; import { QueryVariableBuilder } from './queryVariableBuilder'; +import { TextBoxVariableBuilder } from './textboxVariableBuilder'; export const adHocBuilder = () => new AdHocVariableBuilder(initialAdHocVariableModelState); export const intervalBuilder = () => new IntervalVariableBuilder(initialIntervalVariableModelState); export const datasourceBuilder = () => new DatasourceVariableBuilder(initialDataSourceVariableModelState); export const queryBuilder = () => new QueryVariableBuilder(initialQueryVariableModelState); -export const textboxBuilder = () => new OptionsVariableBuilder(initialTextBoxVariableModelState); +export const textboxBuilder = () => new TextBoxVariableBuilder(initialTextBoxVariableModelState); export const customBuilder = () => new MultiVariableBuilder(initialCustomVariableModelState); export const constantBuilder = () => new OptionsVariableBuilder(initialConstantVariableModelState); diff --git a/public/app/features/variables/shared/testing/textboxVariableBuilder.ts b/public/app/features/variables/shared/testing/textboxVariableBuilder.ts new file mode 100644 index 00000000000..6170c7ee8f7 --- /dev/null +++ b/public/app/features/variables/shared/testing/textboxVariableBuilder.ts @@ -0,0 +1,9 @@ +import { TextBoxVariableModel } from 'app/features/variables/types'; +import { OptionsVariableBuilder } from './optionsVariableBuilder'; + +export class TextBoxVariableBuilder extends OptionsVariableBuilder { + withOriginalQuery(original: string) { + this.variable.originalQuery = original; + return this; + } +} diff --git a/public/app/features/variables/textbox/TextBoxVariableEditor.tsx b/public/app/features/variables/textbox/TextBoxVariableEditor.tsx index 7ca8e3a204d..8f078c2a335 100644 --- a/public/app/features/variables/textbox/TextBoxVariableEditor.tsx +++ b/public/app/features/variables/textbox/TextBoxVariableEditor.tsx @@ -1,36 +1,40 @@ -import React, { ChangeEvent, PureComponent } from 'react'; +import React, { ChangeEvent, ReactElement, useCallback } from 'react'; import { VerticalGroup } from '@grafana/ui'; import { TextBoxVariableModel } from '../types'; import { VariableEditorProps } from '../editor/types'; import { VariableSectionHeader } from '../editor/VariableSectionHeader'; import { VariableTextField } from '../editor/VariableTextField'; +import { selectors } from '@grafana/e2e-selectors'; export interface Props extends VariableEditorProps {} -export class TextBoxVariableEditor extends PureComponent { - onQueryChange = (event: ChangeEvent) => { - event.preventDefault(); - this.props.onPropChange({ propName: 'query', propValue: event.target.value, updateOptions: false }); - }; - onQueryBlur = (event: ChangeEvent) => { - event.preventDefault(); - this.props.onPropChange({ propName: 'query', propValue: event.target.value, updateOptions: true }); - }; - render() { - const { query } = this.props.variable; - return ( - - - - - ); - } + +export function TextBoxVariableEditor({ onPropChange, variable: { query } }: Props): ReactElement { + const updateVariable = useCallback( + (event: ChangeEvent, updateOptions: boolean) => { + event.preventDefault(); + onPropChange({ propName: 'originalQuery', propValue: event.target.value, updateOptions: false }); + onPropChange({ propName: 'query', propValue: event.target.value, updateOptions }); + }, + [onPropChange] + ); + + const onChange = useCallback((e: ChangeEvent) => updateVariable(e, false), [updateVariable]); + const onBlur = useCallback((e: ChangeEvent) => updateVariable(e, true), [updateVariable]); + + return ( + + + + + ); } diff --git a/public/app/features/variables/textbox/TextBoxVariablePicker.tsx b/public/app/features/variables/textbox/TextBoxVariablePicker.tsx index 10632773665..b5f3e4a1570 100644 --- a/public/app/features/variables/textbox/TextBoxVariablePicker.tsx +++ b/public/app/features/variables/textbox/TextBoxVariablePicker.tsx @@ -1,43 +1,45 @@ -import React, { ChangeEvent, FocusEvent, KeyboardEvent, PureComponent } from 'react'; +import React, { ChangeEvent, FocusEvent, KeyboardEvent, ReactElement, useCallback, useEffect, useState } from 'react'; import { TextBoxVariableModel } from '../types'; -import { toVariableIdentifier, toVariablePayload } from '../state/types'; -import { dispatch } from '../../../store/store'; +import { toVariablePayload } from '../state/types'; import { changeVariableProp } from '../state/sharedReducer'; import { VariablePickerProps } from '../pickers/types'; -import { updateOptions } from '../state/actions'; +import { Input } from '@grafana/ui'; +import { variableAdapters } from '../adapters'; +import { useDispatch } from 'react-redux'; export interface Props extends VariablePickerProps {} -export class TextBoxVariablePicker extends PureComponent { - onQueryChange = (event: ChangeEvent) => { +export function TextBoxVariablePicker({ variable }: Props): ReactElement { + const dispatch = useDispatch(); + const [updatedValue, setUpdatedValue] = useState(variable.current.value); + useEffect(() => { + setUpdatedValue(variable.current.value); + }, [variable]); + + const updateVariable = useCallback(() => { + if (variable.current.value === updatedValue) { + return; + } + dispatch( - changeVariableProp(toVariablePayload(this.props.variable, { propName: 'query', propValue: event.target.value })) + changeVariableProp( + toVariablePayload({ id: variable.id, type: variable.type }, { propName: 'query', propValue: updatedValue }) + ) ); - }; + variableAdapters.get(variable.type).updateOptions(variable); + }, [dispatch, variable, updatedValue]); - onQueryBlur = (event: FocusEvent) => { - if (this.props.variable.current.value !== this.props.variable.query) { - dispatch(updateOptions(toVariableIdentifier(this.props.variable))); + const onChange = useCallback((event: ChangeEvent) => setUpdatedValue(event.target.value), [ + setUpdatedValue, + ]); + + const onBlur = (e: FocusEvent) => updateVariable(); + const onKeyDown = (event: KeyboardEvent) => { + if (event.keyCode === 13) { + updateVariable(); } }; - onQueryKeyDown = (event: KeyboardEvent) => { - if (event.keyCode === 13 && this.props.variable.current.value !== this.props.variable.query) { - dispatch(updateOptions(toVariableIdentifier(this.props.variable))); - } - }; - - render() { - return ( - - ); - } + return ; } diff --git a/public/app/features/variables/textbox/adapter.test.ts b/public/app/features/variables/textbox/adapter.test.ts new file mode 100644 index 00000000000..e2e9d1db7ba --- /dev/null +++ b/public/app/features/variables/textbox/adapter.test.ts @@ -0,0 +1,84 @@ +import { variableAdapters } from '../adapters'; +import { createTextBoxVariableAdapter } from './adapter'; +import { textboxBuilder } from '../shared/testing/builders'; +import { VariableHide } from '../types'; + +variableAdapters.setInit(() => [createTextBoxVariableAdapter()]); + +describe('createTextBoxVariableAdapter', () => { + describe('getSaveModel', () => { + describe('when called and query differs from the original query and not saving current as default', () => { + it('then the model should be correct', () => { + const text = textboxBuilder() + .withId('text') + .withName('text') + .withQuery('query') + .withOriginalQuery('original') + .withCurrent('query') + .withOptions('query') + .build(); + + const adapter = variableAdapters.get('textbox'); + + const result = adapter.getSaveModel(text, false); + + expect(result).toEqual({ + name: 'text', + query: 'original', + current: { selected: false, text: 'original', value: 'original' }, + options: [{ selected: false, text: 'original', value: 'original' }], + type: 'textbox', + label: null, + hide: VariableHide.dontHide, + skipUrlSync: false, + error: null, + description: null, + }); + }); + }); + + describe('when called and query differs from the original query and saving current as default', () => { + it('then the model should be correct', () => { + const text = textboxBuilder() + .withId('text') + .withName('text') + .withQuery('query') + .withOriginalQuery('original') + .withCurrent('query') + .withOptions('query') + .build(); + + const adapter = variableAdapters.get('textbox'); + + const result = adapter.getSaveModel(text, true); + + expect(result).toEqual({ + name: 'text', + query: 'query', + current: { selected: true, text: 'query', value: 'query' }, + options: [{ selected: false, text: 'query', value: 'query' }], + type: 'textbox', + label: null, + hide: VariableHide.dontHide, + skipUrlSync: false, + error: null, + description: null, + }); + }); + }); + }); + + describe('beforeAdding', () => { + describe('when called', () => { + it('then originalQuery should be same added', () => { + const model = { name: 'text', query: 'a query' }; + + const adapter = variableAdapters.get('textbox'); + + const result = adapter.beforeAdding!(model); + + expect(result).toEqual({ name: 'text', query: 'a query', originalQuery: 'a query' }); + }); + }); + }); +}); diff --git a/public/app/features/variables/textbox/adapter.ts b/public/app/features/variables/textbox/adapter.ts index 801983d4c54..d57fe26b7c8 100644 --- a/public/app/features/variables/textbox/adapter.ts +++ b/public/app/features/variables/textbox/adapter.ts @@ -31,12 +31,22 @@ export const createTextBoxVariableAdapter = (): VariableAdapter { await dispatch(updateTextBoxVariableOptions(toVariableIdentifier(variable))); }, - getSaveModel: variable => { - const { index, id, state, global, ...rest } = cloneDeep(variable); + getSaveModel: (variable, saveCurrentAsDefault) => { + const { index, id, state, global, originalQuery, ...rest } = cloneDeep(variable); + + if (variable.query !== originalQuery && !saveCurrentAsDefault) { + const origQuery = originalQuery ?? ''; + const current = { selected: false, text: origQuery, value: origQuery }; + return { ...rest, query: origQuery, current, options: [current] }; + } + return rest; }, getValueForUrl: variable => { return variable.current.value; }, + beforeAdding: model => { + return { ...cloneDeep(model), originalQuery: model.query }; + }, }; }; diff --git a/public/app/features/variables/textbox/reducer.ts b/public/app/features/variables/textbox/reducer.ts index ca962e87b08..bb4714ff55e 100644 --- a/public/app/features/variables/textbox/reducer.ts +++ b/public/app/features/variables/textbox/reducer.ts @@ -10,6 +10,7 @@ export const initialTextBoxVariableModelState: TextBoxVariableModel = { query: '', current: {} as VariableOption, options: [], + originalQuery: null, }; export const textBoxVariableSlice = createSlice({ diff --git a/public/app/features/variables/types.ts b/public/app/features/variables/types.ts index a466aa8d10c..2e3c916f62a 100644 --- a/public/app/features/variables/types.ts +++ b/public/app/features/variables/types.ts @@ -86,7 +86,9 @@ export interface QueryVariableModel extends DataSourceVariableModel { query: any; } -export interface TextBoxVariableModel extends VariableWithOptions {} +export interface TextBoxVariableModel extends VariableWithOptions { + originalQuery: string | null; +} export interface ConstantVariableModel extends VariableWithOptions {}