Commit aa0982da by Tobias Skarhed Committed by GitHub

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
parent 20aac7f0
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} />;
};
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>
);
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}/>
\ No newline at end of file
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 = [
{
CascaderStories.addDecorator(withCenteredStory); label: 'First',
value: '1',
items: [
{
label: 'Second',
value: '2',
},
{
label: 'Third',
value: '3',
},
{
label: 'Fourth',
value: '4',
},
],
},
{
label: 'FirstFirst',
value: '5',
},
];
CascaderStories.add('default', () => { export const simple = () => (
const { disabled, text, options } = getKnobs(); <Cascader separator={text('Separator', '')} options={options} onSelect={val => console.log(val)} />
return <Cascader disabled={disabled} options={options} value={['A']} expandIcon={null} buttonText={text} />; );
}); export const withInitialValue = () => (
<Cascader options={options} initialValue="3" onSelect={val => console.log(val)} />
);
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);
});
});
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';
import { Select } from '../Forms/Select/Select';
import { FormInputSize } from '../Forms/types';
import { Input } from '../Forms/Input/Input';
import { SelectableValue } from '@grafana/data';
import { css } from 'emotion';
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 { export interface CascaderOption {
value: any;
label: string; label: string;
value: string; items?: CascaderOption[];
children?: 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 class Cascader extends React.PureComponent<CascaderProps, CascaderState> {
constructor(props: CascaderProps) {
super(props);
const searchableOptions = this.flattenOptions(props.options);
const { rcValue, activeLabel } = this.setInitialValue(searchableOptions, props.initialValue);
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;
};
export const Cascader: React.FC<CascaderProps> = props => ( setInitialValue(searchableOptions: Array<SelectableValue<string[]>>, initValue?: string) {
<RCCascader {...props}> if (!initValue) {
<button className="gf-form-label gf-form-label--btn" disabled={props.disabled}> return { rcValue: [], activeLabel: '' };
{props.buttonText} <i className="fa fa-caret-down" /> }
</button> for (const option of searchableOptions) {
</RCCascader> 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'; ...@@ -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>({ ...@@ -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> { ...@@ -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) => { ...@@ -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>({ ...@@ -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>({ ...@@ -214,6 +222,7 @@ export function SelectBase<T>({
options, options,
onChange, onChange,
onBlur, onBlur,
onKeyDown,
menuShouldScrollIntoView: false, menuShouldScrollIntoView: false,
renderControl, renderControl,
}; };
......
import { Meta, Story, Preview, Props } from '@storybook/addon-docs/blocks';
import { UnitPicker } from './UnitPicker';
# UnitPicker
<Props of={UnitPicker}/>
\ No newline at end of file
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)} />;
@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'; ...@@ -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
......
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> { ...@@ -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> { ...@@ -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 @@ ...@@ -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 ...@@ -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()', () => { ...@@ -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()', () => { ...@@ -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()', () => { ...@@ -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'; ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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 ...@@ -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}
......
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