Commit 04d857df by Hugo Häggmark Committed by GitHub

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
parent b7dc6a1a
import React, { FC, ReactNode, HTMLProps } from 'react'; import React, { FC, HTMLProps, ReactNode } from 'react';
import { css, cx } from 'emotion'; import { css, cx } from 'emotion';
import { useStyles } from '../../themes'; import { useStyles } from '../../themes';
...@@ -18,6 +18,7 @@ export const InlineFieldRow: FC<Props> = ({ children, className, ...htmlProps }) ...@@ -18,6 +18,7 @@ export const InlineFieldRow: FC<Props> = ({ children, className, ...htmlProps })
const getStyles = () => { const getStyles = () => {
return { return {
container: css` container: css`
label: InlineFieldRow;
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-wrap: wrap; flex-wrap: wrap;
......
...@@ -8,6 +8,10 @@ import { AdHocVariableEditorState } from './reducer'; ...@@ -8,6 +8,10 @@ import { AdHocVariableEditorState } from './reducer';
import { changeVariableDatasource, initAdHocVariableEditor } from './actions'; import { changeVariableDatasource, initAdHocVariableEditor } from './actions';
import { connectWithStore } from 'app/core/utils/connectWithReduxStore'; import { connectWithStore } from 'app/core/utils/connectWithReduxStore';
import { StoreState } from 'app/types'; 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> {} export interface OwnProps extends VariableEditorProps<AdHocVariableModel> {}
...@@ -27,45 +31,33 @@ export class AdHocVariableEditorUnConnected extends PureComponent<Props> { ...@@ -27,45 +31,33 @@ export class AdHocVariableEditorUnConnected extends PureComponent<Props> {
this.props.initAdHocVariableEditor(); this.props.initAdHocVariableEditor();
} }
onDatasourceChanged = (event: React.ChangeEvent<HTMLSelectElement>) => { onDatasourceChanged = (option: SelectableValue<string>) => {
this.props.changeVariableDatasource(event.target.value); this.props.changeVariableDatasource(option.value ?? '');
}; };
render() { render() {
const { variable, editor } = this.props; const { variable, editor } = this.props;
const dataSources = editor.extended?.dataSources ?? []; const dataSources = editor.extended?.dataSources ?? [];
const infoText = editor.extended?.infoText ?? null; 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 ( return (
<> <VerticalGroup spacing="xs">
<div className="gf-form-group"> <VariableSectionHeader name="Options" />
<h5 className="section-heading">Options</h5> <VerticalGroup spacing="sm">
<div className="gf-form max-width-21"> <InlineFieldRow>
<span className="gf-form-label width-8">Data source</span> <VariableSelectField
<div className="gf-form-select-wrapper max-width-14"> name="Data source"
<select value={value}
className="gf-form-input" options={options}
required
onChange={this.onDatasourceChanged} onChange={this.onDatasourceChanged}
value={variable.datasource ?? ''} labelWidth={10}
aria-label="Variable editor Form AdHoc DataSource select" />
> </InlineFieldRow>
{dataSources.map(ds => ( {infoText ? <Alert title={infoText} severity="info" /> : null}
<option key={ds.value ?? ''} value={ds.value ?? ''} label={ds.text}> </VerticalGroup>
{ds.text} </VerticalGroup>
</option>
))}
</select>
</div>
</div>
</div>
{infoText && (
<div className="alert alert-info gf-form-group" aria-label="Variable editor Form Alert">
{infoText}
</div>
)}
</>
); );
} }
} }
......
import React, { ChangeEvent, FocusEvent, PureComponent } from 'react'; import React, { ChangeEvent, FocusEvent, PureComponent } from 'react';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { VerticalGroup } from '@grafana/ui';
import { ConstantVariableModel } from '../types'; import { ConstantVariableModel } from '../types';
import { VariableEditorProps } from '../editor/types'; import { VariableEditorProps } from '../editor/types';
import { VariableSectionHeader } from '../editor/VariableSectionHeader';
import { VariableTextField } from '../editor/VariableTextField';
export interface Props extends VariableEditorProps<ConstantVariableModel> {} export interface Props extends VariableEditorProps<ConstantVariableModel> {}
...@@ -24,23 +27,19 @@ export class ConstantVariableEditor extends PureComponent<Props> { ...@@ -24,23 +27,19 @@ export class ConstantVariableEditor extends PureComponent<Props> {
render() { render() {
return ( return (
<> <VerticalGroup spacing="xs">
<div className="gf-form-group"> <VariableSectionHeader name="Constant Options" />
<h5 className="section-heading">Constant options</h5> <VariableTextField
<div className="gf-form">
<span className="gf-form-label">Value</span>
<input
type="text"
className="gf-form-input"
value={this.props.variable.query} value={this.props.variable.query}
name="Value"
placeholder="your metric prefix"
onChange={this.onChange} onChange={this.onChange}
onBlur={this.onBlur} onBlur={this.onBlur}
placeholder="your metric prefix" labelWidth={20}
aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInput} ariaLabel={selectors.pages.Dashboard.Settings.Variables.Edit.ConstantVariable.constantOptionsQueryInput}
grow
/> />
</div> </VerticalGroup>
</div>
</>
); );
} }
} }
...@@ -4,9 +4,11 @@ import { SelectionOptionsEditor } from '../editor/SelectionOptionsEditor'; ...@@ -4,9 +4,11 @@ import { SelectionOptionsEditor } from '../editor/SelectionOptionsEditor';
import { OnPropChangeArguments, VariableEditorProps } from '../editor/types'; import { OnPropChangeArguments, VariableEditorProps } from '../editor/types';
import { connectWithStore } from 'app/core/utils/connectWithReduxStore'; import { connectWithStore } from 'app/core/utils/connectWithReduxStore';
import { MapDispatchToProps, MapStateToProps } from 'react-redux'; import { MapDispatchToProps, MapStateToProps } from 'react-redux';
import { Field, TextArea } from '@grafana/ui'; import { VerticalGroup } from '@grafana/ui';
import { StoreState } from 'app/types'; import { StoreState } from 'app/types';
import { changeVariableMultiValue } from '../state/actions'; import { changeVariableMultiValue } from '../state/actions';
import { VariableSectionHeader } from '../editor/VariableSectionHeader';
import { VariableTextAreaField } from '../editor/VariableTextAreaField';
interface OwnProps extends VariableEditorProps<CustomVariableModel> {} interface OwnProps extends VariableEditorProps<CustomVariableModel> {}
...@@ -40,31 +42,28 @@ class CustomVariableEditorUnconnected extends PureComponent<Props> { ...@@ -40,31 +42,28 @@ class CustomVariableEditorUnconnected extends PureComponent<Props> {
render() { render() {
return ( return (
<> <VerticalGroup spacing="xs">
<div className="gf-form-group"> <VariableSectionHeader name="Custom Options" />
<h5 className="section-heading">Custom Options</h5> <VerticalGroup spacing="md">
<div className="gf-form"> <VerticalGroup spacing="none">
<Field label="Values separated by comma"> <VariableTextAreaField
<TextArea name="Values separated by comma"
className="gf-form-input"
value={this.props.variable.query} value={this.props.variable.query}
placeholder="1, 10, mykey : myvalue, myvalue, escaped\,value"
onChange={this.onChange} onChange={this.onChange}
onBlur={this.onBlur} onBlur={this.onBlur}
rows={5}
cols={81}
placeholder="1, 10, mykey : myvalue, myvalue, escaped\,value"
required required
aria-label="Variable editor Form Custom Query field" width={50}
labelWidth={27}
/> />
</Field> </VerticalGroup>
</div>
</div>
<SelectionOptionsEditor <SelectionOptionsEditor
variable={this.props.variable} variable={this.props.variable}
onPropChange={this.onSelectionOptionsChange} onPropChange={this.onSelectionOptionsChange}
onMultiChanged={this.props.changeVariableMultiValue} onMultiChanged={this.props.changeVariableMultiValue}
/> />{' '}
</> </VerticalGroup>
</VerticalGroup>
); );
} }
} }
......
import React, { ChangeEvent, FocusEvent, PureComponent } from 'react'; 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 { DataSourceVariableModel, VariableWithMultiSupport } from '../types';
import { OnPropChangeArguments, VariableEditorProps } from '../editor/types'; import { OnPropChangeArguments, VariableEditorProps } from '../editor/types';
import { SelectionOptionsEditor } from '../editor/SelectionOptionsEditor'; import { SelectionOptionsEditor } from '../editor/SelectionOptionsEditor';
import { InlineFormLabel } from '@grafana/ui';
import { VariableEditorState } from '../editor/reducer'; import { VariableEditorState } from '../editor/reducer';
import { DataSourceVariableEditorState } from './reducer'; import { DataSourceVariableEditorState } from './reducer';
import { initDataSourceVariableEditor } from './actions'; import { initDataSourceVariableEditor } from './actions';
import { MapDispatchToProps, MapStateToProps } from 'react-redux';
import { StoreState } from '../../../types'; import { StoreState } from '../../../types';
import { connectWithStore } from '../../../core/utils/connectWithReduxStore'; import { connectWithStore } from '../../../core/utils/connectWithReduxStore';
import { changeVariableMultiValue } from '../state/actions'; 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> {} export interface OwnProps extends VariableEditorProps<DataSourceVariableModel> {}
...@@ -58,37 +62,38 @@ export class DataSourceVariableEditorUnConnected extends PureComponent<Props> { ...@@ -58,37 +62,38 @@ export class DataSourceVariableEditorUnConnected extends PureComponent<Props> {
return value ?? ''; return value ?? '';
}; };
onDataSourceTypeChanged = (event: ChangeEvent<HTMLSelectElement>) => { onDataSourceTypeChanged = (option: SelectableValue<string>) => {
this.props.onPropChange({ propName: 'query', propValue: event.target.value, updateOptions: true }); this.props.onPropChange({ propName: 'query', propValue: option.value, updateOptions: true });
}; };
render() { 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 ( return (
<> <VerticalGroup spacing="xs">
<div className="gf-form-group"> <VariableSectionHeader name="Data source options" />
<h5 className="section-heading">Data source options</h5> <VerticalGroup spacing="md">
<VerticalGroup spacing="xs">
<div className="gf-form"> <InlineFieldRow>
<label className="gf-form-label width-12">Type</label> <VariableSelectField
<div className="gf-form-select-wrapper max-width-18"> name="Type"
<select value={typeValue}
className="gf-form-input" options={typeOptions}
value={this.getSelectedDataSourceTypeValue()}
onChange={this.onDataSourceTypeChanged} onChange={this.onDataSourceTypeChanged}
> labelWidth={10}
{this.props.editor.extended?.dataSourceTypes?.length && />
this.props.editor.extended?.dataSourceTypes?.map(ds => ( </InlineFieldRow>
<option key={ds.value ?? ''} value={ds.value ?? ''} label={ds.text}> <InlineFieldRow>
{ds.text} <VariableTextField
</option> value={this.props.variable.regex}
))} name="Instance name filter"
</select> placeholder="/.*-(.*)-.*/"
</div> onChange={this.onRegExChange}
</div> onBlur={this.onRegExBlur}
labelWidth={20}
<div className="gf-form">
<InlineFormLabel
width={12}
tooltip={ tooltip={
<div> <div>
Regex filter for which data source instances to choose from in the variable value dropdown. Leave 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> { ...@@ -98,26 +103,17 @@ export class DataSourceVariableEditorUnConnected extends PureComponent<Props> {
Example: <code>/^prod/</code> Example: <code>/^prod/</code>
</div> </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> </InlineFieldRow>
</div> </VerticalGroup>
<SelectionOptionsEditor <SelectionOptionsEditor
variable={this.props.variable} variable={this.props.variable}
onPropChange={this.onSelectionOptionsChange} onPropChange={this.onSelectionOptionsChange}
onMultiChanged={this.props.changeVariableMultiValue} onMultiChanged={this.props.changeVariableMultiValue}
/> />
</> </VerticalGroup>
</VerticalGroup>
); );
} }
} }
......
...@@ -2,7 +2,8 @@ import React, { FC, useCallback, useState } from 'react'; ...@@ -2,7 +2,8 @@ import React, { FC, useCallback, useState } from 'react';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { VariableQueryProps } from 'app/types/plugins'; 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 { GrafanaTheme } from '@grafana/data';
import { css } from 'emotion'; import { css } from 'emotion';
...@@ -17,6 +18,7 @@ export const LegacyVariableQueryEditor: FC<VariableQueryProps> = ({ onChange, qu ...@@ -17,6 +18,7 @@ export const LegacyVariableQueryEditor: FC<VariableQueryProps> = ({ onChange, qu
}, },
[onChange] [onChange]
); );
const onBlur = useCallback( const onBlur = useCallback(
(event: React.FormEvent<HTMLTextAreaElement>) => { (event: React.FormEvent<HTMLTextAreaElement>) => {
onChange(event.currentTarget.value, event.currentTarget.value); onChange(event.currentTarget.value, event.currentTarget.value);
...@@ -25,19 +27,17 @@ export const LegacyVariableQueryEditor: FC<VariableQueryProps> = ({ onChange, qu ...@@ -25,19 +27,17 @@ export const LegacyVariableQueryEditor: FC<VariableQueryProps> = ({ onChange, qu
); );
return ( return (
<div className="gf-form"> <div className={styles.container}>
<InlineField label="Query" labelWidth={20} grow={false} className={styles.inlineFieldOverride}> <VariableTextAreaField
<span hidden /> name="Query"
</InlineField>
<TextArea
rows={getLineCount(value)}
className="gf-form-input"
value={value} value={value}
placeholder="metric name or tags query"
width={100}
onChange={onValueChange} onChange={onValueChange}
onBlur={onBlur} onBlur={onBlur}
placeholder="metric name or tags query"
required required
aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput} labelWidth={20}
ariaLabel={selectors.pages.Dashboard.Settings.Variables.Edit.QueryVariable.queryOptionsQueryInput}
/> />
</div> </div>
); );
...@@ -45,18 +45,10 @@ export const LegacyVariableQueryEditor: FC<VariableQueryProps> = ({ onChange, qu ...@@ -45,18 +45,10 @@ export const LegacyVariableQueryEditor: FC<VariableQueryProps> = ({ onChange, qu
function getStyles(theme: GrafanaTheme) { function getStyles(theme: GrafanaTheme) {
return { return {
inlineFieldOverride: css` container: css`
margin: 0; margin-bottom: ${theme.spacing.xs};
`, `,
}; };
} }
LegacyVariableQueryEditor.displayName = LEGACY_VARIABLE_QUERY_EDITOR_NAME; LegacyVariableQueryEditor.displayName = LEGACY_VARIABLE_QUERY_EDITOR_NAME;
const getLineCount = (value: any) => {
if (value && typeof value === 'string') {
return value.split('\n').length;
}
return 1;
};
import React, { FunctionComponent, useCallback } from 'react'; import React, { FunctionComponent, useCallback } from 'react';
import { LegacyForms } from '@grafana/ui'; import { InlineFieldRow, VerticalGroup } from '@grafana/ui';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { VariableWithMultiSupport } from '../types'; import { VariableWithMultiSupport } from '../types';
import { VariableEditorProps } from './types'; import { VariableEditorProps } from './types';
import { toVariableIdentifier, VariableIdentifier } from '../state/types'; import { toVariableIdentifier, VariableIdentifier } from '../state/types';
import { VariableSectionHeader } from './VariableSectionHeader';
const { Switch } = LegacyForms; import { VariableSwitchField } from './VariableSwitchField';
import { VariableTextField } from './VariableTextField';
export interface SelectionOptionsEditorProps<Model extends VariableWithMultiSupport = VariableWithMultiSupport> export interface SelectionOptionsEditorProps<Model extends VariableWithMultiSupport = VariableWithMultiSupport>
extends VariableEditorProps<Model> { extends VariableEditorProps<Model> {
...@@ -35,42 +36,39 @@ export const SelectionOptionsEditor: FunctionComponent<SelectionOptionsEditorPro ...@@ -35,42 +36,39 @@ export const SelectionOptionsEditor: FunctionComponent<SelectionOptionsEditorPro
[props.onPropChange] [props.onPropChange]
); );
return ( return (
<div className="section gf-form-group"> <VerticalGroup spacing="none">
<h5 className="section-heading">Selection Options</h5> <VariableSectionHeader name="Selection Options" />
<div className="section"> <InlineFieldRow>
<div aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch}> <VariableSwitchField
<Switch value={props.variable.multi}
label="Multi-value" name="Multi-value"
labelClass="width-10" tooltip="Enables multiple values to be selected at the same time"
checked={props.variable.multi}
onChange={onMultiChanged} onChange={onMultiChanged}
tooltip={'Enables multiple values to be selected at the same time'} ariaLabel={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsMultiSwitch}
/> />
</div> </InlineFieldRow>
<div aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch}> <InlineFieldRow>
<Switch <VariableSwitchField
label="Include All option" value={props.variable.includeAll}
labelClass="width-10" name="Include All option"
checked={props.variable.includeAll} tooltip="Enables an option to include all variables"
onChange={onIncludeAllChanged} onChange={onIncludeAllChanged}
tooltip={'Enables an option to include all variables'} ariaLabel={selectors.pages.Dashboard.Settings.Variables.Edit.General.selectionOptionsIncludeAllSwitch}
/> />
</div> </InlineFieldRow>
</div>
{props.variable.includeAll && ( {props.variable.includeAll && (
<div className="gf-form"> <InlineFieldRow>
<span className="gf-form-label width-10">Custom all value</span> <VariableTextField
<input
type="text"
className="gf-form-input max-width-15"
value={props.variable.allValue ?? ''} value={props.variable.allValue ?? ''}
onChange={onAllValueChanged} onChange={onAllValueChanged}
name="Custom all value"
placeholder="blank = auto" 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'; SelectionOptionsEditor.displayName = 'SelectionOptionsEditor';
import React, { ChangeEvent, FormEvent, PureComponent } from 'react'; import React, { ChangeEvent, FormEvent, PureComponent } from 'react';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
import { AppEvents, LoadingState, VariableType } from '@grafana/data'; import { AppEvents, LoadingState, SelectableValue, VariableType } from '@grafana/data';
import { Icon, InlineFormLabel } from '@grafana/ui'; import { Button, Icon, InlineFieldRow, VerticalGroup } from '@grafana/ui';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { variableAdapters } from '../adapters'; import { variableAdapters } from '../adapters';
...@@ -18,7 +18,11 @@ import { connectWithStore } from '../../../core/utils/connectWithReduxStore'; ...@@ -18,7 +18,11 @@ import { connectWithStore } from '../../../core/utils/connectWithReduxStore';
import { OnPropChangeArguments } from './types'; import { OnPropChangeArguments } from './types';
import { changeVariableProp, changeVariableType } from '../state/sharedReducer'; import { changeVariableProp, changeVariableType } from '../state/sharedReducer';
import { updateOptions } from '../state/actions'; 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 { export interface OwnProps {
identifier: VariableIdentifier; identifier: VariableIdentifier;
...@@ -63,11 +67,11 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props> { ...@@ -63,11 +67,11 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props> {
this.props.changeVariableName(this.props.identifier, event.target.value); this.props.changeVariableName(this.props.identifier, event.target.value);
}; };
onTypeChange = (event: ChangeEvent<HTMLSelectElement>) => { onTypeChange = (option: SelectableValue<VariableType>) => {
event.preventDefault(); if (!option.value) {
this.props.changeVariableType( return;
toVariablePayload(this.props.identifier, { newType: event.target.value as VariableType }) }
); this.props.changeVariableType(toVariablePayload(this.props.identifier, { newType: option.value }));
}; };
onLabelChange = (event: ChangeEvent<HTMLInputElement>) => { onLabelChange = (event: ChangeEvent<HTMLInputElement>) => {
...@@ -77,12 +81,17 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props> { ...@@ -77,12 +81,17 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props> {
); );
}; };
onHideChange = (event: ChangeEvent<HTMLSelectElement>) => { onDescriptionChange = (event: ChangeEvent<HTMLInputElement>) => {
event.preventDefault(); this.props.changeVariableProp(
toVariablePayload(this.props.identifier, { propName: 'description', propValue: event.target.value })
);
};
onHideChange = (option: SelectableValue<VariableHide>) => {
this.props.changeVariableProp( this.props.changeVariableProp(
toVariablePayload(this.props.identifier, { toVariablePayload(this.props.identifier, {
propName: 'hide', propName: 'hide',
propValue: parseInt(event.target.value, 10) as VariableHide, propValue: option.value,
}) })
); );
}; };
...@@ -114,42 +123,20 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props> { ...@@ -114,42 +123,20 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props> {
return ( return (
<div> <div>
<form aria-label="Variable editor Form" onSubmit={this.onHandleSubmit}> <form aria-label="Variable editor Form" onSubmit={this.onHandleSubmit}>
<h5 className="section-heading">General</h5> <VerticalGroup spacing="lg">
<div className="gf-form-group"> <VerticalGroup spacing="none">
<div className="gf-form-inline"> <VariableSectionHeader name="General" />
<div className="gf-form max-width-19"> <InlineFieldRow>
<span className="gf-form-label width-6">Name</span> <VariableTextField
<input
type="text"
className="gf-form-input"
name="name"
placeholder="name"
required
value={this.props.editor.name} value={this.props.editor.name}
onChange={this.onNameChange} 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> <VariableTypeSelect onChange={this.onTypeChange} type={this.props.variable.type} />
<div className="gf-form max-width-19"> </InlineFieldRow>
<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>
{this.props.editor.errors.name && ( {this.props.editor.errors.name && (
<div className="gf-form"> <div className="gf-form">
...@@ -157,57 +144,43 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props> { ...@@ -157,57 +144,43 @@ export class VariableEditorEditorUnConnected extends PureComponent<Props> {
</div> </div>
)} )}
<div className="gf-form-inline"> <InlineFieldRow>
<div className="gf-form max-width-19"> <VariableTextField
<span className="gf-form-label width-6">Label</span>
<input
type="text"
className="gf-form-input"
value={this.props.variable.label ?? ''} value={this.props.variable.label ?? ''}
onChange={this.onLabelChange} onChange={this.onLabelChange}
name="Label"
placeholder="optional display name" placeholder="optional display name"
aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInput} ariaLabel={selectors.pages.Dashboard.Settings.Variables.Edit.General.generalLabelInput}
/> />
</div> <VariableHideSelect onChange={this.onHideChange} hide={this.props.variable.hide} />
<div className="gf-form max-width-19"> </InlineFieldRow>
<span className="gf-form-label width-6">Hide</span>
<div className="gf-form-select-wrapper max-width-15"> <VariableTextField
<select name="Description"
className="gf-form-input" value={variable.description ?? ''}
value={this.props.variable.hide} placeholder="descriptive text"
onChange={this.onHideChange} onChange={this.onDescriptionChange}
aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.General.generalHideSelect} grow
> />
<option label="" value={VariableHide.dontHide}> </VerticalGroup>
{''}
</option>
<option label="Label" value={VariableHide.hideLabel}>
Label
</option>
<option label="Variable" value={VariableHide.hideVariable}>
Variable
</option>
</select>
</div>
</div>
</div>
</div>
{EditorToRender && <EditorToRender variable={this.props.variable} onPropChange={this.onPropChanged} />} {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"> <VerticalGroup spacing="none">
<button <Button
type="submit" type="submit"
className="btn btn-primary"
aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.General.submitButton} aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.General.submitButton}
disabled={loading} disabled={loading}
> >
Update Update
{loading ? <Icon className="spin-clockwise" name="sync" size="sm" style={{ marginLeft: '2px' }} /> : null} {loading ? (
</button> <Icon className="spin-clockwise" name="sync" size="sm" style={{ marginLeft: '2px' }} />
</div> ) : null}
</Button>
</VerticalGroup>
</VerticalGroup>
</form> </form>
</div> </div>
); );
......
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}
/>
);
}
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};
`,
};
}
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};
`,
};
}
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};
`,
};
}
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;
`,
};
}
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>
);
}
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}
/>
);
}
import React, { useCallback, useEffect, useState } from 'react'; import React, { MouseEvent, useCallback, useEffect, useState } from 'react';
import { VariableModel, VariableOption, VariableWithOptions } from '../types'; import { VariableOption, VariableWithOptions } from '../types';
import { selectors } from '@grafana/e2e-selectors'; 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 { 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 [previewLimit, setPreviewLimit] = useState(20);
const [previewOptions, setPreviewOptions] = useState<VariableOption[]>([]); const [previewOptions, setPreviewOptions] = useState<VariableOption[]>([]);
const showMoreOptions = useCallback(() => setPreviewLimit(previewLimit + 20), [previewLimit, setPreviewLimit]); const showMoreOptions = useCallback(
useEffect(() => { (event: MouseEvent) => {
if (!variable || !variable.hasOwnProperty('options')) { event.preventDefault();
return; setPreviewLimit(previewLimit + 20);
} },
const variableWithOptions = variable as VariableWithOptions; [previewLimit, setPreviewLimit]
setPreviewOptions(variableWithOptions.options.slice(0, previewLimit)); );
}, [previewLimit, variable]); const styles = useStyles(getStyles);
useEffect(() => setPreviewOptions(options.slice(0, previewLimit)), [previewLimit, options]);
if (!previewOptions.length) { if (!previewOptions.length) {
return null; return null;
} }
return ( return (
<div className="gf-form-group"> <VerticalGroup spacing="none">
<h5>Preview of values</h5> <h5>Preview of values</h5>
<div className="gf-form-inline"> <InlineFieldRow>
{previewOptions.map((o, index) => ( {previewOptions.map((o, index) => (
<div className="gf-form" key={`${o.value}-${index}`}> <InlineFieldRow key={`${o.value}-${index}`} className={styles.optionContainer}>
<span <InlineLabel aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption}>
className="gf-form-label"
aria-label={selectors.pages.Dashboard.Settings.Variables.Edit.General.previewOfValuesOption}
>
{o.text} {o.text}
</span> </InlineLabel>
</div> </InlineFieldRow>
))} ))}
{previewOptions.length > previewLimit && ( </InlineFieldRow>
<div className="gf-form" ng-if="current.options.length > optionsLimit"> {options.length > previewLimit && (
<a <InlineFieldRow className={styles.optionContainer}>
className="gf-form-label btn-secondary" <Button
onClick={showMoreOptions} onClick={showMoreOptions}
variant="secondary"
size="sm"
aria-label="Variable editor Preview of Values Show More link" aria-label="Variable editor Preview of Values Show More link"
> >
Show more Show more
</a> </Button>
</div> </InlineFieldRow>
)} )}
</div> </VerticalGroup>
</div>
); );
}; };
VariableValuesPreview.displayName = 'VariableValuesPreview'; VariableValuesPreview.displayName = 'VariableValuesPreview';
function getStyles(theme: GrafanaTheme) {
return {
optionContainer: css`
margin-left: ${theme.spacing.xs};
margin-bottom: ${theme.spacing.xs};
`,
};
}
...@@ -21,6 +21,7 @@ import { ...@@ -21,6 +21,7 @@ import {
VariableModel, VariableModel,
VariableQueryEditorType, VariableQueryEditorType,
VariableWithMultiSupport, VariableWithMultiSupport,
VariableWithOptions,
} from './types'; } from './types';
import { VariableQueryProps } from '../../types'; import { VariableQueryProps } from '../../types';
import { LEGACY_VARIABLE_QUERY_EDITOR_NAME } from './editor/LegacyVariableQueryEditor'; import { LEGACY_VARIABLE_QUERY_EDITOR_NAME } from './editor/LegacyVariableQueryEditor';
...@@ -42,6 +43,15 @@ export const isMulti = (model: VariableModel): model is VariableWithMultiSupport ...@@ -42,6 +43,15 @@ export const isMulti = (model: VariableModel): model is VariableWithMultiSupport
return withMulti.hasOwnProperty('multi') && typeof withMulti.multi === 'boolean'; 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< interface DataSourceWithLegacyVariableSupport<
TQuery extends DataQuery = DataQuery, TQuery extends DataQuery = DataQuery,
TOptions extends DataSourceJsonData = DataSourceJsonData TOptions extends DataSourceJsonData = DataSourceJsonData
......
import React, { ChangeEvent, FocusEvent, PureComponent } from 'react'; import React, { ChangeEvent, FocusEvent, PureComponent } from 'react';
import { InlineFieldRow, VerticalGroup } from '@grafana/ui';
import { IntervalVariableModel } from '../types'; import { IntervalVariableModel } from '../types';
import { VariableEditorProps } from '../editor/types'; import { VariableEditorProps } from '../editor/types';
import { InlineFormLabel, LegacyForms } from '@grafana/ui'; import { VariableSectionHeader } from '../editor/VariableSectionHeader';
import { VariableTextField } from '../editor/VariableTextField';
const { Switch } = LegacyForms; import { VariableSwitchField } from '../editor/VariableSwitchField';
import { VariableSelectField } from '../editor/VariableSelectField';
import { SelectableValue } from '@grafana/data';
export interface Props extends VariableEditorProps<IntervalVariableModel> {} export interface Props extends VariableEditorProps<IntervalVariableModel> {}
...@@ -32,10 +35,10 @@ export class IntervalVariableEditor extends PureComponent<Props> { ...@@ -32,10 +35,10 @@ export class IntervalVariableEditor extends PureComponent<Props> {
}); });
}; };
onAutoCountChanged = (event: ChangeEvent<HTMLSelectElement>) => { onAutoCountChanged = (option: SelectableValue<number>) => {
this.props.onPropChange({ this.props.onPropChange({
propName: 'auto_count', propName: 'auto_count',
propValue: event.target.value, propValue: option.value,
updateOptions: true, updateOptions: true,
}); });
}; };
...@@ -49,73 +52,59 @@ export class IntervalVariableEditor extends PureComponent<Props> { ...@@ -49,73 +52,59 @@ export class IntervalVariableEditor extends PureComponent<Props> {
}; };
render() { render() {
return ( const { variable } = this.props;
<> const stepOptions = [1, 2, 3, 4, 5, 10, 20, 30, 40, 50, 100, 200, 300, 400, 500].map(count => ({
<div className="gf-form-group"> label: `${count}`,
<h5 className="section-heading">Interval Options</h5> value: count,
}));
const stepValue = stepOptions.find(o => o.value === variable.auto_count) ?? stepOptions[0];
<div className="gf-form"> return (
<span className="gf-form-label width-9">Values</span> <VerticalGroup spacing="xs">
<input <VariableSectionHeader name="Interval Options" />
type="text" <VerticalGroup spacing="none">
className="gf-form-input" <VariableTextField
value={this.props.variable.query} value={this.props.variable.query}
name="Values"
placeholder="1m,10m,1h,6h,1d,7d" placeholder="1m,10m,1h,6h,1d,7d"
onChange={this.onQueryChanged} onChange={this.onQueryChanged}
onBlur={this.onQueryBlur} onBlur={this.onQueryBlur}
labelWidth={20}
grow
required required
/> />
</div> <InlineFieldRow>
<VariableSwitchField
<div className="gf-form-inline"> value={this.props.variable.auto}
<Switch name="Auto Option"
label="Auto Option" tooltip="Interval will be dynamically calculated by dividing time range by the count specified"
labelClass="width-9"
checked={this.props.variable.auto}
onChange={this.onAutoChange} 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"> <VariableSelectField
<InlineFormLabel name="Step count"
width={9} value={stepValue}
tooltip={'How many times should the current time range be divided to calculate the value'} options={stepOptions}
>
Step count
</InlineFormLabel>
<div className="gf-form-select-wrapper max-width-10">
<select
className="gf-form-input"
value={this.props.variable.auto_count}
onChange={this.onAutoCountChanged} onChange={this.onAutoCountChanged}
> tooltip="How many times should the current time range be divided to calculate the value"
{[1, 2, 3, 4, 5, 10, 20, 30, 40, 50, 100, 200, 300, 400, 500].map(count => ( labelWidth={7}
<option key={`auto_count_key-${count}`} label={`${count}`}> width={9}
{count} />
</option> <VariableTextField
))}
</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} value={this.props.variable.auto_min}
onChange={this.onAutoMinChanged} name="Min interval"
placeholder="10s" 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>
); );
} }
} }
import React, { FunctionComponent, useMemo } from 'react'; import React, { FunctionComponent, PropsWithChildren, ReactElement, useMemo } from 'react';
import { VariableHide, VariableModel } from '../types'; import { VariableHide, VariableModel } from '../types';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { variableAdapters } from '../adapters'; import { variableAdapters } from '../adapters';
import { Tooltip } from '@grafana/ui';
interface Props { interface Props {
variable: VariableModel; variable: VariableModel;
...@@ -9,7 +10,6 @@ interface Props { ...@@ -9,7 +10,6 @@ interface Props {
export const PickerRenderer: FunctionComponent<Props> = props => { export const PickerRenderer: FunctionComponent<Props> = props => {
const PickerToRender = useMemo(() => variableAdapters.get(props.variable.type).picker, [props.variable]); 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) { if (!props.variable) {
return <div>Couldn't load variable</div>; return <div>Couldn't load variable</div>;
...@@ -17,17 +17,40 @@ export const PickerRenderer: FunctionComponent<Props> = props => { ...@@ -17,17 +17,40 @@ export const PickerRenderer: FunctionComponent<Props> = props => {
return ( return (
<div className="gf-form"> <div className="gf-form">
{props.variable.hide === VariableHide.dontHide && ( <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 <label
className="gf-form-label gf-form-label--variable" className="gf-form-label gf-form-label--variable"
aria-label={selectors.pages.Dashboard.SubMenu.submenuItemLabels(labelOrName)} aria-label={selectors.pages.Dashboard.SubMenu.submenuItemLabels(labelOrName)}
> >
{labelOrName} {labelOrName}
</label> </label>
)} </Tooltip>
{props.variable.hide !== VariableHide.hideVariable && PickerToRender && (
<PickerToRender variable={props.variable} />
)}
</div>
); );
}; }
return (
<label
className="gf-form-label gf-form-label--variable"
aria-label={selectors.pages.Dashboard.SubMenu.submenuItemLabels(labelOrName)}
>
{labelOrName}
</label>
);
}
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}
/>
);
}
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."
/>
);
}
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."
/>
);
}
...@@ -756,6 +756,7 @@ function createVariable(extend?: Partial<QueryVariableModel>): QueryVariableMode ...@@ -756,6 +756,7 @@ function createVariable(extend?: Partial<QueryVariableModel>): QueryVariableMode
includeAll: true, includeAll: true,
state: LoadingState.NotStarted, state: LoadingState.NotStarted,
error: null, error: null,
description: null,
...(extend ?? {}), ...(extend ?? {}),
}; };
} }
......
...@@ -28,6 +28,7 @@ export const getVariableState = ( ...@@ -28,6 +28,7 @@ export const getVariableState = (
global: false, global: false,
state: LoadingState.NotStarted, state: LoadingState.NotStarted,
error: null, error: null,
description: null,
}; };
} }
...@@ -43,6 +44,7 @@ export const getVariableState = ( ...@@ -43,6 +44,7 @@ export const getVariableState = (
global: false, global: false,
state: LoadingState.NotStarted, state: LoadingState.NotStarted,
error: null, error: null,
description: null,
}; };
} }
......
...@@ -71,6 +71,7 @@ describe('sharedReducer', () => { ...@@ -71,6 +71,7 @@ describe('sharedReducer', () => {
global: false, global: false,
state: LoadingState.NotStarted, state: LoadingState.NotStarted,
error: null, error: null,
description: null,
}, },
'2': { '2': {
id: '2', id: '2',
...@@ -83,6 +84,7 @@ describe('sharedReducer', () => { ...@@ -83,6 +84,7 @@ describe('sharedReducer', () => {
global: false, global: false,
state: LoadingState.NotStarted, state: LoadingState.NotStarted,
error: null, error: null,
description: null,
}, },
}); });
}); });
...@@ -107,6 +109,7 @@ describe('sharedReducer', () => { ...@@ -107,6 +109,7 @@ describe('sharedReducer', () => {
global: false, global: false,
state: LoadingState.NotStarted, state: LoadingState.NotStarted,
error: null, error: null,
description: null,
}, },
'2': { '2': {
id: '2', id: '2',
...@@ -119,6 +122,7 @@ describe('sharedReducer', () => { ...@@ -119,6 +122,7 @@ describe('sharedReducer', () => {
global: false, global: false,
state: LoadingState.NotStarted, state: LoadingState.NotStarted,
error: null, error: null,
description: null,
}, },
}); });
}); });
...@@ -143,6 +147,7 @@ describe('sharedReducer', () => { ...@@ -143,6 +147,7 @@ describe('sharedReducer', () => {
global: false, global: false,
state: LoadingState.NotStarted, state: LoadingState.NotStarted,
error: null, error: null,
description: null,
}, },
'1': { '1': {
id: '1', id: '1',
...@@ -155,6 +160,7 @@ describe('sharedReducer', () => { ...@@ -155,6 +160,7 @@ describe('sharedReducer', () => {
global: false, global: false,
state: LoadingState.NotStarted, state: LoadingState.NotStarted,
error: null, error: null,
description: null,
}, },
'2': { '2': {
id: '2', id: '2',
...@@ -167,6 +173,7 @@ describe('sharedReducer', () => { ...@@ -167,6 +173,7 @@ describe('sharedReducer', () => {
global: false, global: false,
state: LoadingState.NotStarted, state: LoadingState.NotStarted,
error: null, error: null,
description: null,
}, },
'11': { '11': {
...initialQueryVariableModelState, ...initialQueryVariableModelState,
...@@ -198,6 +205,7 @@ describe('sharedReducer', () => { ...@@ -198,6 +205,7 @@ describe('sharedReducer', () => {
global: false, global: false,
state: LoadingState.NotStarted, state: LoadingState.NotStarted,
error: null, error: null,
description: null,
}, },
'1': { '1': {
id: '1', id: '1',
...@@ -210,6 +218,7 @@ describe('sharedReducer', () => { ...@@ -210,6 +218,7 @@ describe('sharedReducer', () => {
global: false, global: false,
state: LoadingState.NotStarted, state: LoadingState.NotStarted,
error: null, error: null,
description: null,
}, },
'2': { '2': {
id: '2', id: '2',
...@@ -222,6 +231,7 @@ describe('sharedReducer', () => { ...@@ -222,6 +231,7 @@ describe('sharedReducer', () => {
global: false, global: false,
state: LoadingState.NotStarted, state: LoadingState.NotStarted,
error: null, error: null,
description: null,
}, },
}); });
}); });
......
import React, { ChangeEvent, PureComponent } from 'react'; import React, { ChangeEvent, PureComponent } from 'react';
import { VerticalGroup } from '@grafana/ui';
import { TextBoxVariableModel } from '../types'; import { TextBoxVariableModel } from '../types';
import { VariableEditorProps } from '../editor/types'; import { VariableEditorProps } from '../editor/types';
import { VariableSectionHeader } from '../editor/VariableSectionHeader';
import { VariableTextField } from '../editor/VariableTextField';
export interface Props extends VariableEditorProps<TextBoxVariableModel> {} export interface Props extends VariableEditorProps<TextBoxVariableModel> {}
export class TextBoxVariableEditor extends PureComponent<Props> { export class TextBoxVariableEditor extends PureComponent<Props> {
...@@ -15,20 +19,18 @@ export class TextBoxVariableEditor extends PureComponent<Props> { ...@@ -15,20 +19,18 @@ export class TextBoxVariableEditor extends PureComponent<Props> {
render() { render() {
const { query } = this.props.variable; const { query } = this.props.variable;
return ( return (
<div className="gf-form-group"> <VerticalGroup spacing="xs">
<h5 className="section-heading">Text options</h5> <VariableSectionHeader name="Text Options" />
<div className="gf-form"> <VariableTextField
<span className="gf-form-label">Default value</span>
<input
type="text"
className="gf-form-input"
value={query} value={query}
name="Default value"
placeholder="default value, if any"
onChange={this.onQueryChange} onChange={this.onQueryChange}
onBlur={this.onQueryBlur} onBlur={this.onQueryBlur}
placeholder="default value, if any" labelWidth={20}
grow
/> />
</div> </VerticalGroup>
</div>
); );
} }
} }
...@@ -139,6 +139,7 @@ export interface VariableModel extends BaseVariableModel { ...@@ -139,6 +139,7 @@ export interface VariableModel extends BaseVariableModel {
index: number; index: number;
state: LoadingState; state: LoadingState;
error: any | null; error: any | null;
description: string | null;
} }
export const initialVariableModelState: VariableModel = { export const initialVariableModelState: VariableModel = {
...@@ -152,6 +153,7 @@ export const initialVariableModelState: VariableModel = { ...@@ -152,6 +153,7 @@ export const initialVariableModelState: VariableModel = {
skipUrlSync: false, skipUrlSync: false,
state: LoadingState.NotStarted, state: LoadingState.NotStarted,
error: null, error: null,
description: null,
}; };
export type VariableQueryEditorType< export type VariableQueryEditorType<
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment