VariableEditor: Use new form styles (#56326)

This commit is contained in:
kay delaney
2022-10-12 09:43:41 +01:00
committed by GitHub
parent 6edce00b1a
commit 45edce90d3
32 changed files with 521 additions and 560 deletions

View File

@@ -5301,16 +5301,11 @@ exports[`better eslint`] = {
[0, 0, 0, "Do not use any type assertions.", "1"]
],
"public/app/features/variables/editor/VariableEditorEditor.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/variables/editor/VariableSelectField.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/variables/editor/VariableTextAreaField.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"]
],
"public/app/features/variables/editor/getVariableQueryEditor.test.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],

View File

@@ -10,7 +10,9 @@ describe('Variables - Constant', () => {
// Create a new "Constant" variable
e2e.components.CallToActionCard.buttonV2('Add variable').click();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().type('Constant{enter}');
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().within(() => {
e2e().get('input').type('Constant{enter}');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type('VariableUnderTest').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type('Variable under test').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInputV2().type('pesto').blur();

View File

@@ -3,7 +3,9 @@ import { e2e } from '@grafana/e2e';
const PAGE_UNDER_TEST = 'kVi2Gex7z/test-variable-output';
function fillInCustomVariable(name: string, label: string, value: string) {
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().type('Custom{enter}');
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().within(() => {
e2e().get('input').type('Custom{enter}');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type(name).blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type(label).blur();
e2e.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput().type(value).blur();
@@ -42,7 +44,9 @@ describe('Variables - Custom', () => {
// Create a new "Custom" variable
e2e.components.CallToActionCard.buttonV2('Add variable').click();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().type('Custom{enter}');
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().within(() => {
e2e().get('input').type('Custom{enter}');
});
// Set its name, label, and content
fillInCustomVariable('VariableUnderTest', 'Variable under test', 'One : 1,Two : 2, Three : 3');

View File

@@ -9,13 +9,17 @@ describe('Variables - Datasource', () => {
// Create a new "Datasource" variable
e2e.components.CallToActionCard.buttonV2('Add variable').click();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().type('Data source{enter}');
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().within(() => {
e2e().get('input').type('Data source{enter}');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type('VariableUnderTest').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type('Variable under test').blur();
// If this is failing, but sure to check there are Prometheus datasources named "gdev-prometheus" and "gdev-slow-prometheus"
// Or, just update is to match some gdev datasources to test with :)
e2e.pages.Dashboard.Settings.Variables.Edit.DatasourceVariable.datasourceSelect().type('Prometheus{enter}');
e2e.pages.Dashboard.Settings.Variables.Edit.DatasourceVariable.datasourceSelect().within(() => {
e2e().get('input').type('Prometheus{enter}');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption()
.eq(0)
.should('have.text', 'gdev-prometheus');

View File

@@ -16,7 +16,9 @@ describe('Variables - Interval', () => {
// Create a new "Interval" variable
e2e.components.CallToActionCard.buttonV2('Add variable').click();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().type('Interval{enter}');
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().within(() => {
e2e().get('input').type('Interval{enter}');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type('VariableUnderTest').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type('Variable under test').blur();

View File

@@ -12,7 +12,7 @@ describe('Variables - Query - Add variable', () => {
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2()
.should('be.visible')
.within((input) => {
expect(input.attr('placeholder')).equals('name');
expect(input.attr('placeholder')).equals('Variable name');
expect(input.val()).equals('query0');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2()
@@ -23,21 +23,17 @@ describe('Variables - Query - Add variable', () => {
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2()
.should('be.visible')
.within((input) => {
expect(input.attr('placeholder')).equals('optional display name');
expect(input.attr('placeholder')).equals('Label name');
expect(input.val()).equals('');
});
e2e()
.get('#Description')
.get('[placeholder="Descriptive text"]')
.should('be.visible')
.within((input) => {
expect(input.attr('placeholder')).equals('descriptive text');
expect(input.attr('placeholder')).equals('Descriptive text');
expect(input.val()).equals('');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalHideSelectV2()
.should('be.visible')
.within((select) => {
e2e.components.Select.singleValue().should('have.text', '');
});
e2e().get('label').contains('Show on dashboard').should('be.visible');
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsDataSourceSelect()
.should('be.visible')
@@ -45,11 +41,9 @@ describe('Variables - Query - Add variable', () => {
e2e.components.Select.singleValue().should('have.text', 'gdev-testdata');
});
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRefreshSelectV2()
.should('be.visible')
.within((select) => {
e2e.components.Select.singleValue().should('have.text', 'On dashboard load');
});
e2e().get('label').contains('Refresh').scrollIntoView().should('be.visible');
e2e().get('label').contains('On dashboard load').scrollIntoView().should('be.visible');
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2()
.should('be.visible')
.within((input) => {
@@ -62,8 +56,18 @@ describe('Variables - Query - Add variable', () => {
.within((select) => {
e2e.components.Select.singleValue().should('have.text', 'Disabled');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch().should('not.be.checked');
e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch().should('not.be.checked');
e2e()
.contains('label', 'Multi-value')
.within(() => {
e2e().get('input[type="checkbox"]').should('not.be.checked');
});
e2e()
.contains('label', 'Include All option')
.within(() => {
e2e().get('input[type="checkbox"]').should('not.be.checked');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should('not.exist');
e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInputV2().should('not.exist');
@@ -80,9 +84,9 @@ describe('Variables - Query - Add variable', () => {
.clear()
.type('a label');
e2e().get('#Description').should('be.visible').clear().type('a description');
e2e().get('[placeholder="Descriptive text"]').should('be.visible').clear().type('a description');
e2e.components.DataSourcePicker.container().should('be.visible').type('gdev-testdata{enter}');
e2e.components.DataSourcePicker.inputV2().should('be.visible').type('gdev-testdata{enter}');
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput()
.should('be.visible')
@@ -96,7 +100,7 @@ describe('Variables - Query - Add variable', () => {
e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should('exist');
e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().should('be.visible').click();
e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().scrollIntoView().should('be.visible').click();
e2e().wait(1500);
@@ -130,9 +134,9 @@ describe('Variables - Query - Add variable', () => {
.clear()
.type('a label');
e2e().get('#Description').should('be.visible').clear().type('a description');
e2e().get('[placeholder="Descriptive text"]').should('be.visible').clear().type('a description');
e2e.components.DataSourcePicker.container().type('gdev-testdata{enter}');
e2e.components.DataSourcePicker.inputV2().type('gdev-testdata{enter}');
e2e.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput()
.should('be.visible')
@@ -144,13 +148,17 @@ describe('Variables - Query - Add variable', () => {
.type('/.*C.*/')
.blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch()
.click({ force: true })
.should('be.checked');
e2e()
.contains('label', 'Multi-value')
.within(() => {
e2e().get('input[type="checkbox"]').click({ force: true }).should('be.checked');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch()
.click({ force: true })
.should('be.checked');
e2e()
.contains('label', 'Include All option')
.within(() => {
e2e().get('input[type="checkbox"]').click({ force: true }).should('be.checked');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInputV2().within((input) => {
expect(input.attr('placeholder')).equals('blank = auto');
@@ -159,7 +167,7 @@ describe('Variables - Query - Add variable', () => {
e2e.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption().should('exist');
e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().should('be.visible').click();
e2e.pages.Dashboard.Settings.Variables.Edit.General.submitButton().scrollIntoView().should('be.visible').click();
e2e().wait(500);

View File

@@ -10,7 +10,9 @@ describe('Variables - Text box', () => {
// Create a new "text box" variable
e2e.components.CallToActionCard.buttonV2('Add variable').click();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().type('Text box{enter}');
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2().within(() => {
e2e().get('input').type('Text box{enter}');
});
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2().clear().type('VariableUnderTest').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2().type('Variable under test').blur();
e2e.pages.Dashboard.Settings.Variables.Edit.TextBoxVariable.textBoxOptionsQueryInputV2().type('cat-dog').blur();

View File

@@ -3,10 +3,10 @@ import { connect, ConnectedProps } from 'react-redux';
import { DataSourceInstanceSettings, getDataSourceRef } from '@grafana/data';
import { DataSourcePicker } from '@grafana/runtime';
import { Alert, InlineField, InlineFieldRow, VerticalGroup } from '@grafana/ui';
import { Alert, Field } from '@grafana/ui';
import { StoreState } from 'app/types';
import { VariableSectionHeader } from '../editor/VariableSectionHeader';
import { VariableLegend } from '../editor/VariableLegend';
import { initialVariableEditorState } from '../editor/reducer';
import { getAdhocVariableEditorState } from '../editor/selectors';
import { VariableEditorProps } from '../editor/types';
@@ -61,18 +61,14 @@ export class AdHocVariableEditorUnConnected extends PureComponent<Props> {
const infoText = extended?.infoText ?? null;
return (
<VerticalGroup spacing="xs">
<VariableSectionHeader name="Options" />
<VerticalGroup spacing="sm">
<InlineFieldRow>
<InlineField label="Data source" labelWidth={20} htmlFor="data-source-picker">
<DataSourcePicker current={variable.datasource} onChange={this.onDatasourceChanged} noDefault />
</InlineField>
</InlineFieldRow>
<>
<VariableLegend>Ad-hoc options</VariableLegend>
<Field label="Data source" htmlFor="data-source-picker">
<DataSourcePicker current={variable.datasource} onChange={this.onDatasourceChanged} width={30} noDefault />
</Field>
{infoText ? <Alert title={infoText} severity="info" /> : null}
</VerticalGroup>
</VerticalGroup>
</>
);
}
}

View File

@@ -1,9 +1,8 @@
import React, { FormEvent, PureComponent } from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { VerticalGroup } from '@grafana/ui';
import { VariableSectionHeader } from '../editor/VariableSectionHeader';
import { VariableLegend } from '../editor/VariableLegend';
import { VariableTextField } from '../editor/VariableTextField';
import { VariableEditorProps } from '../editor/types';
import { ConstantVariableModel } from '../types';
@@ -28,19 +27,18 @@ export class ConstantVariableEditor extends PureComponent<Props> {
render() {
return (
<VerticalGroup spacing="xs">
<VariableSectionHeader name="Constant options" />
<>
<VariableLegend>Constant options</VariableLegend>
<VariableTextField
value={this.props.variable.query}
name="Value"
placeholder="your metric prefix"
onChange={this.onChange}
onBlur={this.onBlur}
labelWidth={20}
testId={selectors.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInputV2}
grow
width={30}
/>
</VerticalGroup>
</>
);
}
}

View File

@@ -2,12 +2,11 @@ import React, { FormEvent, PureComponent } from 'react';
import { MapDispatchToProps, MapStateToProps } from 'react-redux';
import { selectors } from '@grafana/e2e-selectors';
import { VerticalGroup } from '@grafana/ui';
import { connectWithStore } from 'app/core/utils/connectWithReduxStore';
import { StoreState } from 'app/types';
import { SelectionOptionsEditor } from '../editor/SelectionOptionsEditor';
import { VariableSectionHeader } from '../editor/VariableSectionHeader';
import { VariableLegend } from '../editor/VariableLegend';
import { VariableTextAreaField } from '../editor/VariableTextAreaField';
import { OnPropChangeArguments, VariableEditorProps } from '../editor/types';
import { changeVariableMultiValue } from '../state/actions';
@@ -45,10 +44,9 @@ class CustomVariableEditorUnconnected extends PureComponent<Props> {
render() {
return (
<VerticalGroup spacing="xs">
<VariableSectionHeader name="Custom options" />
<VerticalGroup spacing="md">
<VerticalGroup spacing="none">
<>
<VariableLegend>Custom options</VariableLegend>
<VariableTextAreaField
name="Values separated by comma"
value={this.props.variable.query}
@@ -56,18 +54,16 @@ class CustomVariableEditorUnconnected extends PureComponent<Props> {
onChange={this.onChange}
onBlur={this.onBlur}
required
width={50}
labelWidth={27}
width={52}
testId={selectors.pages.Dashboard.Settings.Variables.Edit.CustomVariable.customValueInput}
/>
</VerticalGroup>
<VariableLegend>Selection options</VariableLegend>
<SelectionOptionsEditor
variable={this.props.variable}
onPropChange={this.onSelectionOptionsChange}
onMultiChanged={this.props.changeVariableMultiValue}
/>{' '}
</VerticalGroup>
</VerticalGroup>
/>
</>
);
}
}

View File

@@ -41,13 +41,13 @@ describe('DataSourceVariableEditor', () => {
it('has a regex filter field', () => {
render(<DataSourceVariableEditor {...props} />);
const field = screen.getByLabelText('Instance name filter');
const field = screen.getByLabelText(/Instance name filter/);
expect(field).toBeInTheDocument();
});
it('calls the handler when the regex filter is changed', () => {
render(<DataSourceVariableEditor {...props} />);
const field = screen.getByLabelText('Instance name filter');
const field = screen.getByLabelText(/Instance name filter/);
fireEvent.change(field, { target: { value: '/prod/' } });
expect(props.onPropChange).toBeCalledWith({ propName: 'regex', propValue: '/prod/' });
});

View File

@@ -3,11 +3,10 @@ import { connect, ConnectedProps } from 'react-redux';
import { SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { InlineFieldRow, VerticalGroup } from '@grafana/ui';
import { StoreState } from '../../../types';
import { SelectionOptionsEditor } from '../editor/SelectionOptionsEditor';
import { VariableSectionHeader } from '../editor/VariableSectionHeader';
import { VariableLegend } from '../editor/VariableLegend';
import { VariableSelectField } from '../editor/VariableSelectField';
import { VariableTextField } from '../editor/VariableTextField';
import { initialVariableEditorState } from '../editor/reducer';
@@ -103,48 +102,40 @@ export class DataSourceVariableEditorUnConnected extends PureComponent<Props> {
const typeValue = typeOptions.find((o) => o.value === variable.query) ?? typeOptions[0];
return (
<VerticalGroup spacing="xs">
<VariableSectionHeader name="Data source options" />
<VerticalGroup spacing="md">
<VerticalGroup spacing="xs">
<InlineFieldRow>
<>
<VariableLegend>Data source options</VariableLegend>
<VariableSelectField
name="Type"
value={typeValue}
options={typeOptions}
onChange={this.onDataSourceTypeChanged}
labelWidth={10}
testId={selectors.pages.Dashboard.Settings.Variables.Edit.DatasourceVariable.datasourceSelect}
/>
</InlineFieldRow>
<InlineFieldRow>
<VariableTextField
value={this.props.variable.regex}
name="Instance name filter"
placeholder="/.*-(.*)-.*/"
onChange={this.onRegExChange}
onBlur={this.onRegExBlur}
labelWidth={20}
tooltip={
description={
<div>
Regex filter for which data source instances to choose from in the variable value list. Leave empty
for all.
Regex filter for which data source instances to choose from in the variable value list. Leave empty for
all.
<br />
<br />
Example: <code>/^prod/</code>
</div>
}
/>
</InlineFieldRow>
</VerticalGroup>
<VariableLegend>Selection options</VariableLegend>
<SelectionOptionsEditor
variable={variable}
onPropChange={this.onSelectionOptionsChange}
onMultiChanged={changeVariableMultiValue}
/>
</VerticalGroup>
</VerticalGroup>
</>
);
}
}

View File

@@ -15,7 +15,7 @@ import { dataSourceVariableReducer, initialDataSourceVariableModelState } from '
export const createDataSourceVariableAdapter = (): VariableAdapter<DataSourceVariableModel> => {
return {
id: 'datasource',
description: 'Enabled you to dynamically switch the data source for multiple panels.',
description: 'Enables you to dynamically switch the data source for multiple panels.',
name: 'Data source',
initialState: initialDataSourceVariableModelState,
reducer: dataSourceVariableReducer,

View File

@@ -1,18 +1,17 @@
import { css } from '@emotion/css';
import { useId } from '@react-aria/utils';
import React, { FC, useCallback, useState } from 'react';
import { GrafanaTheme } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { useStyles } from '@grafana/ui';
import { TextArea, useStyles2 } from '@grafana/ui';
import { VariableQueryEditorProps } from '../types';
import { VariableTextAreaField } from './VariableTextAreaField';
import { getStyles } from './VariableTextAreaField';
export const LEGACY_VARIABLE_QUERY_EDITOR_NAME = 'Grafana-LegacyVariableQueryEditor';
export const LegacyVariableQueryEditor: FC<VariableQueryEditorProps> = ({ onChange, query }) => {
const styles = useStyles(getStyles);
const styles = useStyles2(getStyles);
const [value, setValue] = useState(query);
const onValueChange = (event: React.FormEvent<HTMLTextAreaElement>) => {
setValue(event.currentTarget.value);
@@ -25,29 +24,22 @@ export const LegacyVariableQueryEditor: FC<VariableQueryEditorProps> = ({ onChan
[onChange]
);
const id = useId();
return (
<div className={styles.container}>
<VariableTextAreaField
name="Query"
<TextArea
id={id}
rows={2}
value={value}
placeholder="metric name or tags query"
width={100}
onChange={onValueChange}
onBlur={onBlur}
placeholder="Metric name or tags query"
required
labelWidth={20}
ariaLabel={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput}
aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput}
cols={52}
className={styles.textarea}
/>
</div>
);
};
function getStyles(theme: GrafanaTheme) {
return {
container: css`
margin-bottom: ${theme.spacing.xs};
`,
};
}
LegacyVariableQueryEditor.displayName = LEGACY_VARIABLE_QUERY_EDITOR_NAME;

View File

@@ -1,14 +1,13 @@
import React, { ChangeEvent, FormEvent, FunctionComponent, useCallback } from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { InlineFieldRow, VerticalGroup } from '@grafana/ui';
import { VerticalGroup } from '@grafana/ui';
import { KeyedVariableIdentifier } from '../state/types';
import { VariableWithMultiSupport } from '../types';
import { toKeyedVariableIdentifier } from '../utils';
import { VariableSectionHeader } from './VariableSectionHeader';
import { VariableSwitchField } from './VariableSwitchField';
import { VariableCheckboxField } from './VariableCheckboxField';
import { VariableTextField } from './VariableTextField';
import { VariableEditorProps } from './types';
@@ -44,37 +43,27 @@ export const SelectionOptionsEditor: FunctionComponent<SelectionOptionsEditorPro
);
return (
<VerticalGroup spacing="none">
<VariableSectionHeader name="Selection options" />
<InlineFieldRow>
<VariableSwitchField
<VerticalGroup spacing="md" height="inherit">
<VariableCheckboxField
value={variable.multi}
name="Multi-value"
tooltip="Enables multiple values to be selected at the same time"
description="Enables multiple values to be selected at the same time"
onChange={onMultiChanged}
ariaLabel={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch}
/>
</InlineFieldRow>
<InlineFieldRow>
<VariableSwitchField
<VariableCheckboxField
value={variable.includeAll}
name="Include All option"
tooltip="Enables an option to include all variables"
description="Enables an option to include all variables"
onChange={onIncludeAllChanged}
ariaLabel={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch}
/>
</InlineFieldRow>
{variable.includeAll && (
<InlineFieldRow>
<VariableTextField
value={variable.allValue ?? ''}
onChange={onAllValueChanged}
name="Custom all value"
placeholder="blank = auto"
testId={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsCustomAllInputV2}
labelWidth={20}
/>
</InlineFieldRow>
)}
</VerticalGroup>
);

View File

@@ -0,0 +1,33 @@
import { useId } from '@react-aria/utils';
import React, { ChangeEvent, PropsWithChildren, ReactElement } from 'react';
import { Checkbox } from '@grafana/ui';
interface VariableCheckboxFieldProps {
value: boolean;
name: string;
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
description?: string;
ariaLabel?: string;
}
export function VariableCheckboxField({
value,
name,
description,
onChange,
ariaLabel,
}: PropsWithChildren<VariableCheckboxFieldProps>): ReactElement {
const uniqueId = useId();
return (
<Checkbox
id={uniqueId}
label={name}
description={description}
value={value}
onChange={onChange}
aria-label={ariaLabel}
/>
);
}

View File

@@ -1,14 +1,12 @@
import { isEqual } from 'lodash';
import React, { FormEvent, PureComponent } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { bindActionCreators } from 'redux';
import { AppEvents, LoadingState, SelectableValue, VariableType } from '@grafana/data';
import { LoadingState, SelectableValue, VariableType } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { locationService } from '@grafana/runtime';
import { Button, HorizontalGroup, Icon, InlineFieldRow, VerticalGroup } from '@grafana/ui';
import { Button, HorizontalGroup, Icon } from '@grafana/ui';
import { appEvents } from '../../../core/core';
import { StoreState, ThunkDispatch } from '../../../types';
import { variableAdapters } from '../adapters';
import { hasOptions } from '../guard';
@@ -22,7 +20,8 @@ import { toKeyedVariableIdentifier, toVariablePayload } from '../utils';
import { ConfirmDeleteModal } from './ConfirmDeleteModal';
import { VariableHideSelect } from './VariableHideSelect';
import { VariableSectionHeader } from './VariableSectionHeader';
import { VariableLegend } from './VariableLegend';
import { VariableTextAreaField } from './VariableTextAreaField';
import { VariableTextField } from './VariableTextField';
import { VariableTypeSelect } from './VariableTypeSelect';
import { VariableValuesPreview } from './VariableValuesPreview';
@@ -75,14 +74,6 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props, State>
this.props.variableEditorMount(this.props.identifier);
}
componentDidUpdate(prevProps: Readonly<Props>, prevState: Readonly<{}>, snapshot?: any): void {
if (!isEqual(prevProps.editor.errors, this.props.editor.errors)) {
Object.values(this.props.editor.errors).forEach((error) => {
appEvents.emit(AppEvents.alertWarning, ['Validation', error]);
});
}
}
componentWillUnmount(): void {
this.props.variableEditorUnMount(this.props.identifier);
}
@@ -104,12 +95,12 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props, State>
this.props.changeVariableProp(this.props.identifier, 'label', event.currentTarget.value);
};
onDescriptionChange = (event: FormEvent<HTMLInputElement>) => {
onDescriptionChange = (event: FormEvent<HTMLTextAreaElement>) => {
this.props.changeVariableProp(this.props.identifier, 'description', event.currentTarget.value);
};
onHideChange = (option: SelectableValue<VariableHide>) => {
this.props.changeVariableProp(this.props.identifier, 'hide', option.value);
onHideChange = (option: VariableHide) => {
this.props.changeVariableProp(this.props.identifier, 'hide', option);
};
onPropChanged = ({ propName, propValue, updateOptions = false }: OnPropChangeArguments) => {
@@ -156,79 +147,68 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props, State>
const loading = variable.state === LoadingState.Loading;
return (
<div>
<>
<form aria-label="Variable editor Form" onSubmit={this.onHandleSubmit}>
<VerticalGroup spacing="lg">
<VerticalGroup spacing="none">
<VariableSectionHeader name="General" />
<InlineFieldRow>
<VariableTypeSelect onChange={this.onTypeChange} type={this.props.variable.type} />
<VariableLegend>General</VariableLegend>
<VariableTextField
value={this.props.editor.name}
onChange={this.onNameChange}
name="Name"
placeholder="name"
placeholder="Variable name"
description="The name of the template variable. (Max. 50 characters)"
invalid={!!this.props.editor.errors.name}
error={this.props.editor.errors.name}
testId={selectors.pages.Dashboard.Settings.Variables.Edit.General.generalNameInputV2}
maxLength={VariableNameConstraints.MaxSize}
required
tooltip="Variable name cannot be longer than 50 characters"
/>
<VariableTypeSelect onChange={this.onTypeChange} type={this.props.variable.type} />
</InlineFieldRow>
{this.props.editor.errors.name && (
<div className="gf-form">
<span className="gf-form-label gf-form-label--error">{this.props.editor.errors.name}</span>
</div>
)}
<InlineFieldRow>
<VariableTextField
value={this.props.variable.label ?? ''}
onChange={this.onLabelChange}
name="Label"
placeholder="optional display name"
description="Optional display name"
value={this.props.variable.label ?? ''}
placeholder="Label name"
onChange={this.onLabelChange}
testId={selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInputV2}
/>
<VariableTextAreaField
name="Description"
value={variable.description ?? ''}
placeholder="Descriptive text"
onChange={this.onDescriptionChange}
width={52}
/>
<VariableHideSelect
onChange={this.onHideChange}
hide={this.props.variable.hide}
type={this.props.variable.type}
/>
</InlineFieldRow>
<VariableTextField
name="Description"
value={variable.description ?? ''}
placeholder="descriptive text"
onChange={this.onDescriptionChange}
grow
/>
</VerticalGroup>
{EditorToRender && <EditorToRender variable={this.props.variable} onPropChange={this.onPropChanged} />}
{hasOptions(this.props.variable) ? <VariableValuesPreview variable={this.props.variable} /> : null}
<HorizontalGroup spacing="md">
<Button variant="destructive" onClick={this.onModalOpen}>
<div style={{ marginTop: '16px' }}>
<HorizontalGroup spacing="md" height="inherit">
<Button variant="destructive" fill="outline" onClick={this.onModalOpen}>
Delete
</Button>
<Button
type="submit"
aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.General.submitButton}
disabled={loading}
variant={'secondary'}
variant="secondary"
>
Run query
{loading ? (
<Icon className="spin-clockwise" name="sync" size="sm" style={{ marginLeft: '2px' }} />
) : null}
{loading && <Icon className="spin-clockwise" name="sync" size="sm" style={{ marginLeft: '2px' }} />}
</Button>
<Button variant="primary" onClick={this.onApply}>
Apply
</Button>
</HorizontalGroup>
</VerticalGroup>
</div>
</form>
<ConfirmDeleteModal
isOpen={this.state.showDeleteModal}
@@ -236,7 +216,7 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props, State>
onConfirm={this.onDelete}
onDismiss={this.onModalClose}
/>
</div>
</>
);
}
}

View File

@@ -1,37 +1,30 @@
import React, { PropsWithChildren, useMemo } from 'react';
import { SelectableValue, VariableType } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { VariableSelectField } from '../editor/VariableSelectField';
import { VariableHide } from '../types';
import { VariableType, VariableHide } from '@grafana/data';
import { Field, RadioButtonGroup } from '@grafana/ui';
interface Props {
onChange: (option: SelectableValue<VariableHide>) => void;
onChange: (option: VariableHide) => void;
hide: VariableHide;
type: VariableType;
}
const HIDE_OPTIONS = [
{ label: '', value: VariableHide.dontHide },
{ label: 'Label', value: VariableHide.hideLabel },
{ label: 'Variable', value: VariableHide.hideVariable },
{ label: 'Label and value', value: VariableHide.dontHide },
{ label: 'Value', value: VariableHide.hideLabel },
{ label: 'Nothing', value: VariableHide.hideVariable },
];
export function VariableHideSelect({ onChange, hide, type }: PropsWithChildren<Props>) {
const value = useMemo(() => HIDE_OPTIONS.find((o) => o.value === hide) ?? HIDE_OPTIONS[0], [hide]);
const value = useMemo(() => HIDE_OPTIONS.find((o) => o.value === hide)?.value ?? HIDE_OPTIONS[0].value, [hide]);
if (type === 'constant') {
return null;
}
return (
<VariableSelectField
name="Hide"
value={value}
options={HIDE_OPTIONS}
onChange={onChange}
testId={selectors.pages.Dashboard.Settings.Variables.Edit.General.generalHideSelectV2}
/>
<Field label="Show on dashboard">
<RadioButtonGroup options={HIDE_OPTIONS} onChange={onChange} value={value} />
</Field>
);
}

View File

@@ -0,0 +1,19 @@
import { css, cx } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Legend, useStyles2 } from '@grafana/ui';
export function VariableLegend({ className, ...rest }: Parameters<typeof Legend>['0']) {
const styles = useStyles2(getStyles);
return <Legend {...rest} className={cx(styles.legend, className)} />;
}
function getStyles(theme: GrafanaTheme2) {
return {
legend: css({
marginTop: theme.spacing(3),
marginBottom: theme.spacing(1),
}),
};
}

View File

@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import React, { PropsWithChildren, ReactElement } from 'react';
import { GrafanaTheme, SelectableValue } from '@grafana/data';
import { InlineFormLabel, Select, useStyles } from '@grafana/ui';
import { Field, Select, useStyles } from '@grafana/ui';
import { useUniqueId } from 'app/plugins/datasource/influxdb/components/useUniqueId';
interface VariableSelectFieldProps<T> {
@@ -10,42 +10,37 @@ interface VariableSelectFieldProps<T> {
value: SelectableValue<T>;
options: Array<SelectableValue<T>>;
onChange: (option: SelectableValue<T>) => void;
tooltip?: string;
testId?: string;
width?: number;
labelWidth?: number;
description?: React.ReactNode;
}
export function VariableSelectField({
name,
description,
value,
options,
tooltip,
onChange,
testId,
width,
labelWidth,
}: PropsWithChildren<VariableSelectFieldProps<any>>): ReactElement {
const styles = useStyles(getStyles);
const uniqueId = useUniqueId();
const inputId = `variable-select-input-${name}-${uniqueId}`;
return (
<>
<InlineFormLabel width={labelWidth ?? 6} tooltip={tooltip} htmlFor={inputId}>
{name}
</InlineFormLabel>
<Field label={name} description={description} htmlFor={inputId}>
<div data-testid={testId}>
<Select
inputId={inputId}
onChange={onChange}
value={value}
width={width ?? 25}
width={width ?? 30}
options={options}
className={styles.selectContainer}
/>
</div>
</>
</Field>
);
}

View File

@@ -1,32 +0,0 @@
import React, { ChangeEvent, PropsWithChildren, ReactElement } from 'react';
import { InlineField, InlineSwitch } from '@grafana/ui';
import { useUniqueId } from 'app/plugins/datasource/influxdb/components/useUniqueId';
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 uniqueId = useUniqueId();
return (
<InlineField label={name} labelWidth={20} tooltip={tooltip}>
<InlineSwitch
id={`var-switch-${uniqueId}`}
label={name}
value={value}
onChange={onChange}
aria-label={ariaLabel}
/>
</InlineField>
);
}

View File

@@ -1,49 +1,43 @@
import { css } from '@emotion/css';
import React, { FormEvent, PropsWithChildren, ReactElement, useCallback } from 'react';
import { useId } from '@react-aria/utils';
import React, { FormEvent, PropsWithChildren, ReactElement } from 'react';
import { GrafanaTheme } from '@grafana/data';
import { InlineField, TextArea, useStyles } from '@grafana/ui';
import { GrafanaTheme2 } from '@grafana/data';
import { Field, TextArea, useStyles2 } from '@grafana/ui';
interface VariableTextAreaFieldProps<T> {
interface VariableTextAreaFieldProps {
name: string;
value: string;
placeholder: string;
onChange: (event: FormEvent<HTMLTextAreaElement>) => void;
width: number;
tooltip?: string;
ariaLabel?: string;
required?: boolean;
labelWidth?: number;
testId?: string;
onBlur?: (event: FormEvent<HTMLTextAreaElement>) => void;
description?: React.ReactNode;
}
export function VariableTextAreaField({
name,
value,
name,
description,
placeholder,
tooltip,
onChange,
onBlur,
ariaLabel,
required,
width,
labelWidth,
testId,
}: 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;
}, []);
}: PropsWithChildren<VariableTextAreaFieldProps>): ReactElement {
const styles = useStyles2(getStyles);
const id = useId();
return (
<InlineField label={name} labelWidth={labelWidth ?? 12} tooltip={tooltip}>
<Field label={name} description={description} htmlFor={id}>
<TextArea
rows={getLineCount(value)}
id={id}
rows={2}
value={value}
onChange={onChange}
onBlur={onBlur}
@@ -54,18 +48,19 @@ export function VariableTextAreaField({
className={styles.textarea}
data-testid={testId}
/>
</InlineField>
</Field>
);
}
function getStyles(theme: GrafanaTheme) {
export function getStyles(theme: GrafanaTheme2) {
return {
textarea: css`
white-space: pre-wrap;
min-height: 32px;
min-height: ${theme.spacing(4)};
height: auto;
overflow: auto;
padding: 6px 8px;
padding: ${theme.spacing(0.75, 1)};
width: inherit;
`,
};
}

View File

@@ -1,53 +1,55 @@
import React, { FormEvent, PropsWithChildren, ReactElement } from 'react';
import { useId } from '@react-aria/utils';
import React, { FormEvent, PropsWithChildren } from 'react';
import { InlineField, Input, PopoverContent } from '@grafana/ui';
import { Field, Input } from '@grafana/ui';
interface VariableTextFieldProps {
value: string;
name: string;
placeholder: string;
placeholder?: string;
onChange: (event: FormEvent<HTMLInputElement>) => void;
testId?: string;
tooltip?: PopoverContent;
required?: boolean;
width?: number;
labelWidth?: number;
grow?: boolean;
onBlur?: (event: FormEvent<HTMLInputElement>) => void;
interactive?: boolean;
maxLength?: number;
description?: React.ReactNode;
invalid?: boolean;
error?: React.ReactNode;
}
export function VariableTextField({
value,
name,
placeholder,
placeholder = '',
onChange,
testId,
width,
labelWidth,
required,
onBlur,
tooltip,
grow,
interactive,
description,
invalid,
error,
maxLength,
}: PropsWithChildren<VariableTextFieldProps>): ReactElement {
}: PropsWithChildren<VariableTextFieldProps>) {
const id = useId(name);
return (
<InlineField interactive={interactive} label={name} labelWidth={labelWidth ?? 12} tooltip={tooltip} grow={grow}>
<Field label={name} description={description} invalid={invalid} error={error} htmlFor={id}>
<Input
type="text"
id={name}
name={name}
id={id}
placeholder={placeholder}
value={value}
onChange={onChange}
onBlur={onBlur}
width={grow ? undefined : width ?? 25}
width={grow ? undefined : width ?? 30}
data-testid={testId}
maxLength={maxLength}
required={required}
/>
</InlineField>
</Field>
);
}

View File

@@ -3,7 +3,6 @@ import React, { PropsWithChildren, useMemo } from 'react';
import { SelectableValue, VariableType } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { variableAdapters } from '../adapters';
import { VariableSelectField } from '../editor/VariableSelectField';
import { getVariableTypes } from '../utils';
@@ -18,11 +17,10 @@ export function VariableTypeSelect({ onChange, type }: PropsWithChildren<Props>)
return (
<VariableSelectField
name="Type"
name="Select variable type"
value={value}
options={options}
onChange={onChange}
tooltip={variableAdapters.get(type).description}
testId={selectors.pages.Dashboard.Settings.Variables.Edit.General.generalTypeSelectV2}
/>
);

View File

@@ -1,9 +1,9 @@
import { css } from '@emotion/css';
import React, { MouseEvent, useCallback, useEffect, useState } from 'react';
import { GrafanaTheme } from '@grafana/data';
import { GrafanaTheme2 } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { Button, InlineFieldRow, InlineLabel, useStyles, VerticalGroup } from '@grafana/ui';
import { Button, InlineFieldRow, InlineLabel, useStyles2 } from '@grafana/ui';
import { VariableOption, VariableWithOptions } from '../types';
@@ -23,7 +23,7 @@ export const VariableValuesPreview: React.FunctionComponent<VariableValuesPrevie
},
[previewLimit, setPreviewLimit]
);
const styles = useStyles(getStyles);
const styles = useStyles2(getStyles);
useEffect(() => setPreviewOptions(options.slice(0, previewLimit)), [previewLimit, options]);
if (!previewOptions.length) {
@@ -31,7 +31,7 @@ export const VariableValuesPreview: React.FunctionComponent<VariableValuesPrevie
}
return (
<VerticalGroup spacing="none">
<div style={{ display: 'flex', flexDirection: 'column', marginTop: '16px' }}>
<h5>Preview of values</h5>
<InlineFieldRow>
{previewOptions.map((o, index) => (
@@ -54,22 +54,27 @@ export const VariableValuesPreview: React.FunctionComponent<VariableValuesPrevie
</Button>
</InlineFieldRow>
)}
</VerticalGroup>
</div>
);
};
VariableValuesPreview.displayName = 'VariableValuesPreview';
function getStyles(theme: GrafanaTheme) {
function getStyles(theme: GrafanaTheme2) {
return {
optionContainer: css`
margin-left: ${theme.spacing.xs};
margin-bottom: ${theme.spacing.xs};
`,
label: css`
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 50vw;
`,
wrapper: css({
display: 'flex',
flexDirection: 'column',
marginTop: theme.spacing(2),
}),
optionContainer: css({
marginLeft: theme.spacing(0.5),
marginBottom: theme.spacing(0.5),
}),
label: css({
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis',
maxWidth: '50vw',
}),
};
}

View File

@@ -1,113 +1,119 @@
import React, { ChangeEvent, FormEvent, PureComponent } from 'react';
import { css } from '@emotion/css';
import React, { ChangeEvent, FormEvent } from 'react';
import { SelectableValue } from '@grafana/data';
import { GrafanaTheme2, IntervalVariableModel, SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { InlineFieldRow, VerticalGroup } from '@grafana/ui';
import { useStyles2 } from '@grafana/ui';
import { VariableSectionHeader } from '../editor/VariableSectionHeader';
import { VariableCheckboxField } from '../editor/VariableCheckboxField';
import { VariableLegend } from '../editor/VariableLegend';
import { VariableSelectField } from '../editor/VariableSelectField';
import { VariableSwitchField } from '../editor/VariableSwitchField';
import { VariableTextField } from '../editor/VariableTextField';
import { VariableEditorProps } from '../editor/types';
import { IntervalVariableModel } from '../types';
const STEP_OPTIONS = [1, 2, 3, 4, 5, 10, 20, 30, 40, 50, 100, 200, 300, 400, 500].map((count) => ({
label: `${count}`,
value: count,
}));
export interface Props extends VariableEditorProps<IntervalVariableModel> {}
export class IntervalVariableEditor extends PureComponent<Props> {
onAutoChange = (event: ChangeEvent<HTMLInputElement>) => {
this.props.onPropChange({
export const IntervalVariableEditor = React.memo(({ onPropChange, variable }: Props) => {
const onAutoChange = (event: ChangeEvent<HTMLInputElement>) => {
onPropChange({
propName: 'auto',
propValue: event.target.checked,
updateOptions: true,
});
};
onQueryChanged = (event: FormEvent<HTMLInputElement>) => {
this.props.onPropChange({
const onQueryChanged = (event: FormEvent<HTMLInputElement>) => {
onPropChange({
propName: 'query',
propValue: event.currentTarget.value,
});
};
onQueryBlur = (event: FormEvent<HTMLInputElement>) => {
this.props.onPropChange({
const onQueryBlur = (event: FormEvent<HTMLInputElement>) => {
onPropChange({
propName: 'query',
propValue: event.currentTarget.value,
updateOptions: true,
});
};
onAutoCountChanged = (option: SelectableValue<number>) => {
this.props.onPropChange({
const onAutoCountChanged = (option: SelectableValue<number>) => {
onPropChange({
propName: 'auto_count',
propValue: option.value,
updateOptions: true,
});
};
onAutoMinChanged = (event: FormEvent<HTMLInputElement>) => {
this.props.onPropChange({
const onAutoMinChanged = (event: FormEvent<HTMLInputElement>) => {
onPropChange({
propName: 'auto_min',
propValue: event.currentTarget.value,
updateOptions: true,
});
};
render() {
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];
const stepValue = STEP_OPTIONS.find((o) => o.value === variable.auto_count) ?? STEP_OPTIONS[0];
const styles = useStyles2(getStyles);
return (
<VerticalGroup spacing="xs">
<VariableSectionHeader name="Interval options" />
<VerticalGroup spacing="none">
<>
<VariableLegend>Interval options</VariableLegend>
<VariableTextField
value={this.props.variable.query}
value={variable.query}
name="Values"
placeholder="1m,10m,1h,6h,1d,7d"
onChange={this.onQueryChanged}
onBlur={this.onQueryBlur}
labelWidth={20}
onChange={onQueryChanged}
onBlur={onQueryBlur}
testId={selectors.pages.Dashboard.Settings.Variables.Edit.IntervalVariable.intervalsValueInput}
grow
width={32}
required
/>
<InlineFieldRow>
<VariableSwitchField
value={this.props.variable.auto}
<VariableCheckboxField
value={variable.auto}
name="Auto option"
tooltip="Dynamically calculates interval by dividing time range by the count specified."
onChange={this.onAutoChange}
description="Dynamically calculates interval by dividing time range by the count specified"
onChange={onAutoChange}
/>
{this.props.variable.auto ? (
<>
{variable.auto && (
<div className={styles.autoFields}>
<VariableSelectField
name="Step count"
description="How many times the current time range should be divided to calculate the value"
value={stepValue}
options={stepOptions}
onChange={this.onAutoCountChanged}
tooltip="How many times the current time range should be divided to calculate the value."
labelWidth={7}
options={STEP_OPTIONS}
onChange={onAutoCountChanged}
width={9}
/>
<VariableTextField
value={this.props.variable.auto_min}
value={variable.auto_min}
name="Min interval"
description="The calculated value will not go below this threshold"
placeholder="10s"
onChange={this.onAutoMinChanged}
tooltip="The calculated value will not go below this threshold."
labelWidth={13}
onChange={onAutoMinChanged}
width={11}
/>
</div>
)}
</>
) : null}
</InlineFieldRow>
</VerticalGroup>
</VerticalGroup>
);
}
});
IntervalVariableEditor.displayName = 'IntervalVariableEditor';
function getStyles(theme: GrafanaTheme2) {
return {
autoFields: css({
marginTop: theme.spacing(2),
display: 'flex',
flexDirection: 'column',
}),
};
}

View File

@@ -110,7 +110,7 @@ describe('QueryVariableEditor', () => {
const getQueryField = () =>
screen.getByRole('textbox', { name: /variable editor form default variable query editor textarea/i });
const getRegExField = () => screen.getByLabelText('Regex');
const getRegExField = () => screen.getByLabelText(/Regex/);
const fieldAccessors: Record<string, () => HTMLElement> = {
query: getQueryField,

View File

@@ -1,17 +1,16 @@
import { css } from '@emotion/css';
import React, { FormEvent, PureComponent } from 'react';
import { connect, ConnectedProps } from 'react-redux';
import { DataSourceInstanceSettings, getDataSourceRef, LoadingState, SelectableValue } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { DataSourcePicker, getTemplateSrv } from '@grafana/runtime';
import { InlineField, InlineFieldRow, VerticalGroup } from '@grafana/ui';
import { Field } from '@grafana/ui';
import { StoreState } from '../../../types';
import { getTimeSrv } from '../../dashboard/services/TimeSrv';
import { SelectionOptionsEditor } from '../editor/SelectionOptionsEditor';
import { VariableSectionHeader } from '../editor/VariableSectionHeader';
import { VariableTextField } from '../editor/VariableTextField';
import { VariableLegend } from '../editor/VariableLegend';
import { VariableTextAreaField } from '../editor/VariableTextAreaField';
import { initialVariableEditorState } from '../editor/reducer';
import { getQueryVariableEditorState } from '../editor/selectors';
import { OnPropChangeArguments, VariableEditorProps } from '../editor/types';
@@ -105,19 +104,19 @@ export class QueryVariableEditorUnConnected extends PureComponent<Props, State>
}
};
onRegExChange = (event: FormEvent<HTMLInputElement>) => {
onRegExChange = (event: FormEvent<HTMLTextAreaElement>) => {
this.setState({ regex: event.currentTarget.value });
};
onRegExBlur = async (event: FormEvent<HTMLInputElement>) => {
onRegExBlur = async (event: FormEvent<HTMLTextAreaElement>) => {
const regex = event.currentTarget.value;
if (this.props.variable.regex !== regex) {
this.props.onPropChange({ propName: 'regex', propValue: regex, updateOptions: true });
}
};
onRefreshChange = (option: SelectableValue<VariableRefresh>) => {
this.props.onPropChange({ propName: 'refresh', propValue: option.value });
onRefreshChange = (option: VariableRefresh) => {
this.props.onPropChange({ propName: 'refresh', propValue: option });
};
onSortChange = async (option: SelectableValue<VariableSort>) => {
@@ -141,12 +140,14 @@ export class QueryVariableEditorUnConnected extends PureComponent<Props, State>
if (isLegacyQueryEditor(VariableQueryEditor, datasource)) {
return (
<Field label="Query">
<VariableQueryEditor
datasource={datasource}
query={query}
templateSrv={getTemplateSrv()}
onChange={this.onLegacyQueryChange}
/>
</Field>
);
}
@@ -154,6 +155,7 @@ export class QueryVariableEditorUnConnected extends PureComponent<Props, State>
if (isQueryEditor(VariableQueryEditor, datasource)) {
return (
<Field label="Query">
<VariableQueryEditor
datasource={datasource}
query={query}
@@ -164,6 +166,7 @@ export class QueryVariableEditorUnConnected extends PureComponent<Props, State>
onBlur={() => {}}
history={[]}
/>
</Field>
);
}
@@ -172,40 +175,27 @@ export class QueryVariableEditorUnConnected extends PureComponent<Props, State>
render() {
return (
<VerticalGroup spacing="xs">
<VariableSectionHeader name="Query Options" />
<VerticalGroup spacing="lg">
<VerticalGroup spacing="none">
<InlineFieldRow>
<InlineField label="Data source" labelWidth={20} htmlFor="data-source-picker">
<>
<VariableLegend>Query options</VariableLegend>
<Field label="Data source" htmlFor="data-source-picker">
<DataSourcePicker
current={this.props.variable.datasource}
onChange={this.onDataSourceChange}
variables={true}
width={30}
/>
</InlineField>
<QueryVariableRefreshSelect onChange={this.onRefreshChange} refresh={this.props.variable.refresh} />
</InlineFieldRow>
<div
className={css`
flex-direction: column;
width: 100%;
`}
>
</Field>
{this.renderQueryEditor()}
</div>
<VariableTextField
<VariableTextAreaField
value={this.state.regex ?? this.props.variable.regex}
name="Regex"
placeholder="/.*-(?<text>.*)-(?<value>.*)-.*/"
onChange={this.onRegExChange}
onBlur={this.onRegExBlur}
labelWidth={20}
interactive={true}
tooltip={
description={
<div>
Optional, if you want to extract part of a series name or metric node segment. Named capture groups
can be used to separate the display text and value (
Optional, if you want to extract part of a series name or metric node segment.
<br />
Named capture groups can be used to separate the display text and value (
<a
className="external-link"
href="https://grafana.com/docs/grafana/latest/variables/filter-variables-with-regex#filter-and-modify-using-named-text-and-value-capture-groups"
@@ -216,19 +206,24 @@ export class QueryVariableEditorUnConnected extends PureComponent<Props, State>
).
</div>
}
placeholder="/.*-(?<text>.*)-(?<value>.*)-.*/"
onChange={this.onRegExChange}
onBlur={this.onRegExBlur}
testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRegExInputV2}
grow
width={52}
/>
<QueryVariableSortSelect onChange={this.onSortChange} sort={this.props.variable.sort} />
</VerticalGroup>
<QueryVariableSortSelect onChange={this.onSortChange} sort={this.props.variable.sort} />
<QueryVariableRefreshSelect onChange={this.onRefreshChange} refresh={this.props.variable.refresh} />
<VariableLegend>Selection options</VariableLegend>
<SelectionOptionsEditor
variable={this.props.variable}
onPropChange={this.onSelectionOptionsChange}
onMultiChanged={this.props.changeVariableMultiValue}
/>
</VerticalGroup>
</VerticalGroup>
</>
);
}
}

View File

@@ -1,13 +1,10 @@
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';
import { VariableRefresh } from '@grafana/data';
import { Field, RadioButtonGroup } from '@grafana/ui';
interface Props {
onChange: (option: SelectableValue<VariableRefresh>) => void;
onChange: (option: VariableRefresh) => void;
refresh: VariableRefresh;
}
@@ -17,17 +14,14 @@ const REFRESH_OPTIONS = [
];
export function QueryVariableRefreshSelect({ onChange, refresh }: PropsWithChildren<Props>) {
const value = useMemo(() => REFRESH_OPTIONS.find((o) => o.value === refresh) ?? REFRESH_OPTIONS[0], [refresh]);
const value = useMemo(
() => REFRESH_OPTIONS.find((o) => o.value === refresh)?.value ?? REFRESH_OPTIONS[0].value,
[refresh]
);
return (
<VariableSelectField
name="Refresh"
value={value}
options={REFRESH_OPTIONS}
onChange={onChange}
labelWidth={10}
testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsRefreshSelectV2}
tooltip="When to update the values of this variable."
/>
<Field label="Refresh" description="When to update the values of this variable">
<RadioButtonGroup options={REFRESH_OPTIONS} onChange={onChange} value={value} />
</Field>
);
}

View File

@@ -27,12 +27,12 @@ export function QueryVariableSortSelect({ onChange, sort }: PropsWithChildren<Pr
return (
<VariableSelectField
name="Sort"
description="How to sort the values of this variable"
value={value}
options={SORT_OPTIONS}
onChange={onChange}
labelWidth={10}
testId={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsSortSelectV2}
tooltip="How to sort the values of this variable."
width={25}
/>
);
}

View File

@@ -1,9 +1,8 @@
import React, { FormEvent, ReactElement, useCallback } from 'react';
import { selectors } from '@grafana/e2e-selectors';
import { VerticalGroup } from '@grafana/ui';
import { VariableSectionHeader } from '../editor/VariableSectionHeader';
import { VariableLegend } from '../editor/VariableLegend';
import { VariableTextField } from '../editor/VariableTextField';
import { VariableEditorProps } from '../editor/types';
import { TextBoxVariableModel } from '../types';
@@ -24,18 +23,17 @@ export function TextBoxVariableEditor({ onPropChange, variable: { query } }: Pro
const onBlur = useCallback((e: FormEvent<HTMLInputElement>) => updateVariable(e, true), [updateVariable]);
return (
<VerticalGroup spacing="xs">
<VariableSectionHeader name="Text options" />
<>
<VariableLegend>Text options</VariableLegend>
<VariableTextField
value={query}
name="Default value"
placeholder="default value, if any"
onChange={onChange}
onBlur={onBlur}
labelWidth={20}
grow
width={30}
testId={selectors.pages.Dashboard.Settings.Variables.Edit.TextBoxVariable.textBoxOptionsQueryInputV2}
/>
</VerticalGroup>
</>
);
}

View File

@@ -193,9 +193,10 @@ export function getVariableTypes(): Array<{ label: string; value: VariableType }
return variableAdapters
.list()
.filter((v) => v.id !== 'system')
.map(({ id, name }) => ({
.map(({ id, name, description }) => ({
label: name,
value: id,
description,
}));
}