Commit 0e4850f2 by Erik Sundell Committed by GitHub

UI: Segment fixes (#20947)

* Add support for primitive values/onchange

* Fix segment clickaway bug

* Fix onchange

* Use primitive in cloudwatch

* Add placeholder

* Use placeholder in cloudwatch editor

* Fix lint error

* Fix lodash import

* Use new component story format

* Add support for autofocus

* Use selectable value for onchange event

* Fix lint error
parent 26789d1e
import React from 'react';
import { storiesOf } from '@storybook/react';
import React, { useState } from 'react';
import { action } from '@storybook/addon-actions';
import { SelectableValue } from '@grafana/data';
import { Segment } from './';
import { UseState } from '../../utils/storybook/UseState';
const SegmentStories = storiesOf('UI/Segment/SegmentSync', module);
const AddButton = (
<a className="gf-form-label query-part">
......@@ -15,131 +9,125 @@ const AddButton = (
);
const toOption = (value: any) => ({ label: value, value: value });
SegmentStories.add('Array Options', () => {
const options = ['Option1', 'Option2', 'OptionWithLooongLabel', 'Option4'].map(toOption);
options[0].label = 'Option1 Label';
return (
<UseState initialState={options[0] as SelectableValue}>
{(value, updateValue) => (
<>
<div className="gf-form-inline">
<div className="gf-form">
<span className="gf-form-label width-8 query-keyword">Segment Name</span>
</div>
<Segment
value={value}
options={options}
onChange={item => {
updateValue(item);
action('Segment value changed')(item.value);
}}
/>
<Segment
Component={AddButton}
onChange={(value: SelectableValue<string>) => action('New value added')(value)}
options={options}
/>
</div>
</>
)}
</UseState>
);
});
const options = ['Option1', 'Option2', 'OptionWithLooongLabel', 'Option4'].map(toOption);
const groupedOptions = [
{ label: 'Names', options: ['Jane', 'Tom', 'Lisa'].map(toOption) },
{ label: 'Prime', options: [2, 3, 5, 7, 11, 13].map(toOption) },
];
SegmentStories.add('Grouped Array Options', () => {
const SegmentFrame = ({ options, children }: any) => (
<>
<div className="gf-form-inline">
<div className="gf-form">
<span className="gf-form-label width-8 query-keyword">Segment Name</span>
</div>
{children}
<Segment Component={AddButton} onChange={({ value }) => action('New value added')(value)} options={options} />
</div>
</>
);
export const ArrayOptions = () => {
const [value, setValue] = useState<any>(options[0]);
return (
<SegmentFrame options={options}>
<Segment
value={value}
options={options}
onChange={item => {
setValue(item);
action('Segment value changed')(item.value);
}}
/>
</SegmentFrame>
);
};
export default {
title: 'UI/Segment/SegmentSync',
component: ArrayOptions,
};
export const ArrayOptionsWithPrimitiveValue = () => {
const [value, setValue] = useState('Option1');
return (
<SegmentFrame options={options}>
<Segment
value={value}
options={options}
onChange={({ value }) => {
setValue(value);
action('Segment value changed')(value);
}}
/>
</SegmentFrame>
);
};
export const ArrayOptionsWithPlaceholder = () => {
const [value, setValue] = useState<any>(undefined);
return (
<SegmentFrame options={options}>
<Segment
value={value}
options={options}
placeholder="Enter a value"
onChange={item => {
setValue(item);
action('Segment value changed')(item.value);
}}
/>
</SegmentFrame>
);
};
export const GroupedArrayOptions = () => {
const [value, setValue] = useState<any>(groupedOptions[0].options[0]);
return (
<UseState initialState={groupedOptions[0].options[0] as SelectableValue}>
{(value, updateValue) => (
<>
<div className="gf-form-inline">
<div className="gf-form">
<span className="gf-form-label width-8 query-keyword">Segment Name</span>
</div>
<Segment
value={value}
options={groupedOptions}
onChange={item => {
updateValue(item);
action('Segment value changed')(item.value);
}}
/>
<Segment
Component={AddButton}
onChange={value => action('New value added')(value)}
options={groupedOptions}
/>
</div>
</>
)}
</UseState>
<SegmentFrame options={options}>
<Segment
value={value}
options={groupedOptions}
onChange={item => {
setValue(item);
action('Segment value changed')(item.value);
}}
/>
</SegmentFrame>
);
});
};
SegmentStories.add('With custom options allowed', () => {
const options = ['Option1', 'Option2', 'OptionWithLooongLabel', 'Option4'].map(toOption);
export const CustomOptionsAllowed = () => {
const [value, setValue] = useState(options[0]);
return (
<UseState initialState={options[0] as SelectableValue}>
{(value, updateValue) => (
<>
<div className="gf-form-inline">
<div className="gf-form">
<span className="gf-form-label width-8 query-keyword">Segment Name</span>
</div>
<Segment
allowCustomValue
value={value}
options={options}
onChange={item => {
updateValue(item);
action('Segment value changed')(item.value);
}}
/>
<Segment
allowCustomValue
Component={AddButton}
onChange={(value: SelectableValue<string>) => action('New value added')(value)}
options={options}
/>
</div>
</>
)}
</UseState>
<SegmentFrame options={options}>
<Segment
allowCustomValue
value={value}
options={options}
onChange={({ value }) => {
setValue(value);
action('Segment value changed')(value);
}}
/>
</SegmentFrame>
);
});
};
const CustomLabelComponent = ({ value: { value } }: any) => <div className="gf-form-label">custom({value})</div>;
const CustomLabelComponent = ({ value }: any) => <div className="gf-form-label">custom({value})</div>;
SegmentStories.add('Custom Label Field', () => {
export const CustomLabelField = () => {
const [value, setValue] = useState<any>(groupedOptions[0].options[0].value);
return (
<UseState initialState={groupedOptions[0].options[0] as SelectableValue}>
{(value, setValue) => (
<>
<div className="gf-form-inline">
<div className="gf-form">
<span className="gf-form-label width-8 query-keyword">Segment Name</span>
</div>
<Segment
Component={<CustomLabelComponent value={value} />}
options={groupedOptions}
onChange={item => {
setValue(item);
action('Segment value changed')(item.value);
}}
/>
<Segment
Component={AddButton}
onChange={value => action('New value added')(value)}
options={groupedOptions}
/>
</div>
</>
)}
</UseState>
<SegmentFrame options={options}>
<Segment
Component={<CustomLabelComponent value={value} />}
options={groupedOptions}
onChange={({ value }) => {
setValue(value);
action('Segment value changed')(value);
}}
/>
</SegmentFrame>
);
});
};
import React from 'react';
import { cx } from 'emotion';
import _ from 'lodash';
import { SelectableValue } from '@grafana/data';
import { SegmentSelect, useExpandableLabel, SegmentProps } from './';
export interface SegmentSyncProps<T> extends SegmentProps<T> {
value?: SelectableValue<T>;
value?: T | SelectableValue<T>;
onChange: (item: SelectableValue<T>) => void;
options: Array<SelectableValue<T>>;
}
......@@ -16,20 +17,28 @@ export function Segment<T>({
Component,
className,
allowCustomValue,
placeholder,
}: React.PropsWithChildren<SegmentSyncProps<T>>) {
const [Label, width, expanded, setExpanded] = useExpandableLabel(false);
if (!expanded) {
const label = _.isObject(value) ? value.label : value;
return (
<Label
Component={Component || <a className={cx('gf-form-label', 'query-part', className)}>{value && value.label}</a>}
Component={
Component || (
<a className={cx('gf-form-label', 'query-part', !value && placeholder && 'query-placeholder', className)}>
{label || placeholder}
</a>
)
}
/>
);
}
return (
<SegmentSelect
value={value}
value={value && !_.isObject(value) ? { value } : value}
options={options}
width={width}
onClickOutside={() => setExpanded(false)}
......
import React from 'react';
import { storiesOf } from '@storybook/react';
import React, { useState } from 'react';
import { action } from '@storybook/addon-actions';
import { SelectableValue } from '@grafana/data';
const SegmentStories = storiesOf('UI/Segment/SegmentAsync', module);
import { SegmentAsync } from './';
import { UseState } from '../../utils/storybook/UseState';
const AddButton = (
<a className="gf-form-label query-part">
......@@ -13,132 +10,116 @@ const AddButton = (
);
const toOption = (value: any) => ({ label: value, value: value });
const options = ['Option1', 'Option2', 'OptionWithLooongLabel', 'Option4'].map(toOption);
const loadOptions = (options: any): Promise<Array<SelectableValue<string>>> =>
new Promise(res => setTimeout(() => res(options), 2000));
SegmentStories.add('Array Options', () => {
const options = ['Option1', 'Option2', 'OptionWithLooongLabel', 'Option4'].map(toOption);
const SegmentFrame = ({ loadOptions, children }: any) => (
<>
<div className="gf-form-inline">
<div className="gf-form">
<span className="gf-form-label width-8 query-keyword">Segment Name</span>
</div>
{children}
<SegmentAsync
Component={AddButton}
onChange={value => action('New value added')(value)}
loadOptions={() => loadOptions(options)}
/>
</div>
</>
);
export const ArrayOptions = () => {
const [value, setValue] = useState<any>(options[0]);
return (
<UseState initialState={options[0] as SelectableValue}>
{(value, updateValue) => (
<>
<div className="gf-form-inline">
<div className="gf-form">
<span className="gf-form-label width-8 query-keyword">Segment Name</span>
</div>
<SegmentAsync
value={value}
loadOptions={() => loadOptions(options)}
onChange={item => {
updateValue(item);
action('Segment value changed')(item.value);
}}
/>
<SegmentAsync
Component={AddButton}
onChange={value => action('New value added')(value)}
loadOptions={() => loadOptions(options)}
/>
</div>
</>
)}
</UseState>
<SegmentFrame loadOptions={() => loadOptions(options)}>
<SegmentAsync
value={value}
loadOptions={() => loadOptions(options)}
onChange={item => {
setValue(item);
action('Segment value changed')(item.value);
}}
/>
</SegmentFrame>
);
});
};
const groupedOptions = [
export default {
title: 'UI/Segment/SegmentAsync',
component: ArrayOptions,
};
export const ArrayOptionsWithPrimitiveValue = () => {
const [value, setValue] = useState(options[0].value);
return (
<SegmentFrame loadOptions={() => loadOptions(options)}>
<SegmentAsync
value={value}
loadOptions={() => loadOptions(options)}
onChange={({ value }) => {
setValue(value);
action('Segment value changed')(value);
}}
/>
</SegmentFrame>
);
};
const groupedOptions: any = [
{ label: 'Names', options: ['Jane', 'Tom', 'Lisa'].map(toOption) },
{ label: 'Prime', options: [2, 3, 5, 7, 11, 13].map(toOption) },
];
SegmentStories.add('Grouped Array Options', () => {
export const GroupedArrayOptions = () => {
const [value, setValue] = useState(groupedOptions[0].options[0]);
return (
<UseState initialState={groupedOptions[0].options[0] as SelectableValue}>
{(value, updateValue) => (
<>
<div className="gf-form-inline">
<div className="gf-form">
<span className="gf-form-label width-8 query-keyword">Segment Name</span>
</div>
<SegmentAsync
value={value}
loadOptions={() => loadOptions(groupedOptions)}
onChange={item => {
updateValue(item);
action('Segment value changed')(item.value);
}}
/>
<SegmentAsync
Component={AddButton}
onChange={value => action('New value added')(value)}
loadOptions={() => loadOptions(groupedOptions)}
/>
</div>
</>
)}
</UseState>
<SegmentFrame loadOptions={() => loadOptions(groupedOptions)}>
<SegmentAsync
value={value}
loadOptions={() => loadOptions(groupedOptions)}
onChange={item => {
setValue(item);
action('Segment value changed')(item.value);
}}
/>
</SegmentFrame>
);
});
};
SegmentStories.add('With custom options allowed', () => {
const options = ['Option1', 'Option2', 'OptionWithLooongLabel', 'Option4'].map(toOption);
export const CustomOptionsAllowed = () => {
const [value, setValue] = useState(groupedOptions[0].options[0]);
return (
<UseState initialState={options[0] as SelectableValue}>
{(value, updateValue) => (
<>
<div className="gf-form-inline">
<div className="gf-form">
<span className="gf-form-label width-8 query-keyword">Segment Name</span>
</div>
<SegmentAsync
allowCustomValue
value={value}
loadOptions={() => loadOptions(options)}
onChange={item => {
updateValue(item);
action('Segment value changed')(item.value);
}}
/>
<SegmentAsync
allowCustomValue
Component={AddButton}
onChange={value => action('New value added')(value)}
loadOptions={() => loadOptions(options)}
/>
</div>
</>
)}
</UseState>
<SegmentFrame loadOptions={() => loadOptions(groupedOptions)}>
<SegmentAsync
allowCustomValue
value={value}
loadOptions={() => loadOptions(options)}
onChange={item => {
setValue(item);
action('Segment value changed')(item.value);
}}
/>
</SegmentFrame>
);
});
};
const CustomLabelComponent = ({ value }: any) => <div className="gf-form-label">custom({value})</div>;
const CustomLabelComponent = ({ value: { value } }: any) => <div className="gf-form-label">custom({value})</div>;
SegmentStories.add('Custom Label Field', () => {
export const CustomLabel = () => {
const [value, setValue] = useState(groupedOptions[0].options[0].value);
return (
<UseState initialState={groupedOptions[0].options[0] as SelectableValue}>
{(value, updateValue) => (
<>
<div className="gf-form-inline">
<div className="gf-form">
<span className="gf-form-label width-8 query-keyword">Segment Name</span>
</div>
<SegmentAsync
Component={<CustomLabelComponent value={value} />}
loadOptions={() => loadOptions(groupedOptions)}
onChange={item => {
updateValue(item);
action('Segment value changed')(item.value);
}}
/>
<SegmentAsync
Component={AddButton}
onChange={value => action('New value added')(value)}
loadOptions={() => loadOptions(groupedOptions)}
/>
</div>
</>
)}
</UseState>
<SegmentFrame loadOptions={() => loadOptions(groupedOptions)}>
<SegmentAsync
Component={<CustomLabelComponent value={value} />}
loadOptions={() => loadOptions(groupedOptions)}
onChange={({ value }) => {
setValue(value);
action('Segment value changed')(value);
}}
/>
</SegmentFrame>
);
});
};
import React, { useState } from 'react';
import { cx } from 'emotion';
import _ from 'lodash';
import { SegmentSelect } from './SegmentSelect';
import { SelectableValue } from '@grafana/data';
import { useExpandableLabel, SegmentProps } from '.';
export interface SegmentAsyncProps<T> extends SegmentProps<T> {
value?: SelectableValue<T>;
value?: T | SelectableValue<T>;
loadOptions: (query?: string) => Promise<Array<SelectableValue<T>>>;
onChange: (item: SelectableValue<T>) => void;
}
......@@ -17,12 +18,14 @@ export function SegmentAsync<T>({
Component,
className,
allowCustomValue,
placeholder,
}: React.PropsWithChildren<SegmentAsyncProps<T>>) {
const [selectPlaceholder, setSelectPlaceholder] = useState<string>('');
const [loadedOptions, setLoadedOptions] = useState<Array<SelectableValue<T>>>([]);
const [Label, width, expanded, setExpanded] = useExpandableLabel(false);
if (!expanded) {
const label = _.isObject(value) ? value.label : value;
return (
<Label
onClick={async () => {
......@@ -31,14 +34,20 @@ export function SegmentAsync<T>({
setLoadedOptions(opts);
setSelectPlaceholder(opts.length ? '' : 'No options found');
}}
Component={Component || <a className={cx('gf-form-label', 'query-part', className)}>{value && value.label}</a>}
Component={
Component || (
<a className={cx('gf-form-label', 'query-part', !value && placeholder && 'query-placeholder', className)}>
{label || placeholder}
</a>
)
}
/>
);
}
return (
<SegmentSelect
value={value}
value={value && !_.isObject(value) ? { value } : value}
options={loadedOptions}
width={width}
noOptionsMessage={selectPlaceholder}
......
import React from 'react';
import { storiesOf } from '@storybook/react';
import React, { useState } from 'react';
import { action } from '@storybook/addon-actions';
const SegmentStories = storiesOf('UI/Segment/SegmentInput', module);
import { SegmentInput } from '.';
import { UseState } from '../../utils/storybook/UseState';
SegmentStories.add('Segment Input', () => {
const SegmentFrame = ({ children }: any) => (
<>
<div className="gf-form-inline">
<div className="gf-form">
<span className="gf-form-label width-8 query-keyword">Segment Name</span>
</div>
{children}
</div>
</>
);
export const BasicInput = () => {
const [value, setValue] = useState('some text');
return (
<SegmentFrame>
<SegmentInput
value={value}
onChange={text => {
setValue(text as string);
action('Segment value changed')(text);
}}
/>
</SegmentFrame>
);
};
export default {
title: 'UI/Segment/SegmentInput',
component: BasicInput,
};
export const BasicInputWithPlaceholder = () => {
const [value, setValue] = useState('');
return (
<SegmentFrame>
<SegmentInput
placeholder="add text"
value={value}
onChange={text => {
setValue(text as string);
action('Segment value changed')(text);
}}
/>
</SegmentFrame>
);
};
const InputComponent = ({ initialValue }: any) => {
const [value, setValue] = useState(initialValue);
return (
<SegmentInput
placeholder="add text"
autofocus
value={value}
onChange={text => {
setValue(text as string);
action('Segment value changed')(text);
}}
/>
);
};
export const InputWithAutoFocus = () => {
const [inputComponents, setInputComponents] = useState<any>([]);
return (
<UseState initialState={'some text'}>
{(value, updateValue) => (
<>
<div className="gf-form-inline">
<div className="gf-form">
<span className="gf-form-label width-8 query-keyword">Segment Name</span>
</div>
<SegmentInput
value={value}
onChange={text => {
updateValue(text as string);
action('Segment value changed')(text);
}}
/>
</div>
</>
)}
</UseState>
<SegmentFrame>
{inputComponents.map((InputComponent: any) => (
<InputComponent intitialValue="test"></InputComponent>
))}
<a
className="gf-form-label query-part"
onClick={() => {
setInputComponents([...inputComponents, InputComponent]);
}}
>
<i className="fa fa-plus" />
</a>
</SegmentFrame>
);
});
};
......@@ -7,6 +7,7 @@ import { useExpandableLabel, SegmentProps } from '.';
export interface SegmentInputProps<T> extends SegmentProps<T> {
value: string | number;
onChange: (text: string | number) => void;
autofocus?: boolean;
}
const FONT_SIZE = 14;
......@@ -16,16 +17,30 @@ export function SegmentInput<T>({
onChange,
Component,
className,
placeholder,
autofocus = false,
}: React.PropsWithChildren<SegmentInputProps<T>>) {
const ref = useRef(null);
const ref = useRef<HTMLInputElement>(null);
const [value, setValue] = useState<number | string>(initialValue);
const [inputWidth, setInputWidth] = useState<number>(measureText(initialValue.toString(), FONT_SIZE).width);
const [Label, , expanded, setExpanded] = useExpandableLabel(false);
useClickAway(ref, () => setExpanded(false));
const [inputWidth, setInputWidth] = useState<number>(measureText((initialValue || '').toString(), FONT_SIZE).width);
const [Label, , expanded, setExpanded] = useExpandableLabel(autofocus);
useClickAway(ref, () => {
setExpanded(false);
onChange(value);
});
if (!expanded) {
return (
<Label Component={Component || <a className={cx('gf-form-label', 'query-part', className)}>{initialValue}</a>} />
<Label
Component={
Component || (
<a className={cx('gf-form-label', 'query-part', !value && placeholder && 'query-placeholder', className)}>
{initialValue || placeholder}
</a>
)
}
/>
);
}
......
......@@ -4,4 +4,5 @@ export interface SegmentProps<T> {
Component?: ReactElement;
className?: string;
allowCustomValue?: boolean;
placeholder?: string;
}
......@@ -34,15 +34,13 @@ export const Dimensions: FunctionComponent<Props> = ({ dimensions, loadValues, l
return options.filter(({ value }) => !Object.keys(data).includes(value));
};
const toOption = (value: any) => ({ label: value, value });
return (
<>
{Object.entries(data).map(([key, value], index) => (
<Fragment key={index}>
<SegmentAsync
allowCustomValue
value={toOption(key)}
value={key}
loadOptions={() => loadKeys().then(keys => [removeOption, ...excludeUsedKeys(keys)])}
onChange={({ value: newKey }) => {
const { [key]: value, ...newDimensions } = data;
......@@ -56,7 +54,8 @@ export const Dimensions: FunctionComponent<Props> = ({ dimensions, loadValues, l
<label className="gf-form-label query-segment-operator">=</label>
<SegmentAsync
allowCustomValue
value={toOption(value || 'select dimension value')}
value={value}
placeholder="select dimension value"
loadOptions={() => loadValues(key)}
onChange={({ value: newValue }) => setData({ ...data, [key]: newValue })}
/>
......
......@@ -112,7 +112,8 @@ export class QueryEditor extends PureComponent<Props, State> {
<>
<QueryInlineField label="Region">
<Segment
value={this.toOption(query.region || 'Select region')}
value={query.region}
placeholder="Select region"
options={regions}
allowCustomValue
onChange={({ value: region }) => this.onChange({ ...query, region })}
......@@ -123,7 +124,8 @@ export class QueryEditor extends PureComponent<Props, State> {
<>
<QueryInlineField label="Namespace">
<Segment
value={this.toOption(query.namespace || 'Select namespace')}
value={query.namespace}
placeholder="Select namespace"
allowCustomValue
options={namespaces}
onChange={({ value: namespace }) => this.onChange({ ...query, namespace })}
......@@ -132,7 +134,8 @@ export class QueryEditor extends PureComponent<Props, State> {
<QueryInlineField label="Metric Name">
<SegmentAsync
value={this.toOption(query.metricName || 'Select metric name')}
value={query.metricName}
placeholder="Select metric name"
allowCustomValue
loadOptions={this.loadMetricNames}
onChange={({ value: metricName }) => this.onChange({ ...query, metricName })}
......
......@@ -12,7 +12,6 @@ export interface Props {
const removeText = '-- remove stat --';
const removeOption: SelectableValue<string> = { label: removeText, value: removeText };
const toOption = (value: any) => ({ label: value, value });
export const Stats: FunctionComponent<Props> = ({ stats, values, onChange, variableOptionGroup }) => (
<>
......@@ -21,7 +20,7 @@ export const Stats: FunctionComponent<Props> = ({ stats, values, onChange, varia
<Segment
allowCustomValue
key={value + index}
value={toOption(value)}
value={value}
options={[removeOption, ...stats, variableOptionGroup]}
onChange={({ value }) =>
onChange(
......
......@@ -7,6 +7,10 @@
color: $orange;
}
.query-placeholder {
color: $gray-2;
}
.query-editor-rows {
margin: 20px 0;
}
......
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