mirror of https://github.com/grafana/grafana
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 testspull/29419/head^2
parent
15f8dd44e5
commit
3dcfe54d8d
@ -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 |
||||
} |
||||
@ -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'); |
||||
} |
||||
@ -0,0 +1,9 @@ |
||||
import { TextBoxVariableModel } from 'app/features/variables/types'; |
||||
import { OptionsVariableBuilder } from './optionsVariableBuilder'; |
||||
|
||||
export class TextBoxVariableBuilder<T extends TextBoxVariableModel> extends OptionsVariableBuilder<T> { |
||||
withOriginalQuery(original: string) { |
||||
this.variable.originalQuery = original; |
||||
return this; |
||||
} |
||||
} |
||||
@ -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<TextBoxVariableModel> {} |
||||
export class TextBoxVariableEditor extends PureComponent<Props> { |
||||
onQueryChange = (event: ChangeEvent<HTMLInputElement>) => { |
||||
event.preventDefault(); |
||||
this.props.onPropChange({ propName: 'query', propValue: event.target.value, updateOptions: false }); |
||||
}; |
||||
onQueryBlur = (event: ChangeEvent<HTMLInputElement>) => { |
||||
event.preventDefault(); |
||||
this.props.onPropChange({ propName: 'query', propValue: event.target.value, updateOptions: true }); |
||||
}; |
||||
render() { |
||||
const { query } = this.props.variable; |
||||
return ( |
||||
<VerticalGroup spacing="xs"> |
||||
<VariableSectionHeader name="Text Options" /> |
||||
<VariableTextField |
||||
value={query} |
||||
name="Default value" |
||||
placeholder="default value, if any" |
||||
onChange={this.onQueryChange} |
||||
onBlur={this.onQueryBlur} |
||||
labelWidth={20} |
||||
grow |
||||
/> |
||||
</VerticalGroup> |
||||
); |
||||
} |
||||
|
||||
export function TextBoxVariableEditor({ onPropChange, variable: { query } }: Props): ReactElement { |
||||
const updateVariable = useCallback( |
||||
(event: ChangeEvent<HTMLInputElement>, 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<HTMLInputElement>) => updateVariable(e, false), [updateVariable]); |
||||
const onBlur = useCallback((e: ChangeEvent<HTMLInputElement>) => updateVariable(e, true), [updateVariable]); |
||||
|
||||
return ( |
||||
<VerticalGroup spacing="xs"> |
||||
<VariableSectionHeader name="Text Options" /> |
||||
<VariableTextField |
||||
value={query} |
||||
name="Default value" |
||||
placeholder="default value, if any" |
||||
onChange={onChange} |
||||
onBlur={onBlur} |
||||
labelWidth={20} |
||||
grow |
||||
ariaLabel={selectors.pages.Dashboard.Settings.Variables.Edit.TextBoxVariable.textBoxOptionsQueryInput} |
||||
/> |
||||
</VerticalGroup> |
||||
); |
||||
} |
||||
|
||||
@ -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<TextBoxVariableModel> {} |
||||
|
||||
export class TextBoxVariablePicker extends PureComponent<Props> { |
||||
onQueryChange = (event: ChangeEvent<HTMLInputElement>) => { |
||||
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<HTMLInputElement>) => { |
||||
if (this.props.variable.current.value !== this.props.variable.query) { |
||||
dispatch(updateOptions(toVariableIdentifier(this.props.variable))); |
||||
} |
||||
}; |
||||
const onChange = useCallback((event: ChangeEvent<HTMLInputElement>) => setUpdatedValue(event.target.value), [ |
||||
setUpdatedValue, |
||||
]); |
||||
|
||||
onQueryKeyDown = (event: KeyboardEvent<HTMLInputElement>) => { |
||||
if (event.keyCode === 13 && this.props.variable.current.value !== this.props.variable.query) { |
||||
dispatch(updateOptions(toVariableIdentifier(this.props.variable))); |
||||
const onBlur = (e: FocusEvent<HTMLInputElement>) => updateVariable(); |
||||
const onKeyDown = (event: KeyboardEvent<HTMLInputElement>) => { |
||||
if (event.keyCode === 13) { |
||||
updateVariable(); |
||||
} |
||||
}; |
||||
|
||||
render() { |
||||
return ( |
||||
<input |
||||
type="text" |
||||
value={this.props.variable.query} |
||||
className="gf-form-input width-12" |
||||
onChange={this.onQueryChange} |
||||
onBlur={this.onQueryBlur} |
||||
onKeyDown={this.onQueryKeyDown} |
||||
/> |
||||
); |
||||
} |
||||
return <Input type="text" value={updatedValue} onChange={onChange} onBlur={onBlur} onKeyDown={onKeyDown} />; |
||||
} |
||||
|
||||
@ -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' }); |
||||
}); |
||||
}); |
||||
}); |
||||
}); |
||||
Loading…
Reference in new issue