Variables: Adds description field (#29332)

* Variables: Adds description field

* Refactor: Adds new Form components

* Refactor: Fixes aria labels

* Refactor: removes skipped tests

* Refactor: Breaks out smaller select components

* Refactor: removes gf-form div

* Refactor: Breaks up several more selects into smaller components

* Chore: Fixes typings
This commit is contained in:
Hugo Häggmark 2020-11-25 10:21:48 +01:00 committed by GitHub
parent b7dc6a1a22
commit 04d857dfe6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 868 additions and 1275 deletions

View File

@ -1,710 +0,0 @@
import { e2e } from '@grafana/e2e';
// skipped scenario helper because of some perf issue upgrading cypress to 4.5.0 and splitted the whole test into smaller
// several it functions. Very important to keep the order of these it functions because they have dependency in the order
// https://github.com/cypress-io/cypress/issues/5987
// https://github.com/cypress-io/cypress/issues/6023#issuecomment-574031655
describe.skip('Variables', () => {
let lastUid = '';
let lastData = '';
let variables: VariablesData[] = [
{ name: 'query1', query: '*', label: 'query1-label', options: ['All', 'A', 'B', 'C'], selectedOption: 'A' },
{
name: 'query2',
query: '$query1.*',
label: 'query2-label',
options: ['All', 'AA', 'AB', 'AC'],
selectedOption: 'AA',
},
{
name: 'query3',
query: '$query1.$query2.*',
label: 'query3-label',
options: ['All', 'AAA', 'AAB', 'AAC'],
selectedOption: 'AAA',
},
];
beforeEach(() => {
e2e.flows.login('admin', 'admin');
if (!lastUid || !lastData) {
e2e.flows.addDataSource();
e2e.flows.addDashboard();
lastUid = 'test';
lastData = 'test';
} else {
e2e.flows.openDashboard();
}
});
it(`asserts defaults`, () => {
e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings').click();
e2e.pages.Dashboard.Settings.General.sectionItems('Variables').click();
e2e.pages.Dashboard.Settings.Variables.List.addVariableCTA().click();
assertDefaultsForNewVariable();
});
variables.forEach((variable, index) => {
it(`creates variable ${variable.name}`, () => {
e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings').click();
e2e.pages.Dashboard.Settings.General.sectionItems('Variables').click();
if (index === 0) {
e2e.pages.Dashboard.Settings.Variables.List.addVariableCTA().click();
} else {
e2e.pages.Dashboard.Settings.Variables.List.newButton().click();
}
const { name, label, query, options, selectedOption } = variable;
e2e.getScenarioContext().then(({ lastAddedDataSource }: any) => {
createQueryVariable({
dataSourceName: lastAddedDataSource,
name,
label,
query,
options,
selectedOption,
});
});
e2e.pages.Dashboard.Settings.General.saveDashBoard()
.should('be.visible')
.click();
e2e.pages.SaveDashboardModal.save()
.should('be.visible')
.click();
e2e.flows.assertSuccessNotification();
e2e.components.BackButton.backArrow()
.should('be.visible')
.click();
});
});
it(`asserts submenus`, () => {
assertVariableLabelsAndComponents(variables);
});
it(`asserts variable table`, () => {
e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings')
.should('be.visible')
.click();
e2e.pages.Dashboard.Settings.General.sectionItems('Variables')
.should('be.visible')
.click();
assertVariableTable(variables);
});
it(`asserts variable selects`, () => {
assertSelects(variables);
});
it(`asserts duplicate variable`, () => {
// mutates variables
variables = assertDuplicateItem(variables);
e2e.flows.saveDashboard();
});
it(`asserts delete variable`, () => {
// mutates variables
variables = assertDeleteItem(variables);
e2e.flows.saveDashboard();
});
it(`asserts update variable`, () => {
// mutates variables
variables = assertUpdateItem(variables);
e2e.components.BackButton.backArrow()
.should('be.visible')
.should('be.visible')
.click();
e2e.flows.saveDashboard();
});
it(`asserts move variable down`, () => {
e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings')
.should('be.visible')
.click();
e2e.pages.Dashboard.Settings.General.sectionItems('Variables')
.should('be.visible')
.click();
// mutates variables
variables = assertMoveDownItem(variables);
e2e.flows.saveDashboard();
});
it(`asserts move variable up`, () => {
e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings')
.should('be.visible')
.click();
e2e.pages.Dashboard.Settings.General.sectionItems('Variables')
.should('be.visible')
.click();
// mutates variables
assertMoveUpItem(variables);
});
});
interface VariablesData {
name: string;
query: string;
label: string;
options: string[];
selectedOption: string;
}
interface CreateQueryVariableArguments extends VariablesData {
dataSourceName: string;
}
const assertDefaultsForNewVariable = () => {
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInput().within(input => {
expect(input.attr('placeholder')).equals('name');
expect(input.val()).equals('');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelect().within(select => {
e2e()
.get('option:selected')
.should('have.text', 'Query');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInput().within(input => {
expect(input.attr('placeholder')).equals('optional display name');
expect(input.val()).equals('');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalHideSelect().within(select => {
e2e()
.get('option:selected')
.should('have.text', '');
});
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect().within(select => {
e2e()
.get('option:selected')
.should('have.text', '');
});
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput().should('not.exist');
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRefreshSelect().within(select => {
e2e()
.get('option:selected')
.should('have.text', 'Never');
});
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInput().within(input => {
expect(input.attr('placeholder')).equals('/.*-(.*)-.*/');
expect(input.val()).equals('');
});
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelect().within(select => {
e2e()
.get('option:selected')
.should('have.text', 'Disabled');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch().within(select => {
e2e()
.get('input')
.should('not.be.checked');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch().within(select => {
e2e()
.get('input')
.should('not.be.checked');
});
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.valueGroupsTagsEnabledSwitch().within(select => {
e2e()
.get('input')
.should('not.be.checked');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should('not.exist');
e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput().should('not.exist');
};
const createQueryVariable = ({ name, label, dataSourceName, query }: CreateQueryVariableArguments) => {
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInput().should('be.visible');
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInput().type(name);
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInput().type(label);
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect()
.select(`${dataSourceName}`)
.blur();
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput()
.type(query)
.blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should('exist');
e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch()
.click()
.within(() => {
e2e()
.get('input')
.should('be.checked');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch()
.click()
.within(() => {
e2e()
.get('input')
.should('be.checked');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput().within(input => {
expect(input.attr('placeholder')).equals('blank = auto');
expect(input.val()).equals('');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().click();
};
const assertVariableLabelAndComponent = ({ label, options, selectedOption }: VariablesData) => {
e2e.pages.Dashboard.SubMenu.submenuItemLabels(label).should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts(selectedOption)
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown().should('be.visible');
for (let optionIndex = 0; optionIndex < options.length; optionIndex++) {
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts(options[optionIndex]).should('be.visible');
}
};
const assertVariableLabelsAndComponents = (args: VariablesData[]) => {
e2e.pages.Dashboard.SubMenu.submenuItem().should('have.length', args.length);
for (let index = 0; index < args.length; index++) {
e2e.pages.Dashboard.SubMenu.submenuItem()
.eq(index)
.within(() => {
e2e()
.get('label')
.contains(args[index].name);
});
assertVariableLabelAndComponent(args[index]);
}
};
const assertVariableTableRow = ({ name, query }: VariablesData, index: number, length: number) => {
e2e.pages.Dashboard.Settings.Variables.List.tableRowNameFields(name)
.should('exist')
.contains(name);
e2e.pages.Dashboard.Settings.Variables.List.tableRowDefinitionFields(name)
.should('exist')
.contains(query);
if (index !== length - 1) {
e2e.pages.Dashboard.Settings.Variables.List.tableRowArrowDownButtons(name).should('exist');
}
if (index !== 0) {
e2e.pages.Dashboard.Settings.Variables.List.tableRowArrowUpButtons(name).should('exist');
}
e2e.pages.Dashboard.Settings.Variables.List.tableRowDuplicateButtons(name).should('exist');
e2e.pages.Dashboard.Settings.Variables.List.tableRowRemoveButtons(name).should('exist');
};
const assertVariableTable = (args: VariablesData[]) => {
e2e.pages.Dashboard.Settings.Variables.List.table()
.should('be.visible')
.within(() => {
e2e()
.get('tbody > tr')
.should('have.length', args.length);
});
for (let index = 0; index < args.length; index++) {
assertVariableTableRow(args[index], index, args.length);
}
};
const assertSelects = (variables: VariablesData[]) => {
// Values in submenus should be
// query1: [A] query2: [AA] query3: [AAA]
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A')
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A')
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B')
.should('be.visible')
.click();
e2e.pages.Dashboard.Toolbar.navBar().click();
// Values in submenus should be
// query1: [B] query2: [All] query3: [All]
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All')
.should('be.visible')
.should('have.length', 2);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All')
.eq(0)
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB')
.should('be.visible')
.click();
e2e.pages.Dashboard.Toolbar.navBar()
.should('be.visible')
.click();
// Values in submenus should be
// query1: [B] query2: [BB] query3: [All]
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All')
.should('be.visible')
.should('have.length', 1);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All')
.eq(0)
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBA').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBC').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB')
.should('be.visible')
.click();
e2e.pages.Dashboard.Toolbar.navBar()
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('have.length', 0);
// Values in submenus should be
// query1: [B] query2: [BB] query3: [BBB]
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB')
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC')
.should('be.visible')
.click();
e2e.pages.Dashboard.Toolbar.navBar()
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB + BC')
.should('be.visible')
.should('have.length', 1);
// Values in submenus should be
// query1: [B] query2: [BB + BC] query3: [BBB]
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB')
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBA').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBB').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BBC').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BCA').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BCB').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BCC').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BCC')
.should('be.visible')
.click();
e2e.pages.Dashboard.Toolbar.navBar()
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BBB + BCC')
.should('be.visible')
.should('have.length', 1);
// Values in submenus should be
// query1: [B] query2: [BB + BC] query3: [BBB + BCC]
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BB + BC')
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC').should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BA')
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BB')
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('BC')
.should('be.visible')
.click();
e2e.pages.Dashboard.Toolbar.navBar()
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('BA')
.should('be.visible')
.should('have.length', 1);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All')
.should('be.visible')
.should('have.length', 1);
// Values in submenus should be
// query1: [B] query2: [BA] query3: [All]
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('B')
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A')
.should('be.visible')
.should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B')
.should('be.visible')
.should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('C')
.should('be.visible')
.should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('A')
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('B')
.should('be.visible')
.click();
e2e.pages.Dashboard.Toolbar.navBar()
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A')
.should('be.visible')
.should('have.length', 1);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All')
.should('be.visible')
.should('have.length', 2);
// Values in submenus should be
// query1: [A] query2: [All] query3: [All]
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All')
.eq(0)
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AA')
.should('be.visible')
.click();
e2e.pages.Dashboard.Toolbar.navBar()
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A')
.should('be.visible')
.should('have.length', 1);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA')
.should('be.visible')
.should('have.length', 1);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All')
.should('be.visible')
.should('have.length', 1);
// Values in submenus should be
// query1: [A] query2: [AA] query3: [All]
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All')
.eq(0)
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts('AAA')
.should('be.visible')
.click();
e2e.pages.Dashboard.Toolbar.navBar()
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('A')
.should('be.visible')
.should('have.length', 1);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AA')
.should('be.visible')
.should('have.length', 1);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('AAA')
.should('be.visible')
.should('have.length', 1);
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts('All').should('have.length', 0);
};
const assertDuplicateItem = (variables: VariablesData[]) => {
const itemToDuplicate = variables[1];
e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings').click();
e2e.pages.Dashboard.Settings.General.sectionItems('Variables').click();
e2e.pages.Dashboard.Settings.Variables.List.tableRowDuplicateButtons(itemToDuplicate.name)
.should('exist')
.click();
e2e.pages.Dashboard.Settings.Variables.List.table()
.should('be.visible')
.within(() => {
e2e()
.get('tbody > tr')
.should('have.length', variables.length + 1);
});
const newItem = { ...itemToDuplicate, name: `copy_of_${itemToDuplicate.name}` };
assertVariableTableRow(newItem, variables.length - 1, variables.length);
e2e.pages.Dashboard.Settings.Variables.List.tableRowNameFields(newItem.name).click();
newItem.label = `copy_of_${itemToDuplicate.label}`;
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInput()
.clear()
.type(newItem.label);
e2e.pages.Dashboard.Settings.General.saveDashBoard().click();
e2e.pages.SaveDashboardModal.save().click();
e2e.flows.assertSuccessNotification();
e2e.components.BackButton.backArrow()
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemLabels(newItem.label).should('be.visible');
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownValueLinkTexts(newItem.selectedOption)
.should('be.visible')
.eq(1)
.click();
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownDropDown().should('be.visible');
for (let optionIndex = 0; optionIndex < newItem.options.length; optionIndex++) {
e2e.pages.Dashboard.SubMenu.submenuItemValueDropDownOptionTexts(newItem.options[optionIndex]).should('be.visible');
}
return [...variables, newItem];
};
const assertDeleteItem = (variables: VariablesData[]) => {
const itemToDelete = variables[1];
e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings').click();
e2e.pages.Dashboard.Settings.General.sectionItems('Variables').click();
e2e.pages.Dashboard.Settings.Variables.List.tableRowRemoveButtons(itemToDelete.name).click();
e2e.pages.Dashboard.Settings.Variables.List.table()
.should('be.visible')
.within(() => {
e2e()
.get('tbody > tr')
.should('have.length', variables.length - 1);
});
e2e.pages.Dashboard.Settings.General.saveDashBoard().click();
e2e.pages.SaveDashboardModal.save().click();
e2e.flows.assertSuccessNotification();
e2e.components.BackButton.backArrow()
.should('be.visible')
.click();
e2e.pages.Dashboard.SubMenu.submenuItemLabels(itemToDelete.label).should('not.exist');
return variables.filter(item => item.name !== itemToDelete.name);
};
const assertUpdateItem = (data: VariablesData[]) => {
const variables = [...data];
// updates an item to a constant variable instead
const itemToUpdate = variables[1];
let updatedItem = {
...itemToUpdate,
name: `update_of_${itemToUpdate.name}`,
label: `update_of_${itemToUpdate.label}`,
query: 'A constant',
options: ['A constant'],
selectedOption: 'undefined',
};
variables[1] = updatedItem;
e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings').click();
e2e.pages.Dashboard.Settings.General.sectionItems('Variables').click();
e2e.pages.Dashboard.Settings.Variables.List.tableRowNameFields(itemToUpdate.name).click();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInput().should('be.visible');
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInput()
.should('have.value', itemToUpdate.name)
.clear()
.type(updatedItem.name);
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInput()
.should('have.value', itemToUpdate.label)
.clear()
.type(updatedItem.label);
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelect().select('Constant');
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalHideSelect().within(select => {
e2e()
.get('option:selected')
.should('have.text', 'Variable');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalHideSelect().select('');
e2e.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInput().type(updatedItem.query);
e2e.components.BackButton.backArrow()
.should('be.visible')
.click();
variables[1].selectedOption = 'A constant';
assertVariableLabelAndComponent(variables[1]);
e2e.pages.Dashboard.Toolbar.toolbarItems('Dashboard settings').click();
e2e.pages.Dashboard.Settings.General.sectionItems('Variables').click();
assertVariableTableRow(variables[1], 1, variables.length);
variables[1].selectedOption = 'A constant';
return variables;
};
const assertMoveDownItem = (data: VariablesData[]) => {
const variables = [...data];
e2e.pages.Dashboard.Settings.Variables.List.tableRowArrowDownButtons(variables[0].name).click();
const temp = { ...variables[0] };
variables[0] = { ...variables[1] };
variables[1] = temp;
e2e.pages.Dashboard.Settings.Variables.List.table().within(() => {
e2e()
.get('tbody > tr')
.eq(0)
.within(() => {
e2e()
.get('td')
.eq(0)
.contains(variables[0].name);
e2e()
.get('td')
.eq(1)
.contains(variables[0].query);
});
e2e()
.get('tbody > tr')
.eq(1)
.within(() => {
e2e()
.get('td')
.eq(0)
.contains(variables[1].name);
e2e()
.get('td')
.eq(1)
.contains(variables[1].query);
});
});
e2e.components.BackButton.backArrow()
.should('be.visible')
.click();
assertVariableLabelsAndComponents(variables);
return variables;
};
const assertMoveUpItem = (data: VariablesData[]) => {
const variables = [...data];
e2e.pages.Dashboard.Settings.Variables.List.tableRowArrowUpButtons(variables[1].name).click();
const temp = { ...variables[0] };
variables[0] = { ...variables[1] };
variables[1] = temp;
e2e.pages.Dashboard.Settings.Variables.List.table().within(() => {
e2e()
.get('tbody > tr')
.eq(0)
.within(() => {
e2e()
.get('td')
.eq(0)
.contains(variables[0].name);
e2e()
.get('td')
.eq(1)
.contains(variables[0].query);
});
e2e()
.get('tbody > tr')
.eq(1)
.within(() => {
e2e()
.get('td')
.eq(0)
.contains(variables[1].name);
e2e()
.get('td')
.eq(1)
.contains(variables[1].query);
});
});
e2e.components.BackButton.backArrow()
.should('be.visible')
.click();
assertVariableLabelsAndComponents(variables);
return variables;
};

View File

@ -1,4 +1,4 @@
import React, { FC, ReactNode, HTMLProps } from 'react';
import React, { FC, HTMLProps, ReactNode } from 'react';
import { css, cx } from 'emotion';
import { useStyles } from '../../themes';
@ -18,6 +18,7 @@ export const InlineFieldRow: FC<Props> = ({ children, className, ...htmlProps })
const getStyles = () => {
return {
container: css`
label: InlineFieldRow;
display: flex;
flex-direction: row;
flex-wrap: wrap;

View File

@ -8,6 +8,10 @@ import { AdHocVariableEditorState } from './reducer';
import { changeVariableDatasource, initAdHocVariableEditor } from './actions';
import { connectWithStore } from 'app/core/utils/connectWithReduxStore';
import { StoreState } from 'app/types';
import { Alert, InlineFieldRow, VerticalGroup } from '@grafana/ui';
import { VariableSectionHeader } from '../editor/VariableSectionHeader';
import { VariableSelectField } from '../editor/VariableSelectField';
import { SelectableValue } from '@grafana/data';
export interface OwnProps extends VariableEditorProps<AdHocVariableModel> {}
@ -27,45 +31,33 @@ export class AdHocVariableEditorUnConnected extends PureComponent<Props> {
this.props.initAdHocVariableEditor();
}
onDatasourceChanged = (event: React.ChangeEvent<HTMLSelectElement>) => {
this.props.changeVariableDatasource(event.target.value);
onDatasourceChanged = (option: SelectableValue<string>) => {
this.props.changeVariableDatasource(option.value ?? '');
};
render() {
const { variable, editor } = this.props;
const dataSources = editor.extended?.dataSources ?? [];
const infoText = editor.extended?.infoText ?? null;
const options = dataSources.map(ds => ({ label: ds.text, value: ds.value ?? '' }));
const value = options.find(o => o.value === variable.datasource) ?? options[0];
return (
<>
<div className="gf-form-group">
<h5 className="section-heading">Options</h5>
<div className="gf-form max-width-21">
<span className="gf-form-label width-8">Data source</span>
<div className="gf-form-select-wrapper max-width-14">
<select
className="gf-form-input"
required
<VerticalGroup spacing="xs">
<VariableSectionHeader name="Options" />
<VerticalGroup spacing="sm">
<InlineFieldRow>
<VariableSelectField
name="Data source"
value={value}
options={options}
onChange={this.onDatasourceChanged}
value={variable.datasource ?? ''}
aria-label="Variable editor Form AdHoc DataSource select"
>
{dataSources.map(ds => (
<option key={ds.value ?? ''} value={ds.value ?? ''} label={ds.text}>
{ds.text}
</option>
))}
</select>
</div>
</div>
</div>
{infoText && (
<div className="alert alert-info gf-form-group" aria-label="Variable editor Form Alert">
{infoText}
</div>
)}
</>
labelWidth={10}
/>
</InlineFieldRow>
{infoText ? <Alert title={infoText} severity="info" /> : null}
</VerticalGroup>
</VerticalGroup>
);
}
}

View File

@ -1,8 +1,11 @@
import React, { ChangeEvent, FocusEvent, PureComponent } from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { VerticalGroup } from '@grafana/ui';
import { ConstantVariableModel } from '../types';
import { VariableEditorProps } from '../editor/types';
import { VariableSectionHeader } from '../editor/VariableSectionHeader';
import { VariableTextField } from '../editor/VariableTextField';
export interface Props extends VariableEditorProps<ConstantVariableModel> {}
@ -24,23 +27,19 @@ export class ConstantVariableEditor extends PureComponent<Props> {
render() {
return (
<>
<div className="gf-form-group">
<h5 className="section-heading">Constant options</h5>
<div className="gf-form">
<span className="gf-form-label">Value</span>
<input
type="text"
className="gf-form-input"
<VerticalGroup spacing="xs">
<VariableSectionHeader name="Constant Options" />
<VariableTextField
value={this.props.variable.query}
name="Value"
placeholder="your metric prefix"
onChange={this.onChange}
onBlur={this.onBlur}
placeholder="your metric prefix"
aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInput}
labelWidth={20}
ariaLabel={selectors.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInput}
grow
/>
</div>
</div>
</>
</VerticalGroup>
);
}
}

View File

@ -4,9 +4,11 @@ import { SelectionOptionsEditor } from '../editor/SelectionOptionsEditor';
import { OnPropChangeArguments, VariableEditorProps } from '../editor/types';
import { connectWithStore } from 'app/core/utils/connectWithReduxStore';
import { MapDispatchToProps, MapStateToProps } from 'react-redux';
import { Field, TextArea } from '@grafana/ui';
import { VerticalGroup } from '@grafana/ui';
import { StoreState } from 'app/types';
import { changeVariableMultiValue } from '../state/actions';
import { VariableSectionHeader } from '../editor/VariableSectionHeader';
import { VariableTextAreaField } from '../editor/VariableTextAreaField';
interface OwnProps extends VariableEditorProps<CustomVariableModel> {}
@ -40,31 +42,28 @@ class CustomVariableEditorUnconnected extends PureComponent<Props> {
render() {
return (
<>
<div className="gf-form-group">
<h5 className="section-heading">Custom Options</h5>
<div className="gf-form">
<Field label="Values separated by comma">
<TextArea
className="gf-form-input"
<VerticalGroup spacing="xs">
<VariableSectionHeader name="Custom Options" />
<VerticalGroup spacing="md">
<VerticalGroup spacing="none">
<VariableTextAreaField
name="Values separated by comma"
value={this.props.variable.query}
placeholder="1, 10, mykey : myvalue, myvalue, escaped\,value"
onChange={this.onChange}
onBlur={this.onBlur}
rows={5}
cols={81}
placeholder="1, 10, mykey : myvalue, myvalue, escaped\,value"
required
aria-label="Variable editor Form Custom Query field"
width={50}
labelWidth={27}
/>
</Field>
</div>
</div>
</VerticalGroup>
<SelectionOptionsEditor
variable={this.props.variable}
onPropChange={this.onSelectionOptionsChange}
onMultiChanged={this.props.changeVariableMultiValue}
/>
</>
/>{' '}
</VerticalGroup>
</VerticalGroup>
);
}
}

View File

@ -1,16 +1,20 @@
import React, { ChangeEvent, FocusEvent, PureComponent } from 'react';
import { MapDispatchToProps, MapStateToProps } from 'react-redux';
import { InlineFieldRow, VerticalGroup } from '@grafana/ui';
import { DataSourceVariableModel, VariableWithMultiSupport } from '../types';
import { OnPropChangeArguments, VariableEditorProps } from '../editor/types';
import { SelectionOptionsEditor } from '../editor/SelectionOptionsEditor';
import { InlineFormLabel } from '@grafana/ui';
import { VariableEditorState } from '../editor/reducer';
import { DataSourceVariableEditorState } from './reducer';
import { initDataSourceVariableEditor } from './actions';
import { MapDispatchToProps, MapStateToProps } from 'react-redux';
import { StoreState } from '../../../types';
import { connectWithStore } from '../../../core/utils/connectWithReduxStore';
import { changeVariableMultiValue } from '../state/actions';
import { VariableSectionHeader } from '../editor/VariableSectionHeader';
import { VariableSelectField } from '../editor/VariableSelectField';
import { SelectableValue } from '@grafana/data';
import { VariableTextField } from '../editor/VariableTextField';
export interface OwnProps extends VariableEditorProps<DataSourceVariableModel> {}
@ -58,37 +62,38 @@ export class DataSourceVariableEditorUnConnected extends PureComponent<Props> {
return value ?? '';
};
onDataSourceTypeChanged = (event: ChangeEvent<HTMLSelectElement>) => {
this.props.onPropChange({ propName: 'query', propValue: event.target.value, updateOptions: true });
onDataSourceTypeChanged = (option: SelectableValue<string>) => {
this.props.onPropChange({ propName: 'query', propValue: option.value, updateOptions: true });
};
render() {
const typeOptions = this.props.editor.extended?.dataSourceTypes?.length
? this.props.editor.extended?.dataSourceTypes?.map(ds => ({ value: ds.value ?? '', label: ds.text }))
: [];
const typeValue = typeOptions.find(o => o.value === this.props.variable.query) ?? typeOptions[0];
return (
<>
<div className="gf-form-group">
<h5 className="section-heading">Data source options</h5>
<div className="gf-form">
<label className="gf-form-label width-12">Type</label>
<div className="gf-form-select-wrapper max-width-18">
<select
className="gf-form-input"
value={this.getSelectedDataSourceTypeValue()}
<VerticalGroup spacing="xs">
<VariableSectionHeader name="Data source options" />
<VerticalGroup spacing="md">
<VerticalGroup spacing="xs">
<InlineFieldRow>
<VariableSelectField
name="Type"
value={typeValue}
options={typeOptions}
onChange={this.onDataSourceTypeChanged}
>
{this.props.editor.extended?.dataSourceTypes?.length &&
this.props.editor.extended?.dataSourceTypes?.map(ds => (
<option key={ds.value ?? ''} value={ds.value ?? ''} label={ds.text}>
{ds.text}
</option>
))}
</select>
</div>
</div>
<div className="gf-form">
<InlineFormLabel
width={12}
labelWidth={10}
/>
</InlineFieldRow>
<InlineFieldRow>
<VariableTextField
value={this.props.variable.regex}
name="Instance name filter"
placeholder="/.*-(.*)-.*/"
onChange={this.onRegExChange}
onBlur={this.onRegExBlur}
labelWidth={20}
tooltip={
<div>
Regex filter for which data source instances to choose from in the variable value dropdown. Leave
@ -98,26 +103,17 @@ export class DataSourceVariableEditorUnConnected extends PureComponent<Props> {
Example: <code>/^prod/</code>
</div>
}
>
Instance name filter
</InlineFormLabel>
<input
type="text"
className="gf-form-input max-width-18"
placeholder="/.*-(.*)-.*/"
value={this.props.variable.regex}
onChange={this.onRegExChange}
onBlur={this.onRegExBlur}
/>
</div>
</div>
</InlineFieldRow>
</VerticalGroup>
<SelectionOptionsEditor
variable={this.props.variable}
onPropChange={this.onSelectionOptionsChange}
onMultiChanged={this.props.changeVariableMultiValue}
/>
</>
</VerticalGroup>
</VerticalGroup>
);
}
}

View File

@ -2,7 +2,8 @@ import React, { FC, useCallback, useState } from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { VariableQueryProps } from 'app/types/plugins';
import { InlineField, TextArea, useStyles } from '@grafana/ui';
import { VariableTextAreaField } from './VariableTextAreaField';
import { useStyles } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/data';
import { css } from 'emotion';
@ -17,6 +18,7 @@ export const LegacyVariableQueryEditor: FC<VariableQueryProps> = ({ onChange, qu
},
[onChange]
);
const onBlur = useCallback(
(event: React.FormEvent<HTMLTextAreaElement>) => {
onChange(event.currentTarget.value, event.currentTarget.value);
@ -25,19 +27,17 @@ export const LegacyVariableQueryEditor: FC<VariableQueryProps> = ({ onChange, qu
);
return (
<div className="gf-form">
<InlineField label="Query" labelWidth={20} grow={false} className={styles.inlineFieldOverride}>
<span hidden />
</InlineField>
<TextArea
rows={getLineCount(value)}
className="gf-form-input"
<div className={styles.container}>
<VariableTextAreaField
name="Query"
value={value}
placeholder="metric name or tags query"
width={100}
onChange={onValueChange}
onBlur={onBlur}
placeholder="metric name or tags query"
required
aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput}
labelWidth={20}
ariaLabel={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput}
/>
</div>
);
@ -45,18 +45,10 @@ export const LegacyVariableQueryEditor: FC<VariableQueryProps> = ({ onChange, qu
function getStyles(theme: GrafanaTheme) {
return {
inlineFieldOverride: css`
margin: 0;
container: css`
margin-bottom: ${theme.spacing.xs};
`,
};
}
LegacyVariableQueryEditor.displayName = LEGACY_VARIABLE_QUERY_EDITOR_NAME;
const getLineCount = (value: any) => {
if (value && typeof value === 'string') {
return value.split('\n').length;
}
return 1;
};

View File

@ -1,12 +1,13 @@
import React, { FunctionComponent, useCallback } from 'react';
import { LegacyForms } from '@grafana/ui';
import { InlineFieldRow, VerticalGroup } from '@grafana/ui';
import { selectors } from '@grafana/e2e-selectors';
import { VariableWithMultiSupport } from '../types';
import { VariableEditorProps } from './types';
import { toVariableIdentifier, VariableIdentifier } from '../state/types';
const { Switch } = LegacyForms;
import { VariableSectionHeader } from './VariableSectionHeader';
import { VariableSwitchField } from './VariableSwitchField';
import { VariableTextField } from './VariableTextField';
export interface SelectionOptionsEditorProps<Model extends VariableWithMultiSupport = VariableWithMultiSupport>
extends VariableEditorProps<Model> {
@ -35,42 +36,39 @@ export const SelectionOptionsEditor: FunctionComponent<SelectionOptionsEditorPro
[props.onPropChange]
);
return (
<div className="section gf-form-group">
<h5 className="section-heading">Selection Options</h5>
<div className="section">
<div aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch}>
<Switch
label="Multi-value"
labelClass="width-10"
checked={props.variable.multi}
<VerticalGroup spacing="none">
<VariableSectionHeader name="Selection Options" />
<InlineFieldRow>
<VariableSwitchField
value={props.variable.multi}
name="Multi-value"
tooltip="Enables multiple values to be selected at the same time"
onChange={onMultiChanged}
tooltip={'Enables multiple values to be selected at the same time'}
ariaLabel={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch}
/>
</div>
<div aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch}>
<Switch
label="Include All option"
labelClass="width-10"
checked={props.variable.includeAll}
</InlineFieldRow>
<InlineFieldRow>
<VariableSwitchField
value={props.variable.includeAll}
name="Include All option"
tooltip="Enables an option to include all variables"
onChange={onIncludeAllChanged}
tooltip={'Enables an option to include all variables'}
ariaLabel={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch}
/>
</div>
</div>
</InlineFieldRow>
{props.variable.includeAll && (
<div className="gf-form">
<span className="gf-form-label width-10">Custom all value</span>
<input
type="text"
className="gf-form-input max-width-15"
<InlineFieldRow>
<VariableTextField
value={props.variable.allValue ?? ''}
onChange={onAllValueChanged}
name="Custom all value"
placeholder="blank = auto"
aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput}
ariaLabel={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInput}
labelWidth={20}
/>
</div>
</InlineFieldRow>
)}
</div>
</VerticalGroup>
);
};
SelectionOptionsEditor.displayName = 'SelectionOptionsEditor';

View File

@ -1,7 +1,7 @@
import React, { ChangeEvent, FormEvent, PureComponent } from 'react';
import isEqual from 'lodash/isEqual';
import { AppEvents, LoadingState, VariableType } from '@grafana/data';
import { Icon, InlineFormLabel } from '@grafana/ui';
import { AppEvents, LoadingState, SelectableValue, VariableType } from '@grafana/data';
import { Button, Icon, InlineFieldRow, VerticalGroup } from '@grafana/ui';
import { selectors } from '@grafana/e2e-selectors';
import { variableAdapters } from '../adapters';
@ -18,7 +18,11 @@ import { connectWithStore } from '../../../core/utils/connectWithReduxStore';
import { OnPropChangeArguments } from './types';
import { changeVariableProp, changeVariableType } from '../state/sharedReducer';
import { updateOptions } from '../state/actions';
import { getVariableTypes } from '../utils';
import { VariableTextField } from './VariableTextField';
import { VariableSectionHeader } from './VariableSectionHeader';
import { hasOptions } from '../guard';
import { VariableTypeSelect } from './VariableTypeSelect';
import { VariableHideSelect } from './VariableHideSelect';
export interface OwnProps {
identifier: VariableIdentifier;
@ -63,11 +67,11 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props> {
this.props.changeVariableName(this.props.identifier, event.target.value);
};
onTypeChange = (event: ChangeEvent<HTMLSelectElement>) => {
event.preventDefault();
this.props.changeVariableType(
toVariablePayload(this.props.identifier, { newType: event.target.value as VariableType })
);
onTypeChange = (option: SelectableValue<VariableType>) => {
if (!option.value) {
return;
}
this.props.changeVariableType(toVariablePayload(this.props.identifier, { newType: option.value }));
};
onLabelChange = (event: ChangeEvent<HTMLInputElement>) => {
@ -77,12 +81,17 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props> {
);
};
onHideChange = (event: ChangeEvent<HTMLSelectElement>) => {
event.preventDefault();
onDescriptionChange = (event: ChangeEvent<HTMLInputElement>) => {
this.props.changeVariableProp(
toVariablePayload(this.props.identifier, { propName: 'description', propValue: event.target.value })
);
};
onHideChange = (option: SelectableValue<VariableHide>) => {
this.props.changeVariableProp(
toVariablePayload(this.props.identifier, {
propName: 'hide',
propValue: parseInt(event.target.value, 10) as VariableHide,
propValue: option.value,
})
);
};
@ -114,42 +123,20 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props> {
return (
<div>
<form aria-label="Variable editor Form" onSubmit={this.onHandleSubmit}>
<h5 className="section-heading">General</h5>
<div className="gf-form-group">
<div className="gf-form-inline">
<div className="gf-form max-width-19">
<span className="gf-form-label width-6">Name</span>
<input
type="text"
className="gf-form-input"
name="name"
placeholder="name"
required
<VerticalGroup spacing="lg">
<VerticalGroup spacing="none">
<VariableSectionHeader name="General" />
<InlineFieldRow>
<VariableTextField
value={this.props.editor.name}
onChange={this.onNameChange}
aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInput}
name="Name"
placeholder="name"
required
ariaLabel={selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInput}
/>
</div>
<div className="gf-form max-width-19">
<InlineFormLabel width={6} tooltip={variableAdapters.get(this.props.variable.type).description}>
Type
</InlineFormLabel>
<div className="gf-form-select-wrapper max-width-17">
<select
className="gf-form-input"
value={this.props.variable.type}
onChange={this.onTypeChange}
aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelect}
>
{getVariableTypes().map(({ label, value }) => (
<option key={value} label={label} value={value}>
{name}
</option>
))}
</select>
</div>
</div>
</div>
<VariableTypeSelect onChange={this.onTypeChange} type={this.props.variable.type} />
</InlineFieldRow>
{this.props.editor.errors.name && (
<div className="gf-form">
@ -157,57 +144,43 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props> {
</div>
)}
<div className="gf-form-inline">
<div className="gf-form max-width-19">
<span className="gf-form-label width-6">Label</span>
<input
type="text"
className="gf-form-input"
<InlineFieldRow>
<VariableTextField
value={this.props.variable.label ?? ''}
onChange={this.onLabelChange}
name="Label"
placeholder="optional display name"
aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInput}
ariaLabel={selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInput}
/>
</div>
<div className="gf-form max-width-19">
<span className="gf-form-label width-6">Hide</span>
<div className="gf-form-select-wrapper max-width-15">
<select
className="gf-form-input"
value={this.props.variable.hide}
onChange={this.onHideChange}
aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.General.generalHideSelect}
>
<option label="" value={VariableHide.dontHide}>
{''}
</option>
<option label="Label" value={VariableHide.hideLabel}>
Label
</option>
<option label="Variable" value={VariableHide.hideVariable}>
Variable
</option>
</select>
</div>
</div>
</div>
</div>
<VariableHideSelect onChange={this.onHideChange} hide={this.props.variable.hide} />
</InlineFieldRow>
<VariableTextField
name="Description"
value={variable.description ?? ''}
placeholder="descriptive text"
onChange={this.onDescriptionChange}
grow
/>
</VerticalGroup>
{EditorToRender && <EditorToRender variable={this.props.variable} onPropChange={this.onPropChanged} />}
<VariableValuesPreview variable={this.props.variable} />
{hasOptions(this.props.variable) ? <VariableValuesPreview variable={this.props.variable} /> : null}
<div className="gf-form-button-row p-y-0">
<button
<VerticalGroup spacing="none">
<Button
type="submit"
className="btn btn-primary"
aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.General.submitButton}
disabled={loading}
>
Update
{loading ? <Icon className="spin-clockwise" name="sync" size="sm" style={{ marginLeft: '2px' }} /> : null}
</button>
</div>
{loading ? (
<Icon className="spin-clockwise" name="sync" size="sm" style={{ marginLeft: '2px' }} />
) : null}
</Button>
</VerticalGroup>
</VerticalGroup>
</form>
</div>
);

View File

@ -0,0 +1,31 @@
import React, { PropsWithChildren, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { VariableSelectField } from '../editor/VariableSelectField';
import { VariableHide } from '../types';
interface Props {
onChange: (option: SelectableValue<VariableHide>) => void;
hide: VariableHide;
}
const HIDE_OPTIONS = [
{ label: '', value: VariableHide.dontHide },
{ label: 'Label', value: VariableHide.hideLabel },
{ label: 'Variable', value: VariableHide.hideVariable },
];
export function VariableHideSelect({ onChange, hide }: PropsWithChildren<Props>) {
const value = useMemo(() => HIDE_OPTIONS.find(o => o.value === hide) ?? HIDE_OPTIONS[0], [hide]);
return (
<VariableSelectField
name="Hide"
value={value}
options={HIDE_OPTIONS}
onChange={onChange}
ariaLabel={selectors.pages.Dashboard.Settings.Variables.Edit.General.generalHideSelect}
/>
);
}

View File

@ -0,0 +1,24 @@
import React, { PropsWithChildren, ReactElement } from 'react';
import { useStyles } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/data';
import { css } from 'emotion';
interface VariableSectionHeaderProps {
name: string;
}
export function VariableSectionHeader({ name }: PropsWithChildren<VariableSectionHeaderProps>): ReactElement {
const styles = useStyles(getStyles);
return <h5 className={styles.sectionHeading}>{name}</h5>;
}
function getStyles(theme: GrafanaTheme) {
return {
sectionHeading: css`
label: sectionHeading;
font-size: ${theme.typography.size.md};
margin-bottom: ${theme.spacing.sm};
`,
};
}

View File

@ -0,0 +1,53 @@
import React, { PropsWithChildren, ReactElement } from 'react';
import { InlineFormLabel, Select, useStyles } from '@grafana/ui';
import { GrafanaTheme, SelectableValue } from '@grafana/data';
import { css } from 'emotion';
interface VariableSelectFieldProps<T> {
name: string;
value: SelectableValue<T>;
options: Array<SelectableValue<T>>;
onChange: (option: SelectableValue<T>) => void;
tooltip?: string;
ariaLabel?: string;
width?: number;
labelWidth?: number;
}
export function VariableSelectField({
name,
value,
options,
tooltip,
onChange,
ariaLabel,
width,
labelWidth,
}: PropsWithChildren<VariableSelectFieldProps<any>>): ReactElement {
const styles = useStyles(getStyles);
return (
<>
<InlineFormLabel width={labelWidth ?? 6} tooltip={tooltip}>
{name}
</InlineFormLabel>
<div aria-label={ariaLabel}>
<Select
onChange={onChange}
value={value}
width={width ?? 25}
options={options}
className={styles.selectContainer}
/>
</div>
</>
);
}
function getStyles(theme: GrafanaTheme) {
return {
selectContainer: css`
margin-right: ${theme.spacing.xs};
`,
};
}

View File

@ -0,0 +1,39 @@
import React, { ChangeEvent, PropsWithChildren, ReactElement } from 'react';
import { InlineField, Switch, useStyles } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/data';
import { css } from 'emotion';
interface VariableSwitchFieldProps {
value: boolean;
name: string;
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
tooltip?: string;
ariaLabel?: string;
}
export function VariableSwitchField({
value,
name,
tooltip,
onChange,
ariaLabel,
}: PropsWithChildren<VariableSwitchFieldProps>): ReactElement {
const styles = useStyles(getStyles);
return (
<InlineField label={name} labelWidth={20} tooltip={tooltip}>
<div aria-label={ariaLabel} className={styles.switchContainer}>
<Switch label={name} value={value} onChange={onChange} />
</div>
</InlineField>
);
}
function getStyles(theme: GrafanaTheme) {
return {
switchContainer: css`
margin-left: ${theme.spacing.sm};
margin-right: ${theme.spacing.sm};
`,
};
}

View File

@ -0,0 +1,79 @@
import React, { FormEvent, PropsWithChildren, ReactElement, useCallback } from 'react';
import { HorizontalGroup, InlineField, TextArea, useStyles } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/data';
import { css } from 'emotion';
interface VariableTextAreaFieldProps<T> {
name: string;
value: string;
placeholder: string;
onChange: (event: FormEvent<HTMLTextAreaElement>) => void;
width: number;
tooltip?: string;
ariaLabel?: string;
required?: boolean;
labelWidth?: number;
onBlur?: (event: FormEvent<HTMLTextAreaElement>) => void;
}
export function VariableTextAreaField({
name,
value,
placeholder,
tooltip,
onChange,
onBlur,
ariaLabel,
required,
width,
labelWidth,
}: PropsWithChildren<VariableTextAreaFieldProps<any>>): ReactElement {
const styles = useStyles(getStyles);
const getLineCount = useCallback((value: any) => {
if (value && typeof value === 'string') {
return value.split('\n').length;
}
return 1;
}, []);
return (
<HorizontalGroup spacing="none">
<InlineField
label={name}
labelWidth={labelWidth ?? 12}
grow={false}
tooltip={tooltip}
className={styles.inlineFieldOverride}
>
<span hidden />
</InlineField>
<TextArea
rows={getLineCount(value)}
value={value}
onChange={onChange}
onBlur={onBlur}
placeholder={placeholder}
required={required}
aria-label={ariaLabel}
cols={width}
className={styles.textarea}
/>
</HorizontalGroup>
);
}
function getStyles(theme: GrafanaTheme) {
return {
inlineFieldOverride: css`
margin: 0;
`,
textarea: css`
white-space: pre-wrap;
min-height: 32px;
height: auto;
overflow: auto;
padding: 6px 8px;
`,
};
}

View File

@ -0,0 +1,47 @@
import React, { FormEvent, PropsWithChildren, ReactElement } from 'react';
import { InlineField, Input, PopoverContent } from '@grafana/ui';
interface VariableTextFieldProps {
value: string;
name: string;
placeholder: string;
onChange: (event: FormEvent<HTMLInputElement>) => void;
ariaLabel?: string;
tooltip?: PopoverContent;
required?: boolean;
width?: number;
labelWidth?: number;
grow?: boolean;
onBlur?: (event: FormEvent<HTMLInputElement>) => void;
}
export function VariableTextField({
value,
name,
placeholder,
onChange,
ariaLabel,
width,
labelWidth,
required,
onBlur,
tooltip,
grow,
}: PropsWithChildren<VariableTextFieldProps>): ReactElement {
return (
<InlineField label={name} labelWidth={labelWidth ?? 12} tooltip={tooltip} grow={grow}>
<Input
type="text"
id={name}
name={name}
placeholder={placeholder}
value={value}
onChange={onChange}
onBlur={onBlur}
width={grow ? undefined : width ?? 25}
aria-label={ariaLabel}
required={required}
/>
</InlineField>
);
}

View File

@ -0,0 +1,28 @@
import React, { PropsWithChildren, useMemo } from 'react';
import { SelectableValue, VariableType } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { VariableSelectField } from '../editor/VariableSelectField';
import { getVariableTypes } from '../utils';
import { variableAdapters } from '../adapters';
interface Props {
onChange: (option: SelectableValue<VariableType>) => void;
type: VariableType;
}
export function VariableTypeSelect({ onChange, type }: PropsWithChildren<Props>) {
const options = useMemo(() => getVariableTypes(), [getVariableTypes]);
const value = useMemo(() => options.find(o => o.value === type) ?? options[0], [options, type]);
return (
<VariableSelectField
name="Type"
value={value}
options={options}
onChange={onChange}
tooltip={variableAdapters.get(type).description}
ariaLabel={selectors.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelect}
/>
);
}

View File

@ -1,54 +1,67 @@
import React, { useCallback, useEffect, useState } from 'react';
import { VariableModel, VariableOption, VariableWithOptions } from '../types';
import React, { MouseEvent, useCallback, useEffect, useState } from 'react';
import { VariableOption, VariableWithOptions } from '../types';
import { selectors } from '@grafana/e2e-selectors';
import { Button, InlineFieldRow, InlineLabel, useStyles, VerticalGroup } from '@grafana/ui';
import { GrafanaTheme } from '@grafana/data';
import { css } from 'emotion';
export interface VariableValuesPreviewProps {
variable: VariableModel;
variable: VariableWithOptions;
}
export const VariableValuesPreview: React.FunctionComponent<VariableValuesPreviewProps> = ({ variable }) => {
export const VariableValuesPreview: React.FunctionComponent<VariableValuesPreviewProps> = ({
variable: { options },
}) => {
const [previewLimit, setPreviewLimit] = useState(20);
const [previewOptions, setPreviewOptions] = useState<VariableOption[]>([]);
const showMoreOptions = useCallback(() => setPreviewLimit(previewLimit + 20), [previewLimit, setPreviewLimit]);
useEffect(() => {
if (!variable || !variable.hasOwnProperty('options')) {
return;
}
const variableWithOptions = variable as VariableWithOptions;
setPreviewOptions(variableWithOptions.options.slice(0, previewLimit));
}, [previewLimit, variable]);
const showMoreOptions = useCallback(
(event: MouseEvent) => {
event.preventDefault();
setPreviewLimit(previewLimit + 20);
},
[previewLimit, setPreviewLimit]
);
const styles = useStyles(getStyles);
useEffect(() => setPreviewOptions(options.slice(0, previewLimit)), [previewLimit, options]);
if (!previewOptions.length) {
return null;
}
return (
<div className="gf-form-group">
<VerticalGroup spacing="none">
<h5>Preview of values</h5>
<div className="gf-form-inline">
<InlineFieldRow>
{previewOptions.map((o, index) => (
<div className="gf-form" key={`${o.value}-${index}`}>
<span
className="gf-form-label"
aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption}
>
<InlineFieldRow key={`${o.value}-${index}`} className={styles.optionContainer}>
<InlineLabel aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption}>
{o.text}
</span>
</div>
</InlineLabel>
</InlineFieldRow>
))}
{previewOptions.length > previewLimit && (
<div className="gf-form" ng-if="current.options.length > optionsLimit">
<a
className="gf-form-label btn-secondary"
</InlineFieldRow>
{options.length > previewLimit && (
<InlineFieldRow className={styles.optionContainer}>
<Button
onClick={showMoreOptions}
variant="secondary"
size="sm"
aria-label="Variable editor Preview of Values Show More link"
>
Show more
</a>
</div>
</Button>
</InlineFieldRow>
)}
</div>
</div>
</VerticalGroup>
);
};
VariableValuesPreview.displayName = 'VariableValuesPreview';
function getStyles(theme: GrafanaTheme) {
return {
optionContainer: css`
margin-left: ${theme.spacing.xs};
margin-bottom: ${theme.spacing.xs};
`,
};
}

View File

@ -21,6 +21,7 @@ import {
VariableModel,
VariableQueryEditorType,
VariableWithMultiSupport,
VariableWithOptions,
} from './types';
import { VariableQueryProps } from '../../types';
import { LEGACY_VARIABLE_QUERY_EDITOR_NAME } from './editor/LegacyVariableQueryEditor';
@ -42,6 +43,15 @@ export const isMulti = (model: VariableModel): model is VariableWithMultiSupport
return withMulti.hasOwnProperty('multi') && typeof withMulti.multi === 'boolean';
};
export const hasOptions = (model: VariableModel): model is VariableWithOptions => {
if (!model) {
return false;
}
const withOptions = model as VariableWithOptions;
return withOptions.hasOwnProperty('options') && typeof withOptions.options === 'object';
};
interface DataSourceWithLegacyVariableSupport<
TQuery extends DataQuery = DataQuery,
TOptions extends DataSourceJsonData = DataSourceJsonData

View File

@ -1,10 +1,13 @@
import React, { ChangeEvent, FocusEvent, PureComponent } from 'react';
import { InlineFieldRow, VerticalGroup } from '@grafana/ui';
import { IntervalVariableModel } from '../types';
import { VariableEditorProps } from '../editor/types';
import { InlineFormLabel, LegacyForms } from '@grafana/ui';
const { Switch } = LegacyForms;
import { VariableSectionHeader } from '../editor/VariableSectionHeader';
import { VariableTextField } from '../editor/VariableTextField';
import { VariableSwitchField } from '../editor/VariableSwitchField';
import { VariableSelectField } from '../editor/VariableSelectField';
import { SelectableValue } from '@grafana/data';
export interface Props extends VariableEditorProps<IntervalVariableModel> {}
@ -32,10 +35,10 @@ export class IntervalVariableEditor extends PureComponent<Props> {
});
};
onAutoCountChanged = (event: ChangeEvent<HTMLSelectElement>) => {
onAutoCountChanged = (option: SelectableValue<number>) => {
this.props.onPropChange({
propName: 'auto_count',
propValue: event.target.value,
propValue: option.value,
updateOptions: true,
});
};
@ -49,73 +52,59 @@ export class IntervalVariableEditor extends PureComponent<Props> {
};
render() {
return (
<>
<div className="gf-form-group">
<h5 className="section-heading">Interval Options</h5>
const { variable } = this.props;
const stepOptions = [1, 2, 3, 4, 5, 10, 20, 30, 40, 50, 100, 200, 300, 400, 500].map(count => ({
label: `${count}`,
value: count,
}));
const stepValue = stepOptions.find(o => o.value === variable.auto_count) ?? stepOptions[0];
<div className="gf-form">
<span className="gf-form-label width-9">Values</span>
<input
type="text"
className="gf-form-input"
return (
<VerticalGroup spacing="xs">
<VariableSectionHeader name="Interval Options" />
<VerticalGroup spacing="none">
<VariableTextField
value={this.props.variable.query}
name="Values"
placeholder="1m,10m,1h,6h,1d,7d"
onChange={this.onQueryChanged}
onBlur={this.onQueryBlur}
labelWidth={20}
grow
required
/>
</div>
<div className="gf-form-inline">
<Switch
label="Auto Option"
labelClass="width-9"
checked={this.props.variable.auto}
<InlineFieldRow>
<VariableSwitchField
value={this.props.variable.auto}
name="Auto Option"
tooltip="Interval will be dynamically calculated by dividing time range by the count specified"
onChange={this.onAutoChange}
tooltip={'Interval will be dynamically calculated by dividing time range by the count specified'}
/>
{this.props.variable.auto && (
{this.props.variable.auto ? (
<>
<div className="gf-form">
<InlineFormLabel
width={9}
tooltip={'How many times should the current time range be divided to calculate the value'}
>
Step count
</InlineFormLabel>
<div className="gf-form-select-wrapper max-width-10">
<select
className="gf-form-input"
value={this.props.variable.auto_count}
<VariableSelectField
name="Step count"
value={stepValue}
options={stepOptions}
onChange={this.onAutoCountChanged}
>
{[1, 2, 3, 4, 5, 10, 20, 30, 40, 50, 100, 200, 300, 400, 500].map(count => (
<option key={`auto_count_key-${count}`} label={`${count}`}>
{count}
</option>
))}
</select>
</div>
</div>
<div className="gf-form">
<InlineFormLabel width={9} tooltip={'The calculated value will not go below this threshold'}>
Min interval
</InlineFormLabel>
<input
type="text"
className="gf-form-input max-width-10"
value={this.props.variable.auto_min}
onChange={this.onAutoMinChanged}
placeholder="10s"
tooltip="How many times should the current time range be divided to calculate the value"
labelWidth={7}
width={9}
/>
<VariableTextField
value={this.props.variable.auto_min}
name="Min interval"
placeholder="10s"
onChange={this.onAutoMinChanged}
tooltip="The calculated value will not go below this threshold"
labelWidth={13}
width={11}
/>
</div>
</>
)}
</div>
</div>
</>
) : null}
</InlineFieldRow>
</VerticalGroup>
</VerticalGroup>
);
}
}

View File

@ -1,7 +1,8 @@
import React, { FunctionComponent, useMemo } from 'react';
import React, { FunctionComponent, PropsWithChildren, ReactElement, useMemo } from 'react';
import { VariableHide, VariableModel } from '../types';
import { selectors } from '@grafana/e2e-selectors';
import { variableAdapters } from '../adapters';
import { Tooltip } from '@grafana/ui';
interface Props {
variable: VariableModel;
@ -9,7 +10,6 @@ interface Props {
export const PickerRenderer: FunctionComponent<Props> = props => {
const PickerToRender = useMemo(() => variableAdapters.get(props.variable.type).picker, [props.variable]);
const labelOrName = useMemo(() => props.variable.label || props.variable.name, [props.variable]);
if (!props.variable) {
return <div>Couldn't load variable</div>;
@ -17,17 +17,40 @@ export const PickerRenderer: FunctionComponent<Props> = props => {
return (
<div className="gf-form">
{props.variable.hide === VariableHide.dontHide && (
<label
className="gf-form-label gf-form-label--variable"
aria-label={selectors.pages.Dashboard.SubMenu.submenuItemLabels(labelOrName)}
>
{labelOrName}
</label>
)}
<PickerLabel variable={props.variable} />
{props.variable.hide !== VariableHide.hideVariable && PickerToRender && (
<PickerToRender variable={props.variable} />
)}
</div>
);
};
function PickerLabel({ variable }: PropsWithChildren<Props>): ReactElement | null {
const labelOrName = useMemo(() => variable.label || variable.name, [variable]);
if (variable.hide !== VariableHide.dontHide) {
return null;
}
if (variable.description) {
return (
<Tooltip content={variable.description} placement={'bottom'}>
<label
className="gf-form-label gf-form-label--variable"
aria-label={selectors.pages.Dashboard.SubMenu.submenuItemLabels(labelOrName)}
>
{labelOrName}
</label>
</Tooltip>
);
}
return (
<label
className="gf-form-label gf-form-label--variable"
aria-label={selectors.pages.Dashboard.SubMenu.submenuItemLabels(labelOrName)}
>
{labelOrName}
</label>
);
}

View File

@ -0,0 +1,28 @@
import React, { PropsWithChildren, useMemo } from 'react';
import { DataSourceSelectItem, SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { VariableSelectField } from '../editor/VariableSelectField';
interface Props {
onChange: (option: SelectableValue<string>) => void;
datasource: string | null;
dataSources?: DataSourceSelectItem[];
}
export function QueryVariableDatasourceSelect({ onChange, datasource, dataSources }: PropsWithChildren<Props>) {
const options = useMemo(() => {
return dataSources ? dataSources.map(ds => ({ label: ds.name, value: ds.value ?? '' })) : [];
}, [dataSources]);
const value = useMemo(() => options.find(o => o.value === datasource) ?? options[0], [options, datasource]);
return (
<VariableSelectField
name="Data source"
value={value}
options={options}
onChange={onChange}
labelWidth={10}
ariaLabel={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect}
/>
);
}

View File

@ -1,9 +1,9 @@
import React, { ChangeEvent, PureComponent } from 'react';
import { MapDispatchToProps, MapStateToProps } from 'react-redux';
import { InlineFormLabel, LegacyForms } from '@grafana/ui';
import { InlineFieldRow, VerticalGroup } from '@grafana/ui';
import { selectors } from '@grafana/e2e-selectors';
import { getTemplateSrv } from '@grafana/runtime';
import { LoadingState } from '@grafana/data';
import { LoadingState, SelectableValue } from '@grafana/data';
import { SelectionOptionsEditor } from '../editor/SelectionOptionsEditor';
import { QueryVariableModel, VariableRefresh, VariableSort, VariableWithMultiSupport } from '../types';
@ -17,8 +17,12 @@ import { toVariableIdentifier } from '../state/types';
import { changeVariableMultiValue } from '../state/actions';
import { getTimeSrv } from '../../dashboard/services/TimeSrv';
import { isLegacyQueryEditor, isQueryEditor } from '../guard';
const { Switch } = LegacyForms;
import { VariableSectionHeader } from '../editor/VariableSectionHeader';
import { VariableTextField } from '../editor/VariableTextField';
import { VariableSwitchField } from '../editor/VariableSwitchField';
import { QueryVariableDatasourceSelect } from './QueryVariableDatasourceSelect';
import { QueryVariableRefreshSelect } from './QueryVariableRefreshSelect';
import { QueryVariableSortSelect } from './QueryVariableSortSelect';
export interface OwnProps extends VariableEditorProps<QueryVariableModel> {}
@ -61,18 +65,9 @@ export class QueryVariableEditorUnConnected extends PureComponent<Props, State>
}
}
getSelectedDataSourceValue = (): string => {
if (!this.props.editor.extended?.dataSources?.length) {
return '';
}
const foundItem = this.props.editor.extended?.dataSources.find(ds => ds.value === this.props.variable.datasource);
const value = foundItem ? foundItem.value : this.props.editor.extended?.dataSources[0].value;
return value ?? '';
};
onDataSourceChange = (event: ChangeEvent<HTMLSelectElement>) => {
onDataSourceChange = (option: SelectableValue<string>) => {
this.props.onPropChange({ propName: 'query', propValue: '' });
this.props.onPropChange({ propName: 'datasource', propValue: event.target.value });
this.props.onPropChange({ propName: 'datasource', propValue: option.value });
};
onLegacyQueryChange = async (query: any, definition: string) => {
@ -126,12 +121,12 @@ export class QueryVariableEditorUnConnected extends PureComponent<Props, State>
}
};
onRefreshChange = (event: ChangeEvent<HTMLSelectElement>) => {
this.props.onPropChange({ propName: 'refresh', propValue: parseInt(event.target.value, 10) });
onRefreshChange = (option: SelectableValue<VariableRefresh>) => {
this.props.onPropChange({ propName: 'refresh', propValue: option.value });
};
onSortChange = async (event: ChangeEvent<HTMLSelectElement>) => {
this.props.onPropChange({ propName: 'sort', propValue: parseInt(event.target.value, 10), updateOptions: true });
onSortChange = async (option: SelectableValue<VariableSort>) => {
this.props.onPropChange({ propName: 'sort', propValue: option.value, updateOptions: true });
};
onSelectionOptionsChange = async ({ propValue, propName }: OnPropChangeArguments<VariableWithMultiSupport>) => {
@ -185,62 +180,28 @@ export class QueryVariableEditorUnConnected extends PureComponent<Props, State>
render() {
return (
<>
<div className="gf-form-group">
<h5 className="section-heading">Query Options</h5>
<div className="gf-form-inline">
<div className="gf-form max-width-21">
<span className="gf-form-label width-10">Data source</span>
<div className="gf-form-select-wrapper max-width-14">
<select
className="gf-form-input"
value={this.getSelectedDataSourceValue()}
<VerticalGroup spacing="xs">
<VariableSectionHeader name="Query Options" />
<VerticalGroup spacing="md">
<VerticalGroup spacing="none">
<VerticalGroup spacing="xs">
<InlineFieldRow>
<QueryVariableDatasourceSelect
onChange={this.onDataSourceChange}
required
aria-label={
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect
}
>
{this.props.editor.extended?.dataSources?.length &&
this.props.editor.extended?.dataSources.map(ds => (
<option key={ds.value ?? ''} value={ds.value ?? ''} label={ds.name}>
{ds.name}
</option>
))}
</select>
</div>
</div>
<div className="gf-form max-width-22">
<InlineFormLabel width={10} tooltip={'When to update the values of this variable.'}>
Refresh
</InlineFormLabel>
<div className="gf-form-select-wrapper width-15">
<select
className="gf-form-input"
value={this.props.variable.refresh}
onChange={this.onRefreshChange}
aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRefreshSelect}
>
<option label="Never" value={VariableRefresh.never}>
Never
</option>
<option label="On Dashboard Load" value={VariableRefresh.onDashboardLoad}>
On Dashboard Load
</option>
<option label="On Time Range Change" value={VariableRefresh.onTimeRangeChanged}>
On Time Range Change
</option>
</select>
</div>
</div>
</div>
{this.renderQueryEditor()}
<div className="gf-form">
<InlineFormLabel
width={10}
datasource={this.props.variable.datasource}
dataSources={this.props.editor.extended?.dataSources}
/>
<QueryVariableRefreshSelect onChange={this.onRefreshChange} refresh={this.props.variable.refresh} />
</InlineFieldRow>
<div style={{ flexDirection: 'column' }}>{this.renderQueryEditor()}</div>
</VerticalGroup>
<VariableTextField
value={this.state.regex ?? this.props.variable.regex}
name="Regex"
placeholder="/.*-(?<text>.*)-(?<value>.*)-.*/"
onChange={this.onRegExChange}
onBlur={this.onRegExBlur}
labelWidth={20}
tooltip={
<div>
Optional, if you want to extract part of a series name or metric node segment. Named capture groups
@ -254,61 +215,11 @@ export class QueryVariableEditorUnConnected extends PureComponent<Props, State>
).
</div>
}
>
Regex
</InlineFormLabel>
<input
type="text"
className="gf-form-input"
placeholder="/.*-(?<text>.*)-(?<value>.*)-.*/"
value={this.state.regex ?? this.props.variable.regex}
onChange={this.onRegExChange}
onBlur={this.onRegExBlur}
aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInput}
ariaLabel={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInput}
grow
/>
</div>
<div className="gf-form max-width-21">
<InlineFormLabel width={10} tooltip={'How to sort the values of this variable.'}>
Sort
</InlineFormLabel>
<div className="gf-form-select-wrapper max-width-14">
<select
className="gf-form-input"
value={this.props.variable.sort}
onChange={this.onSortChange}
aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelect}
>
<option label="Disabled" value={VariableSort.disabled}>
Disabled
</option>
<option label="Alphabetical (asc)" value={VariableSort.alphabeticalAsc}>
Alphabetical (asc)
</option>
<option label="Alphabetical (desc)" value={VariableSort.alphabeticalDesc}>
Alphabetical (desc)
</option>
<option label="Numerical (asc)" value={VariableSort.numericalAsc}>
Numerical (asc)
</option>
<option label="Numerical (desc)" value={VariableSort.numericalDesc}>
Numerical (desc)
</option>
<option
label="Alphabetical (case-insensitive, asc)"
value={VariableSort.alphabeticalCaseInsensitiveAsc}
>
Alphabetical (case-insensitive, asc)
</option>
<option
label="Alphabetical (case-insensitive, desc)"
value={VariableSort.alphabeticalCaseInsensitiveDesc}
>
Alphabetical (case-insensitive, desc)
</option>
</select>
</div>
</div>
</div>
<QueryVariableSortSelect onChange={this.onSortChange} sort={this.props.variable.sort} />
</VerticalGroup>
<SelectionOptionsEditor
variable={this.props.variable}
@ -316,52 +227,45 @@ export class QueryVariableEditorUnConnected extends PureComponent<Props, State>
onMultiChanged={this.props.changeVariableMultiValue}
/>
<div className="gf-form-group">
<VerticalGroup spacing="none">
<h5>Value groups/tags (Experimental feature)</h5>
<div
aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.valueGroupsTagsEnabledSwitch}
>
<Switch
label="Enabled"
label-class="width-10"
checked={this.props.variable.useTags}
<VariableSwitchField
value={this.props.variable.useTags}
name="Enabled"
onChange={this.onUseTagsChange}
ariaLabel={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.valueGroupsTagsEnabledSwitch}
/>
</div>
{this.props.variable.useTags && (
<>
<div className="gf-form last">
<span className="gf-form-label width-10">Tags query</span>
<input
type="text"
className="gf-form-input"
{this.props.variable.useTags ? (
<VerticalGroup spacing="none">
<VariableTextField
value={this.state.tagsQuery ?? this.props.variable.tagsQuery}
name="Tags query"
placeholder="metric name or tags query"
onChange={this.onTagsQueryChange}
onBlur={this.onTagsQueryBlur}
aria-label={
ariaLabel={
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.valueGroupsTagsTagsQueryInput
}
labelWidth={20}
grow
/>
</div>
<div className="gf-form">
<li className="gf-form-label width-10">Tag values query</li>
<input
type="text"
className="gf-form-input"
<VariableTextField
value={this.state.tagValuesQuery ?? this.props.variable.tagValuesQuery}
name="Tag values query"
placeholder="apps.$tag.*"
onChange={this.onTagValuesQueryChange}
onBlur={this.onTagValuesQueryBlur}
aria-label={
ariaLabel={
selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.valueGroupsTagsTagsValuesQueryInput
}
labelWidth={20}
grow
/>
</div>
</>
)}
</div>
</>
</VerticalGroup>
) : null}
</VerticalGroup>
</VerticalGroup>
</VerticalGroup>
);
}
}

View File

@ -0,0 +1,32 @@
import React, { PropsWithChildren, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { VariableSelectField } from '../editor/VariableSelectField';
import { VariableRefresh } from '../types';
interface Props {
onChange: (option: SelectableValue<VariableRefresh>) => void;
refresh: VariableRefresh;
}
const REFRESH_OPTIONS = [
{ label: 'Never', value: VariableRefresh.never },
{ label: 'On Dashboard Load', value: VariableRefresh.onDashboardLoad },
{ label: 'On Time Range Change', value: VariableRefresh.onTimeRangeChanged },
];
export function QueryVariableRefreshSelect({ onChange, refresh }: PropsWithChildren<Props>) {
const value = useMemo(() => REFRESH_OPTIONS.find(o => o.value === refresh) ?? REFRESH_OPTIONS[0], [refresh]);
return (
<VariableSelectField
name="Refresh"
value={value}
options={REFRESH_OPTIONS}
onChange={onChange}
labelWidth={10}
ariaLabel={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRefreshSelect}
tooltip="When to update the values of this variable."
/>
);
}

View File

@ -0,0 +1,36 @@
import React, { PropsWithChildren, useMemo } from 'react';
import { SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { VariableSelectField } from '../editor/VariableSelectField';
import { VariableSort } from '../types';
interface Props {
onChange: (option: SelectableValue<VariableSort>) => void;
sort: VariableSort;
}
const SORT_OPTIONS = [
{ label: 'Disabled', value: VariableSort.disabled },
{ label: 'Alphabetical (asc)', value: VariableSort.alphabeticalAsc },
{ label: 'Alphabetical (desc)', value: VariableSort.alphabeticalDesc },
{ label: 'Numerical (asc)', value: VariableSort.numericalAsc },
{ label: 'Numerical (desc)', value: VariableSort.numericalDesc },
{ label: 'Alphabetical (case-insensitive, asc)', value: VariableSort.alphabeticalCaseInsensitiveAsc },
{ label: 'Alphabetical (case-insensitive, desc)', value: VariableSort.alphabeticalCaseInsensitiveDesc },
];
export function QueryVariableSortSelect({ onChange, sort }: PropsWithChildren<Props>) {
const value = useMemo(() => SORT_OPTIONS.find(o => o.value === sort) ?? SORT_OPTIONS[0], [sort]);
return (
<VariableSelectField
name="Sort"
value={value}
options={SORT_OPTIONS}
onChange={onChange}
labelWidth={10}
ariaLabel={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelect}
tooltip="How to sort the values of this variable."
/>
);
}

View File

@ -756,6 +756,7 @@ function createVariable(extend?: Partial<QueryVariableModel>): QueryVariableMode
includeAll: true,
state: LoadingState.NotStarted,
error: null,
description: null,
...(extend ?? {}),
};
}

View File

@ -28,6 +28,7 @@ export const getVariableState = (
global: false,
state: LoadingState.NotStarted,
error: null,
description: null,
};
}
@ -43,6 +44,7 @@ export const getVariableState = (
global: false,
state: LoadingState.NotStarted,
error: null,
description: null,
};
}

View File

@ -71,6 +71,7 @@ describe('sharedReducer', () => {
global: false,
state: LoadingState.NotStarted,
error: null,
description: null,
},
'2': {
id: '2',
@ -83,6 +84,7 @@ describe('sharedReducer', () => {
global: false,
state: LoadingState.NotStarted,
error: null,
description: null,
},
});
});
@ -107,6 +109,7 @@ describe('sharedReducer', () => {
global: false,
state: LoadingState.NotStarted,
error: null,
description: null,
},
'2': {
id: '2',
@ -119,6 +122,7 @@ describe('sharedReducer', () => {
global: false,
state: LoadingState.NotStarted,
error: null,
description: null,
},
});
});
@ -143,6 +147,7 @@ describe('sharedReducer', () => {
global: false,
state: LoadingState.NotStarted,
error: null,
description: null,
},
'1': {
id: '1',
@ -155,6 +160,7 @@ describe('sharedReducer', () => {
global: false,
state: LoadingState.NotStarted,
error: null,
description: null,
},
'2': {
id: '2',
@ -167,6 +173,7 @@ describe('sharedReducer', () => {
global: false,
state: LoadingState.NotStarted,
error: null,
description: null,
},
'11': {
...initialQueryVariableModelState,
@ -198,6 +205,7 @@ describe('sharedReducer', () => {
global: false,
state: LoadingState.NotStarted,
error: null,
description: null,
},
'1': {
id: '1',
@ -210,6 +218,7 @@ describe('sharedReducer', () => {
global: false,
state: LoadingState.NotStarted,
error: null,
description: null,
},
'2': {
id: '2',
@ -222,6 +231,7 @@ describe('sharedReducer', () => {
global: false,
state: LoadingState.NotStarted,
error: null,
description: null,
},
});
});

View File

@ -1,6 +1,10 @@
import React, { ChangeEvent, PureComponent } 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';
export interface Props extends VariableEditorProps<TextBoxVariableModel> {}
export class TextBoxVariableEditor extends PureComponent<Props> {
@ -15,20 +19,18 @@ export class TextBoxVariableEditor extends PureComponent<Props> {
render() {
const { query } = this.props.variable;
return (
<div className="gf-form-group">
<h5 className="section-heading">Text options</h5>
<div className="gf-form">
<span className="gf-form-label">Default value</span>
<input
type="text"
className="gf-form-input"
<VerticalGroup spacing="xs">
<VariableSectionHeader name="Text Options" />
<VariableTextField
value={query}
name="Default value"
placeholder="default value, if any"
onChange={this.onQueryChange}
onBlur={this.onQueryBlur}
placeholder="default value, if any"
labelWidth={20}
grow
/>
</div>
</div>
</VerticalGroup>
);
}
}

View File

@ -139,6 +139,7 @@ export interface VariableModel extends BaseVariableModel {
index: number;
state: LoadingState;
error: any | null;
description: string | null;
}
export const initialVariableModelState: VariableModel = {
@ -152,6 +153,7 @@ export const initialVariableModelState: VariableModel = {
skipUrlSync: false,
state: LoadingState.NotStarted,
error: null,
description: null,
};
export type VariableQueryEditorType<