mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Select: Allow custom value for selects (#19775)
* WIP: simple poc of allow custom value for selects * Add support for custom value in segment * Update snapshots
This commit is contained in:
@@ -80,6 +80,38 @@ SegmentStories.add('Grouped Array Options', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
SegmentStories.add('With custom options allowed', () => {
|
||||||
|
const options = ['Option1', 'Option2', 'OptionWithLooongLabel', 'Option4'].map(toOption);
|
||||||
|
return (
|
||||||
|
<UseState initialState={options[0].value}>
|
||||||
|
{(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={(value: SelectableValue<string>) => {
|
||||||
|
updateValue(value);
|
||||||
|
action('Segment value changed')(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Segment
|
||||||
|
allowCustomValue
|
||||||
|
Component={AddButton}
|
||||||
|
onChange={(value: SelectableValue<string>) => action('New value added')(value)}
|
||||||
|
options={options}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</UseState>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const CustomLabelComponent = ({ 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', () => {
|
SegmentStories.add('Custom Label Field', () => {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export function Segment<T>({
|
|||||||
onChange,
|
onChange,
|
||||||
Component,
|
Component,
|
||||||
className,
|
className,
|
||||||
|
allowCustomValue,
|
||||||
}: React.PropsWithChildren<SegmentSyncProps<T>>) {
|
}: React.PropsWithChildren<SegmentSyncProps<T>>) {
|
||||||
const [Label, width, expanded, setExpanded] = useExpandableLabel(false);
|
const [Label, width, expanded, setExpanded] = useExpandableLabel(false);
|
||||||
|
|
||||||
@@ -25,6 +26,7 @@ export function Segment<T>({
|
|||||||
width={width}
|
width={width}
|
||||||
options={options}
|
options={options}
|
||||||
onClickOutside={() => setExpanded(false)}
|
onClickOutside={() => setExpanded(false)}
|
||||||
|
allowCustomValue={allowCustomValue}
|
||||||
onChange={value => {
|
onChange={value => {
|
||||||
setExpanded(false);
|
setExpanded(false);
|
||||||
onChange(value);
|
onChange(value);
|
||||||
|
|||||||
@@ -81,8 +81,39 @@ SegmentStories.add('Grouped Array Options', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const CustomLabelComponent = ({ value }: any) => <div className="gf-form-label">custom({value})</div>;
|
SegmentStories.add('With custom options allowed', () => {
|
||||||
|
const options = ['Option1', 'Option2', 'OptionWithLooongLabel', 'Option4'].map(toOption);
|
||||||
|
return (
|
||||||
|
<UseState initialState={options[0].value}>
|
||||||
|
{(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={value => {
|
||||||
|
updateValue(value);
|
||||||
|
action('Segment value changed')(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<SegmentAsync
|
||||||
|
allowCustomValue
|
||||||
|
Component={AddButton}
|
||||||
|
onChange={value => action('New value added')(value)}
|
||||||
|
loadOptions={() => loadOptions(options)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</UseState>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const CustomLabelComponent = ({ value }: any) => <div className="gf-form-label">custom({value})</div>;
|
||||||
SegmentStories.add('Custom Label Field', () => {
|
SegmentStories.add('Custom Label Field', () => {
|
||||||
return (
|
return (
|
||||||
<UseState initialState={groupedOptions[0].options[0].value}>
|
<UseState initialState={groupedOptions[0].options[0].value}>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export function SegmentAsync<T>({
|
|||||||
loadOptions,
|
loadOptions,
|
||||||
Component,
|
Component,
|
||||||
className,
|
className,
|
||||||
|
allowCustomValue,
|
||||||
}: React.PropsWithChildren<SegmentAsyncProps<T>>) {
|
}: React.PropsWithChildren<SegmentAsyncProps<T>>) {
|
||||||
const [selectPlaceholder, setSelectPlaceholder] = useState<string>('');
|
const [selectPlaceholder, setSelectPlaceholder] = useState<string>('');
|
||||||
const [loadedOptions, setLoadedOptions] = useState<Array<SelectableValue<T>>>([]);
|
const [loadedOptions, setLoadedOptions] = useState<Array<SelectableValue<T>>>([]);
|
||||||
@@ -38,6 +39,7 @@ export function SegmentAsync<T>({
|
|||||||
width={width}
|
width={width}
|
||||||
options={loadedOptions}
|
options={loadedOptions}
|
||||||
noOptionsMessage={selectPlaceholder}
|
noOptionsMessage={selectPlaceholder}
|
||||||
|
allowCustomValue={allowCustomValue}
|
||||||
onClickOutside={() => {
|
onClickOutside={() => {
|
||||||
setSelectPlaceholder('');
|
setSelectPlaceholder('');
|
||||||
setLoadedOptions([]);
|
setLoadedOptions([]);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ export interface Props<T> {
|
|||||||
onClickOutside: () => void;
|
onClickOutside: () => void;
|
||||||
width: number;
|
width: number;
|
||||||
noOptionsMessage?: string;
|
noOptionsMessage?: string;
|
||||||
|
allowCustomValue?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SegmentSelect<T>({
|
export function SegmentSelect<T>({
|
||||||
@@ -18,6 +19,7 @@ export function SegmentSelect<T>({
|
|||||||
onClickOutside,
|
onClickOutside,
|
||||||
width,
|
width,
|
||||||
noOptionsMessage = '',
|
noOptionsMessage = '',
|
||||||
|
allowCustomValue = false,
|
||||||
}: React.PropsWithChildren<Props<T>>) {
|
}: React.PropsWithChildren<Props<T>>) {
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
|
|
||||||
@@ -39,6 +41,7 @@ export function SegmentSelect<T>({
|
|||||||
isOpen={true}
|
isOpen={true}
|
||||||
onChange={({ value }) => onChange(value!)}
|
onChange={({ value }) => onChange(value!)}
|
||||||
options={options}
|
options={options}
|
||||||
|
allowCustomValue={allowCustomValue}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ export interface SegmentProps<T> {
|
|||||||
value?: T;
|
value?: T;
|
||||||
Component?: ReactElement;
|
Component?: ReactElement;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
allowCustomValue?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,3 +36,30 @@ SelectStories.add('default', () => {
|
|||||||
</UseState>
|
</UseState>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
SelectStories.add('With allowCustomValue', () => {
|
||||||
|
const intialState: SelectableValue<string> = { label: 'A label', value: 'A value' };
|
||||||
|
const value = object<SelectableValue<string>>('Selected Value:', intialState);
|
||||||
|
const options = object<Array<SelectableValue<string>>>('Options:', [
|
||||||
|
intialState,
|
||||||
|
{ label: 'Another label', value: 'Another value' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UseState initialState={value}>
|
||||||
|
{(value, updateValue) => {
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
value={value}
|
||||||
|
options={options}
|
||||||
|
allowCustomValue={true}
|
||||||
|
onChange={value => {
|
||||||
|
action('onChanged fired')(value);
|
||||||
|
updateValue(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</UseState>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import React, { PureComponent } from 'react';
|
|||||||
|
|
||||||
// Ignoring because I couldn't get @types/react-select work wih Torkel's fork
|
// Ignoring because I couldn't get @types/react-select work wih Torkel's fork
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { default as ReactSelect } from '@torkelo/react-select';
|
import { default as ReactSelect, Creatable } from '@torkelo/react-select';
|
||||||
|
// @ts-ignore
|
||||||
|
import { Creatable } from '@torkelo/react-select/lib/creatable';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { default as ReactAsyncSelect } from '@torkelo/react-select/lib/Async';
|
import { default as ReactAsyncSelect } from '@torkelo/react-select/lib/Async';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -48,6 +50,7 @@ export interface CommonProps<T> {
|
|||||||
onOpenMenu?: () => void;
|
onOpenMenu?: () => void;
|
||||||
onCloseMenu?: () => void;
|
onCloseMenu?: () => void;
|
||||||
tabSelectsValue?: boolean;
|
tabSelectsValue?: boolean;
|
||||||
|
allowCustomValue: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SelectProps<T> extends CommonProps<T> {
|
export interface SelectProps<T> extends CommonProps<T> {
|
||||||
@@ -83,6 +86,7 @@ export class Select<T> extends PureComponent<SelectProps<T>> {
|
|||||||
backspaceRemovesValue: true,
|
backspaceRemovesValue: true,
|
||||||
maxMenuHeight: 300,
|
maxMenuHeight: 300,
|
||||||
tabSelectsValue: true,
|
tabSelectsValue: true,
|
||||||
|
allowCustomValue: false,
|
||||||
components: {
|
components: {
|
||||||
Option: SelectOption,
|
Option: SelectOption,
|
||||||
SingleValue,
|
SingleValue,
|
||||||
@@ -120,6 +124,7 @@ export class Select<T> extends PureComponent<SelectProps<T>> {
|
|||||||
tabSelectsValue,
|
tabSelectsValue,
|
||||||
onCloseMenu,
|
onCloseMenu,
|
||||||
onOpenMenu,
|
onOpenMenu,
|
||||||
|
allowCustomValue,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
let widthClass = '';
|
let widthClass = '';
|
||||||
@@ -127,6 +132,14 @@ export class Select<T> extends PureComponent<SelectProps<T>> {
|
|||||||
widthClass = 'width-' + width;
|
widthClass = 'width-' + width;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let SelectComponent: ReactSelect | Creatable = ReactSelect;
|
||||||
|
const creatableOptions: any = {};
|
||||||
|
|
||||||
|
if (allowCustomValue) {
|
||||||
|
SelectComponent = Creatable;
|
||||||
|
creatableOptions.formatCreateLabel = (input: string) => input;
|
||||||
|
}
|
||||||
|
|
||||||
const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className);
|
const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className);
|
||||||
const selectComponents = { ...Select.defaultProps.components, ...components };
|
const selectComponents = { ...Select.defaultProps.components, ...components };
|
||||||
|
|
||||||
@@ -134,7 +147,7 @@ export class Select<T> extends PureComponent<SelectProps<T>> {
|
|||||||
<WrapInTooltip onCloseMenu={onCloseMenu} onOpenMenu={onOpenMenu} tooltipContent={tooltipContent} isOpen={isOpen}>
|
<WrapInTooltip onCloseMenu={onCloseMenu} onOpenMenu={onOpenMenu} tooltipContent={tooltipContent} isOpen={isOpen}>
|
||||||
{(onOpenMenuInternal, onCloseMenuInternal) => {
|
{(onOpenMenuInternal, onCloseMenuInternal) => {
|
||||||
return (
|
return (
|
||||||
<ReactSelect
|
<SelectComponent
|
||||||
classNamePrefix="gf-form-select-box"
|
classNamePrefix="gf-form-select-box"
|
||||||
className={selectClassNames}
|
className={selectClassNames}
|
||||||
components={selectComponents}
|
components={selectComponents}
|
||||||
@@ -162,6 +175,7 @@ export class Select<T> extends PureComponent<SelectProps<T>> {
|
|||||||
onMenuOpen={onOpenMenuInternal}
|
onMenuOpen={onOpenMenuInternal}
|
||||||
onMenuClose={onCloseMenuInternal}
|
onMenuClose={onCloseMenuInternal}
|
||||||
tabSelectsValue={tabSelectsValue}
|
tabSelectsValue={tabSelectsValue}
|
||||||
|
{...creatableOptions}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -81,6 +81,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned off should not ren
|
|||||||
className="gf-form"
|
className="gf-form"
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
|
allowCustomValue={false}
|
||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
backspaceRemovesValue={true}
|
backspaceRemovesValue={true}
|
||||||
className="gf-form-select-box__control--menu-right"
|
className="gf-form-select-box__control--menu-right"
|
||||||
@@ -166,6 +167,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p
|
|||||||
className="gf-form"
|
className="gf-form"
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
|
allowCustomValue={false}
|
||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
backspaceRemovesValue={true}
|
backspaceRemovesValue={true}
|
||||||
className="gf-form-select-box__control--menu-right"
|
className="gf-form-select-box__control--menu-right"
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ exports[`Render should disable log analytics credentials form 1`] = `
|
|||||||
className="width-25"
|
className="width-25"
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
|
allowCustomValue={false}
|
||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
backspaceRemovesValue={true}
|
backspaceRemovesValue={true}
|
||||||
className=""
|
className=""
|
||||||
@@ -138,6 +139,7 @@ exports[`Render should enable azure log analytics load workspaces button 1`] = `
|
|||||||
className="width-25"
|
className="width-25"
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
|
allowCustomValue={false}
|
||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
backspaceRemovesValue={true}
|
backspaceRemovesValue={true}
|
||||||
className=""
|
className=""
|
||||||
@@ -233,6 +235,7 @@ exports[`Render should render component 1`] = `
|
|||||||
className="width-25"
|
className="width-25"
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
|
allowCustomValue={false}
|
||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
backspaceRemovesValue={true}
|
backspaceRemovesValue={true}
|
||||||
className=""
|
className=""
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ exports[`Render should disable azure monitor secret input 1`] = `
|
|||||||
Azure Cloud
|
Azure Cloud
|
||||||
</Component>
|
</Component>
|
||||||
<Select
|
<Select
|
||||||
|
allowCustomValue={false}
|
||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
backspaceRemovesValue={true}
|
backspaceRemovesValue={true}
|
||||||
className="width-15"
|
className="width-15"
|
||||||
@@ -163,6 +164,7 @@ exports[`Render should disable azure monitor secret input 1`] = `
|
|||||||
className="width-25"
|
className="width-25"
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
|
allowCustomValue={false}
|
||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
backspaceRemovesValue={true}
|
backspaceRemovesValue={true}
|
||||||
className=""
|
className=""
|
||||||
@@ -233,6 +235,7 @@ exports[`Render should enable azure monitor load subscriptions button 1`] = `
|
|||||||
Azure Cloud
|
Azure Cloud
|
||||||
</Component>
|
</Component>
|
||||||
<Select
|
<Select
|
||||||
|
allowCustomValue={false}
|
||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
backspaceRemovesValue={true}
|
backspaceRemovesValue={true}
|
||||||
className="width-15"
|
className="width-15"
|
||||||
@@ -368,6 +371,7 @@ exports[`Render should enable azure monitor load subscriptions button 1`] = `
|
|||||||
className="width-25"
|
className="width-25"
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
|
allowCustomValue={false}
|
||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
backspaceRemovesValue={true}
|
backspaceRemovesValue={true}
|
||||||
className=""
|
className=""
|
||||||
@@ -438,6 +442,7 @@ exports[`Render should render component 1`] = `
|
|||||||
Azure Cloud
|
Azure Cloud
|
||||||
</Component>
|
</Component>
|
||||||
<Select
|
<Select
|
||||||
|
allowCustomValue={false}
|
||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
backspaceRemovesValue={true}
|
backspaceRemovesValue={true}
|
||||||
className="width-15"
|
className="width-15"
|
||||||
@@ -573,6 +578,7 @@ exports[`Render should render component 1`] = `
|
|||||||
className="width-25"
|
className="width-25"
|
||||||
>
|
>
|
||||||
<Select
|
<Select
|
||||||
|
allowCustomValue={false}
|
||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
backspaceRemovesValue={true}
|
backspaceRemovesValue={true}
|
||||||
className=""
|
className=""
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ exports[`Render PromQueryEditor with basic options should render 1`] = `
|
|||||||
Resolution
|
Resolution
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
|
allowCustomValue={false}
|
||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
backspaceRemovesValue={true}
|
backspaceRemovesValue={true}
|
||||||
className=""
|
className=""
|
||||||
@@ -134,6 +135,7 @@ exports[`Render PromQueryEditor with basic options should render 1`] = `
|
|||||||
Format
|
Format
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
|
allowCustomValue={false}
|
||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
backspaceRemovesValue={true}
|
backspaceRemovesValue={true}
|
||||||
className=""
|
className=""
|
||||||
|
|||||||
Reference in New Issue
Block a user