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