mirror of
https://github.com/grafana/grafana.git
synced 2025-08-03 02:42:22 +08: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>;
|
||||
|
||||
SegmentStories.add('Custom Label Field', () => {
|
||||
|
@ -13,6 +13,7 @@ export function Segment<T>({
|
||||
onChange,
|
||||
Component,
|
||||
className,
|
||||
allowCustomValue,
|
||||
}: React.PropsWithChildren<SegmentSyncProps<T>>) {
|
||||
const [Label, width, expanded, setExpanded] = useExpandableLabel(false);
|
||||
|
||||
@ -25,6 +26,7 @@ export function Segment<T>({
|
||||
width={width}
|
||||
options={options}
|
||||
onClickOutside={() => setExpanded(false)}
|
||||
allowCustomValue={allowCustomValue}
|
||||
onChange={value => {
|
||||
setExpanded(false);
|
||||
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', () => {
|
||||
return (
|
||||
<UseState initialState={groupedOptions[0].options[0].value}>
|
||||
|
@ -14,6 +14,7 @@ export function SegmentAsync<T>({
|
||||
loadOptions,
|
||||
Component,
|
||||
className,
|
||||
allowCustomValue,
|
||||
}: React.PropsWithChildren<SegmentAsyncProps<T>>) {
|
||||
const [selectPlaceholder, setSelectPlaceholder] = useState<string>('');
|
||||
const [loadedOptions, setLoadedOptions] = useState<Array<SelectableValue<T>>>([]);
|
||||
@ -38,6 +39,7 @@ export function SegmentAsync<T>({
|
||||
width={width}
|
||||
options={loadedOptions}
|
||||
noOptionsMessage={selectPlaceholder}
|
||||
allowCustomValue={allowCustomValue}
|
||||
onClickOutside={() => {
|
||||
setSelectPlaceholder('');
|
||||
setLoadedOptions([]);
|
||||
|
@ -10,6 +10,7 @@ export interface Props<T> {
|
||||
onClickOutside: () => void;
|
||||
width: number;
|
||||
noOptionsMessage?: string;
|
||||
allowCustomValue?: boolean;
|
||||
}
|
||||
|
||||
export function SegmentSelect<T>({
|
||||
@ -18,6 +19,7 @@ export function SegmentSelect<T>({
|
||||
onClickOutside,
|
||||
width,
|
||||
noOptionsMessage = '',
|
||||
allowCustomValue = false,
|
||||
}: React.PropsWithChildren<Props<T>>) {
|
||||
const ref = useRef(null);
|
||||
|
||||
@ -39,6 +41,7 @@ export function SegmentSelect<T>({
|
||||
isOpen={true}
|
||||
onChange={({ value }) => onChange(value!)}
|
||||
options={options}
|
||||
allowCustomValue={allowCustomValue}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
@ -5,4 +5,5 @@ export interface SegmentProps<T> {
|
||||
value?: T;
|
||||
Component?: ReactElement;
|
||||
className?: string;
|
||||
allowCustomValue?: boolean;
|
||||
}
|
||||
|
@ -36,3 +36,30 @@ SelectStories.add('default', () => {
|
||||
</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
|
||||
// @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
|
||||
import { default as ReactAsyncSelect } from '@torkelo/react-select/lib/Async';
|
||||
// @ts-ignore
|
||||
@ -48,6 +50,7 @@ export interface CommonProps<T> {
|
||||
onOpenMenu?: () => void;
|
||||
onCloseMenu?: () => void;
|
||||
tabSelectsValue?: boolean;
|
||||
allowCustomValue: boolean;
|
||||
}
|
||||
|
||||
export interface SelectProps<T> extends CommonProps<T> {
|
||||
@ -83,6 +86,7 @@ export class Select<T> extends PureComponent<SelectProps<T>> {
|
||||
backspaceRemovesValue: true,
|
||||
maxMenuHeight: 300,
|
||||
tabSelectsValue: true,
|
||||
allowCustomValue: false,
|
||||
components: {
|
||||
Option: SelectOption,
|
||||
SingleValue,
|
||||
@ -120,6 +124,7 @@ export class Select<T> extends PureComponent<SelectProps<T>> {
|
||||
tabSelectsValue,
|
||||
onCloseMenu,
|
||||
onOpenMenu,
|
||||
allowCustomValue,
|
||||
} = this.props;
|
||||
|
||||
let widthClass = '';
|
||||
@ -127,6 +132,14 @@ export class Select<T> extends PureComponent<SelectProps<T>> {
|
||||
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 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}>
|
||||
{(onOpenMenuInternal, onCloseMenuInternal) => {
|
||||
return (
|
||||
<ReactSelect
|
||||
<SelectComponent
|
||||
classNamePrefix="gf-form-select-box"
|
||||
className={selectClassNames}
|
||||
components={selectComponents}
|
||||
@ -162,6 +175,7 @@ export class Select<T> extends PureComponent<SelectProps<T>> {
|
||||
onMenuOpen={onOpenMenuInternal}
|
||||
onMenuClose={onCloseMenuInternal}
|
||||
tabSelectsValue={tabSelectsValue}
|
||||
{...creatableOptions}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
@ -81,6 +81,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned off should not ren
|
||||
className="gf-form"
|
||||
>
|
||||
<Select
|
||||
allowCustomValue={false}
|
||||
autoFocus={false}
|
||||
backspaceRemovesValue={true}
|
||||
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"
|
||||
>
|
||||
<Select
|
||||
allowCustomValue={false}
|
||||
autoFocus={false}
|
||||
backspaceRemovesValue={true}
|
||||
className="gf-form-select-box__control--menu-right"
|
||||
|
@ -43,6 +43,7 @@ exports[`Render should disable log analytics credentials form 1`] = `
|
||||
className="width-25"
|
||||
>
|
||||
<Select
|
||||
allowCustomValue={false}
|
||||
autoFocus={false}
|
||||
backspaceRemovesValue={true}
|
||||
className=""
|
||||
@ -138,6 +139,7 @@ exports[`Render should enable azure log analytics load workspaces button 1`] = `
|
||||
className="width-25"
|
||||
>
|
||||
<Select
|
||||
allowCustomValue={false}
|
||||
autoFocus={false}
|
||||
backspaceRemovesValue={true}
|
||||
className=""
|
||||
@ -233,6 +235,7 @@ exports[`Render should render component 1`] = `
|
||||
className="width-25"
|
||||
>
|
||||
<Select
|
||||
allowCustomValue={false}
|
||||
autoFocus={false}
|
||||
backspaceRemovesValue={true}
|
||||
className=""
|
||||
|
@ -18,6 +18,7 @@ exports[`Render should disable azure monitor secret input 1`] = `
|
||||
Azure Cloud
|
||||
</Component>
|
||||
<Select
|
||||
allowCustomValue={false}
|
||||
autoFocus={false}
|
||||
backspaceRemovesValue={true}
|
||||
className="width-15"
|
||||
@ -163,6 +164,7 @@ exports[`Render should disable azure monitor secret input 1`] = `
|
||||
className="width-25"
|
||||
>
|
||||
<Select
|
||||
allowCustomValue={false}
|
||||
autoFocus={false}
|
||||
backspaceRemovesValue={true}
|
||||
className=""
|
||||
@ -233,6 +235,7 @@ exports[`Render should enable azure monitor load subscriptions button 1`] = `
|
||||
Azure Cloud
|
||||
</Component>
|
||||
<Select
|
||||
allowCustomValue={false}
|
||||
autoFocus={false}
|
||||
backspaceRemovesValue={true}
|
||||
className="width-15"
|
||||
@ -368,6 +371,7 @@ exports[`Render should enable azure monitor load subscriptions button 1`] = `
|
||||
className="width-25"
|
||||
>
|
||||
<Select
|
||||
allowCustomValue={false}
|
||||
autoFocus={false}
|
||||
backspaceRemovesValue={true}
|
||||
className=""
|
||||
@ -438,6 +442,7 @@ exports[`Render should render component 1`] = `
|
||||
Azure Cloud
|
||||
</Component>
|
||||
<Select
|
||||
allowCustomValue={false}
|
||||
autoFocus={false}
|
||||
backspaceRemovesValue={true}
|
||||
className="width-15"
|
||||
@ -573,6 +578,7 @@ exports[`Render should render component 1`] = `
|
||||
className="width-25"
|
||||
>
|
||||
<Select
|
||||
allowCustomValue={false}
|
||||
autoFocus={false}
|
||||
backspaceRemovesValue={true}
|
||||
className=""
|
||||
|
@ -68,6 +68,7 @@ exports[`Render PromQueryEditor with basic options should render 1`] = `
|
||||
Resolution
|
||||
</div>
|
||||
<Select
|
||||
allowCustomValue={false}
|
||||
autoFocus={false}
|
||||
backspaceRemovesValue={true}
|
||||
className=""
|
||||
@ -134,6 +135,7 @@ exports[`Render PromQueryEditor with basic options should render 1`] = `
|
||||
Format
|
||||
</div>
|
||||
<Select
|
||||
allowCustomValue={false}
|
||||
autoFocus={false}
|
||||
backspaceRemovesValue={true}
|
||||
className=""
|
||||
|
Reference in New Issue
Block a user