mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Add component: Cascader (#21410)
* Rename old cascader * Change name of old cascader * Add basic cascader without search * Add basic cascader without search * Flatten options to make it searchable * Add regex search and make backspace work * Add barebone search without styles * Add SearchResult list * Add search navigation * Rewrite of cascader and add some things to SelectBase * Make SelectBase controlllable * Cleanup * Add initial value functionality * Add onblur to hand caret direction * New storyboom format for ButtonCascader * Add knobs to story * Add story and docs for UnitPicker * Make UnitPicker use Cascader * Fix backspace issue and empty value * Fix backspace issue for real * Remove unused code * Fix focus issue * Change children to items and remove ButtonCascaderProps * Remove local CascaderOption * Fix failed test * Revert UnitPicker changes and change format for ButtonCascader * Fix failing tests
This commit is contained in:
parent
20aac7f04b
commit
aa0982da56
@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { withKnobs, text, boolean, object } from '@storybook/addon-knobs';
|
||||||
|
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||||
|
import { ButtonCascader } from './ButtonCascader';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'UI/ButtonCascader',
|
||||||
|
component: ButtonCascader,
|
||||||
|
decorators: [withKnobs, withCenteredStory],
|
||||||
|
};
|
||||||
|
|
||||||
|
const getKnobs = () => {
|
||||||
|
return {
|
||||||
|
disabled: boolean('Disabled', false),
|
||||||
|
text: text('Button Text', 'Click me!'),
|
||||||
|
options: object('Options', [
|
||||||
|
{
|
||||||
|
label: 'A',
|
||||||
|
value: 'A',
|
||||||
|
children: [
|
||||||
|
{ label: 'B', value: 'B' },
|
||||||
|
{ label: 'C', value: 'C' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ label: 'D', value: 'D' },
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const simple = () => {
|
||||||
|
const { disabled, text, options } = getKnobs();
|
||||||
|
return <ButtonCascader disabled={disabled} options={options} value={['A']} expandIcon={null} buttonText={text} />;
|
||||||
|
};
|
@ -0,0 +1,27 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Button } from '../Forms/Button';
|
||||||
|
import { Icon } from '../Icon/Icon';
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
import RCCascader from 'rc-cascader';
|
||||||
|
import { CascaderOption } from '../Cascader/Cascader';
|
||||||
|
|
||||||
|
export interface ButtonCascaderProps {
|
||||||
|
options: CascaderOption[];
|
||||||
|
buttonText: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
expandIcon?: React.ReactNode;
|
||||||
|
value?: string[];
|
||||||
|
|
||||||
|
loadData?: (selectedOptions: CascaderOption[]) => void;
|
||||||
|
onChange?: (value: string[], selectedOptions: CascaderOption[]) => void;
|
||||||
|
onPopupVisibleChange?: (visible: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ButtonCascader: React.FC<ButtonCascaderProps> = props => (
|
||||||
|
<RCCascader {...props} fieldNames={{ label: 'label', value: 'value', children: 'items' }}>
|
||||||
|
<Button variant="secondary" disabled={props.disabled}>
|
||||||
|
{props.buttonText} <Icon name="caret-down" />
|
||||||
|
</Button>
|
||||||
|
</RCCascader>
|
||||||
|
);
|
8
packages/grafana-ui/src/components/Cascader/Cascader.mdx
Normal file
8
packages/grafana-ui/src/components/Cascader/Cascader.mdx
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import { Meta, Story, Preview, Props } from '@storybook/addon-docs/blocks';
|
||||||
|
import { Cascader } from './Cascader';
|
||||||
|
|
||||||
|
# Cascader with search
|
||||||
|
|
||||||
|
<Meta title="MDX|Cascader" component={Cascader} />
|
||||||
|
|
||||||
|
<Props of={Cascader}/>
|
@ -1,32 +1,49 @@
|
|||||||
import React from 'react';
|
import { text } from '@storybook/addon-knobs';
|
||||||
import { storiesOf } from '@storybook/react';
|
|
||||||
import { text, boolean, object } from '@storybook/addon-knobs';
|
|
||||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||||
import { Cascader } from './Cascader';
|
import { Cascader } from './Cascader';
|
||||||
|
// import { Button } from '../Button';
|
||||||
|
import mdx from './Cascader.mdx';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
const getKnobs = () => {
|
export default {
|
||||||
return {
|
title: 'UI/Cascader',
|
||||||
disabled: boolean('Disabled', false),
|
component: Cascader,
|
||||||
text: text('Button Text', 'Click me!'),
|
decorators: [withCenteredStory],
|
||||||
options: object('Options', [
|
parameters: {
|
||||||
{
|
docs: {
|
||||||
label: 'A',
|
page: mdx,
|
||||||
value: 'A',
|
},
|
||||||
children: [
|
},
|
||||||
{ label: 'B', value: 'B' },
|
|
||||||
{ label: 'C', value: 'C' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{ label: 'D', value: 'D' },
|
|
||||||
]),
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const CascaderStories = storiesOf('UI/Cascader', module);
|
const options = [
|
||||||
|
{
|
||||||
|
label: 'First',
|
||||||
|
value: '1',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Second',
|
||||||
|
value: '2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Third',
|
||||||
|
value: '3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Fourth',
|
||||||
|
value: '4',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'FirstFirst',
|
||||||
|
value: '5',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
CascaderStories.addDecorator(withCenteredStory);
|
export const simple = () => (
|
||||||
|
<Cascader separator={text('Separator', '')} options={options} onSelect={val => console.log(val)} />
|
||||||
CascaderStories.add('default', () => {
|
);
|
||||||
const { disabled, text, options } = getKnobs();
|
export const withInitialValue = () => (
|
||||||
return <Cascader disabled={disabled} options={options} value={['A']} expandIcon={null} buttonText={text} />;
|
<Cascader options={options} initialValue="3" onSelect={val => console.log(val)} />
|
||||||
});
|
);
|
||||||
|
@ -0,0 +1,58 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Cascader } from './Cascader';
|
||||||
|
import { shallow } from 'enzyme';
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{
|
||||||
|
label: 'First',
|
||||||
|
value: '1',
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Second',
|
||||||
|
value: '2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Third',
|
||||||
|
value: '3',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Fourth',
|
||||||
|
value: '4',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'FirstFirst',
|
||||||
|
value: '5',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const flatOptions = [
|
||||||
|
{
|
||||||
|
label: 'First / Second',
|
||||||
|
value: ['1', '2'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'First / Third',
|
||||||
|
value: ['1', '3'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'First / Fourth',
|
||||||
|
value: ['1', '4'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'FirstFirst',
|
||||||
|
value: ['5'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('Cascader', () => {
|
||||||
|
let cascader: any;
|
||||||
|
beforeEach(() => {
|
||||||
|
cascader = shallow(<Cascader options={options} onSelect={() => {}} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Should convert options to searchable strings', () => {
|
||||||
|
expect(cascader.state('searchableOptions')).toEqual(flatOptions);
|
||||||
|
});
|
||||||
|
});
|
@ -1,34 +1,204 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { Icon } from '../Icon/Icon';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import RCCascader from 'rc-cascader';
|
import RCCascader from 'rc-cascader';
|
||||||
|
|
||||||
export interface CascaderOption {
|
import { Select } from '../Forms/Select/Select';
|
||||||
label: string;
|
import { FormInputSize } from '../Forms/types';
|
||||||
value: string;
|
import { Input } from '../Forms/Input/Input';
|
||||||
|
import { SelectableValue } from '@grafana/data';
|
||||||
|
import { css } from 'emotion';
|
||||||
|
|
||||||
children?: CascaderOption[];
|
interface CascaderProps {
|
||||||
|
separator?: string;
|
||||||
|
options: CascaderOption[];
|
||||||
|
onSelect(val: string): void;
|
||||||
|
size?: FormInputSize;
|
||||||
|
initialValue?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CascaderState {
|
||||||
|
isSearching: boolean;
|
||||||
|
searchableOptions: Array<SelectableValue<string[]>>;
|
||||||
|
focusCascade: boolean;
|
||||||
|
//Array for cascade navigation
|
||||||
|
rcValue: SelectableValue<string[]>;
|
||||||
|
activeLabel: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CascaderOption {
|
||||||
|
value: any;
|
||||||
|
label: string;
|
||||||
|
items?: CascaderOption[];
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
// Undocumented tooltip API
|
|
||||||
title?: string;
|
title?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CascaderProps {
|
const disableDivFocus = css(`
|
||||||
options: CascaderOption[];
|
&:focus{
|
||||||
buttonText: string;
|
outline: none;
|
||||||
disabled?: boolean;
|
|
||||||
expandIcon?: React.ReactNode;
|
|
||||||
value?: string[];
|
|
||||||
|
|
||||||
loadData?: (selectedOptions: CascaderOption[]) => void;
|
|
||||||
onChange?: (value: string[], selectedOptions: CascaderOption[]) => void;
|
|
||||||
onPopupVisibleChange?: (visible: boolean) => void;
|
|
||||||
}
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
export const Cascader: React.FC<CascaderProps> = props => (
|
export class Cascader extends React.PureComponent<CascaderProps, CascaderState> {
|
||||||
<RCCascader {...props}>
|
constructor(props: CascaderProps) {
|
||||||
<button className="gf-form-label gf-form-label--btn" disabled={props.disabled}>
|
super(props);
|
||||||
{props.buttonText} <i className="fa fa-caret-down" />
|
const searchableOptions = this.flattenOptions(props.options);
|
||||||
</button>
|
const { rcValue, activeLabel } = this.setInitialValue(searchableOptions, props.initialValue);
|
||||||
</RCCascader>
|
this.state = {
|
||||||
);
|
isSearching: false,
|
||||||
|
focusCascade: false,
|
||||||
|
searchableOptions,
|
||||||
|
rcValue,
|
||||||
|
activeLabel,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
flattenOptions = (options: CascaderOption[], optionPath: CascaderOption[] = []) => {
|
||||||
|
let selectOptions: Array<SelectableValue<string[]>> = [];
|
||||||
|
for (const option of options) {
|
||||||
|
const cpy = [...optionPath];
|
||||||
|
cpy.push(option);
|
||||||
|
if (!option.items) {
|
||||||
|
selectOptions.push({
|
||||||
|
label: cpy.map(o => o.label).join(this.props.separator || ' / '),
|
||||||
|
value: cpy.map(o => o.value),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
selectOptions = [...selectOptions, ...this.flattenOptions(option.items, cpy)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return selectOptions;
|
||||||
|
};
|
||||||
|
|
||||||
|
setInitialValue(searchableOptions: Array<SelectableValue<string[]>>, initValue?: string) {
|
||||||
|
if (!initValue) {
|
||||||
|
return { rcValue: [], activeLabel: '' };
|
||||||
|
}
|
||||||
|
for (const option of searchableOptions) {
|
||||||
|
const optionPath = option.value || [];
|
||||||
|
|
||||||
|
if (optionPath.indexOf(initValue) === optionPath.length - 1) {
|
||||||
|
return {
|
||||||
|
rcValue: optionPath,
|
||||||
|
activeLabel: option.label || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { rcValue: [], activeLabel: '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
//For rc-cascader
|
||||||
|
onChange = (value: string[], selectedOptions: CascaderOption[]) => {
|
||||||
|
this.setState({
|
||||||
|
rcValue: value,
|
||||||
|
activeLabel: selectedOptions.map(o => o.label).join(this.props.separator || ' / '),
|
||||||
|
});
|
||||||
|
|
||||||
|
this.props.onSelect(selectedOptions[selectedOptions.length - 1].value);
|
||||||
|
};
|
||||||
|
|
||||||
|
//For select
|
||||||
|
onSelect = (obj: SelectableValue<string[]>) => {
|
||||||
|
this.setState({
|
||||||
|
activeLabel: obj.label || '',
|
||||||
|
rcValue: obj.value || [],
|
||||||
|
isSearching: false,
|
||||||
|
});
|
||||||
|
this.props.onSelect(this.state.rcValue[this.state.rcValue.length - 1]);
|
||||||
|
};
|
||||||
|
|
||||||
|
onClick = () => {
|
||||||
|
this.setState({
|
||||||
|
focusCascade: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onBlur = () => {
|
||||||
|
this.setState({
|
||||||
|
isSearching: false,
|
||||||
|
focusCascade: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.state.activeLabel === '') {
|
||||||
|
this.setState({
|
||||||
|
rcValue: [],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onBlurCascade = () => {
|
||||||
|
this.setState({
|
||||||
|
focusCascade: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
onInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (
|
||||||
|
e.key !== 'ArrowDown' &&
|
||||||
|
e.key !== 'ArrowUp' &&
|
||||||
|
e.key !== 'Enter' &&
|
||||||
|
e.key !== 'ArrowLeft' &&
|
||||||
|
e.key !== 'ArrowRight'
|
||||||
|
) {
|
||||||
|
this.setState({
|
||||||
|
focusCascade: false,
|
||||||
|
isSearching: true,
|
||||||
|
});
|
||||||
|
if (e.key === 'Backspace') {
|
||||||
|
const label = this.state.activeLabel || '';
|
||||||
|
this.setState({
|
||||||
|
activeLabel: label.slice(0, -1),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onInputChange = (value: string) => {
|
||||||
|
this.setState({
|
||||||
|
activeLabel: value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { size } = this.props;
|
||||||
|
const { focusCascade, isSearching, searchableOptions, rcValue, activeLabel } = this.state;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{isSearching ? (
|
||||||
|
<Select
|
||||||
|
inputValue={activeLabel}
|
||||||
|
placeholder="Search"
|
||||||
|
autoFocus={!focusCascade}
|
||||||
|
onChange={this.onSelect}
|
||||||
|
onInputChange={this.onInputChange}
|
||||||
|
onBlur={this.onBlur}
|
||||||
|
options={searchableOptions}
|
||||||
|
size={size || 'md'}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<RCCascader
|
||||||
|
onChange={this.onChange}
|
||||||
|
onClick={this.onClick}
|
||||||
|
options={this.props.options}
|
||||||
|
isFocused={focusCascade}
|
||||||
|
onBlur={this.onBlurCascade}
|
||||||
|
value={rcValue}
|
||||||
|
fieldNames={{ label: 'label', value: 'value', children: 'items' }}
|
||||||
|
>
|
||||||
|
<div className={disableDivFocus}>
|
||||||
|
<Input
|
||||||
|
value={activeLabel}
|
||||||
|
onKeyDown={this.onInputKeyDown}
|
||||||
|
onChange={() => {}}
|
||||||
|
size={size || 'md'}
|
||||||
|
suffix={focusCascade ? <Icon name="caret-up" /> : <Icon name="caret-down" />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</RCCascader>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
|
|
||||||
import { Button, ButtonVariant, ButtonProps } from '../Button';
|
import { Button, ButtonVariant, ButtonProps } from '../Button';
|
||||||
import { ButtonSize } from '../../Button/types';
|
import { ButtonSize } from '../../Button/types';
|
||||||
import { SelectCommonProps, SelectBase } from './SelectBase';
|
import { SelectCommonProps, SelectBase, CustomControlProps } from './SelectBase';
|
||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
import { useTheme } from '../../../themes';
|
import { useTheme } from '../../../themes';
|
||||||
import { Icon } from '../../Icon/Icon';
|
import { Icon } from '../../Icon/Icon';
|
||||||
@ -73,13 +73,13 @@ export function ButtonSelect<T>({
|
|||||||
return (
|
return (
|
||||||
<SelectBase
|
<SelectBase
|
||||||
{...selectProps}
|
{...selectProps}
|
||||||
renderControl={({ onBlur, onClick, value, isOpen }) => {
|
renderControl={React.forwardRef<any, CustomControlProps<T>>(({ onBlur, onClick, value, isOpen }, _ref) => {
|
||||||
return (
|
return (
|
||||||
<SelectButton {...buttonProps} onBlur={onBlur} onClick={onClick} isOpen={isOpen}>
|
<SelectButton {...buttonProps} onBlur={onBlur} onClick={onClick} isOpen={isOpen}>
|
||||||
{value ? value.label : placeholder}
|
{value ? value.label : placeholder}
|
||||||
</SelectButton>
|
</SelectButton>
|
||||||
);
|
);
|
||||||
}}
|
})}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -27,10 +27,13 @@ export interface SelectCommonProps<T> {
|
|||||||
className?: string;
|
className?: string;
|
||||||
options?: Array<SelectableValue<T>>;
|
options?: Array<SelectableValue<T>>;
|
||||||
defaultValue?: any;
|
defaultValue?: any;
|
||||||
|
inputValue?: string;
|
||||||
value?: SelectValue<T>;
|
value?: SelectValue<T>;
|
||||||
getOptionLabel?: (item: SelectableValue<T>) => string;
|
getOptionLabel?: (item: SelectableValue<T>) => string;
|
||||||
getOptionValue?: (item: SelectableValue<T>) => string;
|
getOptionValue?: (item: SelectableValue<T>) => string;
|
||||||
onChange: (value: SelectableValue<T>) => {} | void;
|
onChange: (value: SelectableValue<T>) => {} | void;
|
||||||
|
onInputChange?: (label: string) => void;
|
||||||
|
onKeyDown?: (event: React.KeyboardEvent) => void;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
isSearchable?: boolean;
|
isSearchable?: boolean;
|
||||||
@ -131,9 +134,12 @@ const CustomControl = (props: any) => {
|
|||||||
export function SelectBase<T>({
|
export function SelectBase<T>({
|
||||||
value,
|
value,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
|
inputValue,
|
||||||
|
onInputChange,
|
||||||
options = [],
|
options = [],
|
||||||
onChange,
|
onChange,
|
||||||
onBlur,
|
onBlur,
|
||||||
|
onKeyDown,
|
||||||
onCloseMenu,
|
onCloseMenu,
|
||||||
onOpenMenu,
|
onOpenMenu,
|
||||||
placeholder = 'Choose',
|
placeholder = 'Choose',
|
||||||
@ -201,6 +207,8 @@ export function SelectBase<T>({
|
|||||||
isLoading,
|
isLoading,
|
||||||
menuIsOpen: isOpen,
|
menuIsOpen: isOpen,
|
||||||
defaultValue,
|
defaultValue,
|
||||||
|
inputValue,
|
||||||
|
onInputChange,
|
||||||
value: isMulti ? selectedValue : selectedValue[0],
|
value: isMulti ? selectedValue : selectedValue[0],
|
||||||
getOptionLabel,
|
getOptionLabel,
|
||||||
getOptionValue,
|
getOptionValue,
|
||||||
@ -214,6 +222,7 @@ export function SelectBase<T>({
|
|||||||
options,
|
options,
|
||||||
onChange,
|
onChange,
|
||||||
onBlur,
|
onBlur,
|
||||||
|
onKeyDown,
|
||||||
menuShouldScrollIntoView: false,
|
menuShouldScrollIntoView: false,
|
||||||
renderControl,
|
renderControl,
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
import { Meta, Story, Preview, Props } from '@storybook/addon-docs/blocks';
|
||||||
|
import { UnitPicker } from './UnitPicker';
|
||||||
|
|
||||||
|
# UnitPicker
|
||||||
|
|
||||||
|
<Props of={UnitPicker}/>
|
@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||||
|
import { UnitPicker } from './UnitPicker';
|
||||||
|
import mdx from './UnitPicker.mdx';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'UI/UnitPicker',
|
||||||
|
component: UnitPicker,
|
||||||
|
decorators: [withCenteredStory],
|
||||||
|
parameters: {
|
||||||
|
docs: mdx,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const simple = () => <UnitPicker onChange={val => console.log(val)} />;
|
@ -1,5 +1,5 @@
|
|||||||
@import 'BarGauge/BarGauge';
|
@import 'BarGauge/BarGauge';
|
||||||
@import 'Cascader/Cascader';
|
@import 'ButtonCascader/ButtonCascader';
|
||||||
@import 'ColorPicker/ColorPicker';
|
@import 'ColorPicker/ColorPicker';
|
||||||
@import 'CustomScrollbar/CustomScrollbar';
|
@import 'CustomScrollbar/CustomScrollbar';
|
||||||
@import 'Drawer/Drawer';
|
@import 'Drawer/Drawer';
|
||||||
|
@ -14,6 +14,7 @@ export { IndicatorsContainer } from './Select/IndicatorsContainer';
|
|||||||
export { NoOptionsMessage } from './Select/NoOptionsMessage';
|
export { NoOptionsMessage } from './Select/NoOptionsMessage';
|
||||||
export { default as resetSelectStyles } from './Forms/Select/resetSelectStyles';
|
export { default as resetSelectStyles } from './Forms/Select/resetSelectStyles';
|
||||||
export { ButtonSelect } from './Select/ButtonSelect';
|
export { ButtonSelect } from './Select/ButtonSelect';
|
||||||
|
export { ButtonCascader } from './ButtonCascader/ButtonCascader';
|
||||||
export { Cascader, CascaderOption } from './Cascader/Cascader';
|
export { Cascader, CascaderOption } from './Cascader/Cascader';
|
||||||
|
|
||||||
// Forms
|
// Forms
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ExploreQueryFieldProps } from '@grafana/data';
|
import { ExploreQueryFieldProps } from '@grafana/data';
|
||||||
import { Cascader, CascaderOption } from '@grafana/ui';
|
import { ButtonCascader, CascaderOption } from '@grafana/ui';
|
||||||
|
|
||||||
import InfluxQueryModel from '../influx_query_model';
|
import InfluxQueryModel from '../influx_query_model';
|
||||||
import { AdHocFilterField, KeyValuePair } from 'app/features/explore/AdHocFilterField';
|
import { AdHocFilterField, KeyValuePair } from 'app/features/explore/AdHocFilterField';
|
||||||
@ -75,7 +75,7 @@ export class InfluxLogsQueryField extends React.PureComponent<Props, State> {
|
|||||||
measurements.push({
|
measurements.push({
|
||||||
label: measurementObj.text,
|
label: measurementObj.text,
|
||||||
value: measurementObj.text,
|
value: measurementObj.text,
|
||||||
children: fields,
|
items: fields,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.setState({ measurements });
|
this.setState({ measurements });
|
||||||
@ -134,7 +134,7 @@ export class InfluxLogsQueryField extends React.PureComponent<Props, State> {
|
|||||||
return (
|
return (
|
||||||
<div className="gf-form-inline gf-form-inline--nowrap">
|
<div className="gf-form-inline gf-form-inline--nowrap">
|
||||||
<div className="gf-form flex-shrink-0">
|
<div className="gf-form flex-shrink-0">
|
||||||
<Cascader
|
<ButtonCascader
|
||||||
buttonText={cascadeText}
|
buttonText={cascadeText}
|
||||||
options={measurements}
|
options={measurements}
|
||||||
disabled={!hasMeasurement}
|
disabled={!hasMeasurement}
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Cascader,
|
ButtonCascader,
|
||||||
CascaderOption,
|
CascaderOption,
|
||||||
SlatePrism,
|
SlatePrism,
|
||||||
TypeaheadOutput,
|
TypeaheadOutput,
|
||||||
@ -148,7 +148,7 @@ export class LokiQueryFieldForm extends React.PureComponent<LokiQueryFieldFormPr
|
|||||||
<>
|
<>
|
||||||
<div className="gf-form-inline">
|
<div className="gf-form-inline">
|
||||||
<div className="gf-form">
|
<div className="gf-form">
|
||||||
<Cascader
|
<ButtonCascader
|
||||||
options={logLabelOptions || []}
|
options={logLabelOptions || []}
|
||||||
disabled={buttonDisabled}
|
disabled={buttonDisabled}
|
||||||
buttonText={chooserText}
|
buttonText={chooserText}
|
||||||
|
@ -9,7 +9,7 @@ describe('groupMetricsByPrefix()', () => {
|
|||||||
expect(groupMetricsByPrefix(['foo_metric'])).toMatchObject([
|
expect(groupMetricsByPrefix(['foo_metric'])).toMatchObject([
|
||||||
{
|
{
|
||||||
value: 'foo',
|
value: 'foo',
|
||||||
children: [
|
items: [
|
||||||
{
|
{
|
||||||
value: 'foo_metric',
|
value: 'foo_metric',
|
||||||
},
|
},
|
||||||
@ -22,7 +22,7 @@ describe('groupMetricsByPrefix()', () => {
|
|||||||
expect(groupMetricsByPrefix(['foo_metric'], { foo_metric: [{ type: 'TYPE', help: 'my help' }] })).toMatchObject([
|
expect(groupMetricsByPrefix(['foo_metric'], { foo_metric: [{ type: 'TYPE', help: 'my help' }] })).toMatchObject([
|
||||||
{
|
{
|
||||||
value: 'foo',
|
value: 'foo',
|
||||||
children: [
|
items: [
|
||||||
{
|
{
|
||||||
value: 'foo_metric',
|
value: 'foo_metric',
|
||||||
title: 'foo_metric\nTYPE\nmy help',
|
title: 'foo_metric\nTYPE\nmy help',
|
||||||
@ -44,7 +44,7 @@ describe('groupMetricsByPrefix()', () => {
|
|||||||
expect(groupMetricsByPrefix([':foo_metric:'])).toMatchObject([
|
expect(groupMetricsByPrefix([':foo_metric:'])).toMatchObject([
|
||||||
{
|
{
|
||||||
value: RECORDING_RULES_GROUP,
|
value: RECORDING_RULES_GROUP,
|
||||||
children: [
|
items: [
|
||||||
{
|
{
|
||||||
value: ':foo_metric:',
|
value: ':foo_metric:',
|
||||||
},
|
},
|
||||||
|
@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
|
|
||||||
import { Plugin } from 'slate';
|
import { Plugin } from 'slate';
|
||||||
import {
|
import {
|
||||||
Cascader,
|
ButtonCascader,
|
||||||
CascaderOption,
|
CascaderOption,
|
||||||
SlatePrism,
|
SlatePrism,
|
||||||
TypeaheadInput,
|
TypeaheadInput,
|
||||||
@ -52,7 +52,7 @@ export function groupMetricsByPrefix(metrics: string[], metadata?: PromMetricsMe
|
|||||||
const rulesOption = {
|
const rulesOption = {
|
||||||
label: 'Recording rules',
|
label: 'Recording rules',
|
||||||
value: RECORDING_RULES_GROUP,
|
value: RECORDING_RULES_GROUP,
|
||||||
children: ruleNames
|
items: ruleNames
|
||||||
.slice()
|
.slice()
|
||||||
.sort()
|
.sort()
|
||||||
.map(name => ({ label: name, value: name })),
|
.map(name => ({ label: name, value: name })),
|
||||||
@ -69,7 +69,7 @@ export function groupMetricsByPrefix(metrics: string[], metadata?: PromMetricsMe
|
|||||||
const prefixIsMetric = metricsForPrefix.length === 1 && metricsForPrefix[0] === prefix;
|
const prefixIsMetric = metricsForPrefix.length === 1 && metricsForPrefix[0] === prefix;
|
||||||
const children = prefixIsMetric ? [] : metricsForPrefix.sort().map(m => addMetricsMetadata(m, metadata));
|
const children = prefixIsMetric ? [] : metricsForPrefix.sort().map(m => addMetricsMetadata(m, metadata));
|
||||||
return {
|
return {
|
||||||
children,
|
items: children,
|
||||||
label: prefix,
|
label: prefix,
|
||||||
value: prefix,
|
value: prefix,
|
||||||
};
|
};
|
||||||
@ -198,7 +198,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
|||||||
onChangeMetrics = (values: string[], selectedOptions: CascaderOption[]) => {
|
onChangeMetrics = (values: string[], selectedOptions: CascaderOption[]) => {
|
||||||
let query;
|
let query;
|
||||||
if (selectedOptions.length === 1) {
|
if (selectedOptions.length === 1) {
|
||||||
if (selectedOptions[0].children.length === 0) {
|
if (selectedOptions[0].items.length === 0) {
|
||||||
query = selectedOptions[0].value;
|
query = selectedOptions[0].value;
|
||||||
} else {
|
} else {
|
||||||
// Ignore click on group
|
// Ignore click on group
|
||||||
@ -254,10 +254,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
|||||||
const histogramOptions = histogramMetrics.map((hm: any) => ({ label: hm, value: hm }));
|
const histogramOptions = histogramMetrics.map((hm: any) => ({ label: hm, value: hm }));
|
||||||
const metricsOptions =
|
const metricsOptions =
|
||||||
histogramMetrics.length > 0
|
histogramMetrics.length > 0
|
||||||
? [
|
? [{ label: 'Histograms', value: HISTOGRAM_GROUP, items: histogramOptions, isLeaf: false }, ...metricsByPrefix]
|
||||||
{ label: 'Histograms', value: HISTOGRAM_GROUP, children: histogramOptions, isLeaf: false },
|
|
||||||
...metricsByPrefix,
|
|
||||||
]
|
|
||||||
: metricsByPrefix;
|
: metricsByPrefix;
|
||||||
|
|
||||||
// Hint for big disabled lookups
|
// Hint for big disabled lookups
|
||||||
@ -302,7 +299,7 @@ class PromQueryField extends React.PureComponent<PromQueryFieldProps, PromQueryF
|
|||||||
<>
|
<>
|
||||||
<div className="gf-form-inline gf-form-inline--nowrap flex-grow-1">
|
<div className="gf-form-inline gf-form-inline--nowrap flex-grow-1">
|
||||||
<div className="gf-form flex-shrink-0">
|
<div className="gf-form flex-shrink-0">
|
||||||
<Cascader
|
<ButtonCascader
|
||||||
options={metricsOptions}
|
options={metricsOptions}
|
||||||
buttonText={chooserText}
|
buttonText={chooserText}
|
||||||
disabled={buttonDisabled}
|
disabled={buttonDisabled}
|
||||||
|
Loading…
Reference in New Issue
Block a user