mirror of https://github.com/apache/superset.git
chore: Select component refactoring - SelectControl - Iteration 5 (#16510)
* Refactor Select DatasourceEditor * Fire onChange with allowNewOptions * Clean up * Refactor Select in AnnotationLayer * Handle on clear * Update tests * Refactor Select in SpatialControl * Show search * Refactor Select in FilterBox * Remove search where unnecessary * Update SelectControl - WIP * Refactor Controls * Update SelectControl tests * Clean up * Test allowNewOptions false * Use SelectControl AnnotationLayer * Use SelectControl SpatialControl * Clean up * Render custom label * Show search * Implement filterOption * Improve filterOption * Update Cypress * Update Cypress table test * Use value for defaultValue * Merge with latest changes * Reconcile with latest Select changes * Update superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.test.tsx Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com> * Update superset-frontend/src/explore/components/controls/AnnotationLayerControl/AnnotationLayer.test.tsx Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com> * Revert changes to test * Call onPopoverClear when v value is undefined Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com>
This commit is contained in:
parent
1ab36c94f3
commit
3f0756f637
|
@ -29,13 +29,13 @@ describe('Advanced analytics', () => {
|
||||||
|
|
||||||
cy.get('.ant-collapse-header').contains('Advanced Analytics').click();
|
cy.get('.ant-collapse-header').contains('Advanced Analytics').click();
|
||||||
|
|
||||||
cy.get('[data-test=time_compare]').find('.Select__control').click();
|
cy.get('[data-test=time_compare]').find('.ant-select').click();
|
||||||
cy.get('[data-test=time_compare]')
|
cy.get('[data-test=time_compare]')
|
||||||
.find('input[type=text]')
|
.find('input[type=search]')
|
||||||
.type('28 days{enter}');
|
.type('28 days{enter}');
|
||||||
|
|
||||||
cy.get('[data-test=time_compare]')
|
cy.get('[data-test=time_compare]')
|
||||||
.find('input[type=text]')
|
.find('input[type=search]')
|
||||||
.type('1 year{enter}');
|
.type('1 year{enter}');
|
||||||
|
|
||||||
cy.get('button[data-test="run-query-button"]').click();
|
cy.get('button[data-test="run-query-button"]').click();
|
||||||
|
@ -48,10 +48,10 @@ describe('Advanced analytics', () => {
|
||||||
|
|
||||||
cy.get('.ant-collapse-header').contains('Advanced Analytics').click();
|
cy.get('.ant-collapse-header').contains('Advanced Analytics').click();
|
||||||
cy.get('[data-test=time_compare]')
|
cy.get('[data-test=time_compare]')
|
||||||
.find('.Select__multi-value__label')
|
.find('.ant-select-selector')
|
||||||
.contains('28 days');
|
.contains('28 days');
|
||||||
cy.get('[data-test=time_compare]')
|
cy.get('[data-test=time_compare]')
|
||||||
.find('.Select__multi-value__label')
|
.find('.ant-select-selector')
|
||||||
.contains('1 year');
|
.contains('1 year');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -235,8 +235,8 @@ describe('Groupby control', () => {
|
||||||
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
cy.verifySliceSuccess({ waitAlias: '@chartData' });
|
||||||
|
|
||||||
cy.get('[data-test=groupby]').within(() => {
|
cy.get('[data-test=groupby]').within(() => {
|
||||||
cy.get('.Select__control').click();
|
cy.get('.ant-select').click();
|
||||||
cy.get('input[type=text]').type('state{enter}');
|
cy.get('input[type=search]').type('state{enter}');
|
||||||
});
|
});
|
||||||
cy.get('button[data-test="run-query-button"]').click();
|
cy.get('button[data-test="run-query-button"]').click();
|
||||||
cy.verifySliceSuccess({ waitAlias: '@chartData', chartSelector: 'svg' });
|
cy.verifySliceSuccess({ waitAlias: '@chartData', chartSelector: 'svg' });
|
||||||
|
|
|
@ -54,7 +54,7 @@ describe('Visualization > Table', () => {
|
||||||
granularity_sqla: undefined,
|
granularity_sqla: undefined,
|
||||||
metrics: ['count'],
|
metrics: ['count'],
|
||||||
});
|
});
|
||||||
cy.get('input[name="select-granularity_sqla"]').should('have.value', 'ds');
|
cy.get('[data-test=granularity_sqla] .column-option-label').contains('ds');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Format non-numeric metrics correctly', () => {
|
it('Format non-numeric metrics correctly', () => {
|
||||||
|
|
|
@ -20,8 +20,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import sinon from 'sinon';
|
import sinon from 'sinon';
|
||||||
import { shallow } from 'enzyme';
|
import { shallow } from 'enzyme';
|
||||||
import { Select, CreatableSelect } from 'src/components/Select';
|
import { Select as SelectComponent } from 'src/components';
|
||||||
import OnPasteSelect from 'src/components/Select/OnPasteSelect';
|
|
||||||
import SelectControl from 'src/explore/components/controls/SelectControl';
|
import SelectControl from 'src/explore/components/controls/SelectControl';
|
||||||
import { styledMount as mount } from 'spec/helpers/theming';
|
import { styledMount as mount } from 'spec/helpers/theming';
|
||||||
|
|
||||||
|
@ -48,59 +47,35 @@ describe('SelectControl', () => {
|
||||||
wrapper = shallow(<SelectControl {...defaultProps} />);
|
wrapper = shallow(<SelectControl {...defaultProps} />);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('uses Select in onPasteSelect when freeForm=false', () => {
|
|
||||||
wrapper = shallow(<SelectControl {...defaultProps} multi />);
|
|
||||||
const select = wrapper.find(OnPasteSelect);
|
|
||||||
expect(select.props().selectWrap).toBe(Select);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('uses Creatable in onPasteSelect when freeForm=true', () => {
|
|
||||||
wrapper = shallow(<SelectControl {...defaultProps} multi freeForm />);
|
|
||||||
const select = wrapper.find(OnPasteSelect);
|
|
||||||
expect(select.props().selectWrap).toBe(CreatableSelect);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls props.onChange when select', () => {
|
it('calls props.onChange when select', () => {
|
||||||
const select = wrapper.instance();
|
const select = wrapper.instance();
|
||||||
select.onChange({ value: 50 });
|
select.onChange(50);
|
||||||
expect(defaultProps.onChange.calledWith(50)).toBe(true);
|
expect(defaultProps.onChange.calledWith(50)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns all options on select all', () => {
|
|
||||||
const expectedValues = ['one', 'two'];
|
|
||||||
const selectAllProps = {
|
|
||||||
multi: true,
|
|
||||||
allowAll: true,
|
|
||||||
choices: expectedValues,
|
|
||||||
name: 'row_limit',
|
|
||||||
label: 'Row Limit',
|
|
||||||
valueKey: 'value',
|
|
||||||
onChange: sinon.spy(),
|
|
||||||
};
|
|
||||||
wrapper.setProps(selectAllProps);
|
|
||||||
wrapper.instance().onChange([{ meta: true, value: 'Select all' }]);
|
|
||||||
expect(selectAllProps.onChange.calledWith(expectedValues)).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('render', () => {
|
describe('render', () => {
|
||||||
it('renders with Select by default', () => {
|
it('renders with Select by default', () => {
|
||||||
expect(wrapper.find(OnPasteSelect)).not.toExist();
|
expect(wrapper.find(SelectComponent)).toExist();
|
||||||
expect(wrapper.findWhere(x => x.type() === Select)).toHaveLength(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders with OnPasteSelect when multi', () => {
|
it('renders as mode multiple', () => {
|
||||||
wrapper.setProps({ multi: true });
|
wrapper.setProps({ multi: true });
|
||||||
expect(wrapper.find(OnPasteSelect)).toExist();
|
expect(wrapper.find(SelectComponent)).toExist();
|
||||||
expect(wrapper.findWhere(x => x.type() === Select)).toHaveLength(0);
|
expect(wrapper.find(SelectComponent).prop('mode')).toBe('multiple');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders with Creatable when freeForm', () => {
|
it('renders with allowNewOptions when freeForm', () => {
|
||||||
wrapper.setProps({ freeForm: true });
|
wrapper.setProps({ freeForm: true });
|
||||||
expect(wrapper.find(OnPasteSelect)).not.toExist();
|
expect(wrapper.find(SelectComponent)).toExist();
|
||||||
expect(wrapper.findWhere(x => x.type() === CreatableSelect)).toHaveLength(
|
expect(wrapper.find(SelectComponent).prop('allowNewOptions')).toBe(true);
|
||||||
1,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders with allowNewOptions=false when freeForm=false', () => {
|
||||||
|
wrapper.setProps({ freeForm: false });
|
||||||
|
expect(wrapper.find(SelectComponent)).toExist();
|
||||||
|
expect(wrapper.find(SelectComponent).prop('allowNewOptions')).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
describe('empty placeholder', () => {
|
describe('empty placeholder', () => {
|
||||||
describe('withMulti', () => {
|
describe('withMulti', () => {
|
||||||
it('does not show a placeholder if there are no choices', () => {
|
it('does not show a placeholder if there are no choices', () => {
|
||||||
|
@ -161,16 +136,6 @@ describe('SelectControl', () => {
|
||||||
);
|
);
|
||||||
expect(wrapper.html()).not.toContain('add something');
|
expect(wrapper.html()).not.toContain('add something');
|
||||||
});
|
});
|
||||||
it('shows numbers of options as a placeholder by default', () => {
|
|
||||||
wrapper = mount(<SelectControl {...defaultProps} multi />);
|
|
||||||
expect(wrapper.html()).toContain('2 option(s');
|
|
||||||
});
|
|
||||||
it('reduces the number of options in the placeholder by the value length', () => {
|
|
||||||
wrapper = mount(
|
|
||||||
<SelectControl {...defaultProps} multi value={['today']} />,
|
|
||||||
);
|
|
||||||
expect(wrapper.html()).toContain('1 option(s');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
describe('when select is single', () => {
|
describe('when select is single', () => {
|
||||||
it('does not render the placeholder when a selection has been made', () => {
|
it('does not render the placeholder when a selection has been made', () => {
|
||||||
|
@ -186,82 +151,12 @@ describe('SelectControl', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('optionsRemaining', () => {
|
|
||||||
describe('isMulti', () => {
|
|
||||||
it('returns the options minus selected values', () => {
|
|
||||||
const wrapper = mount(
|
|
||||||
<SelectControl {...defaultProps} multi value={['today']} />,
|
|
||||||
);
|
|
||||||
expect(wrapper.instance().optionsRemaining()).toEqual(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('is not multi', () => {
|
|
||||||
it('returns the length of all options', () => {
|
|
||||||
wrapper = mount(
|
|
||||||
<SelectControl
|
|
||||||
{...defaultProps}
|
|
||||||
value={50}
|
|
||||||
placeholder="add something"
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
expect(wrapper.instance().optionsRemaining()).toEqual(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('with Select all', () => {
|
|
||||||
it('does not count it', () => {
|
|
||||||
const props = { ...defaultProps, multi: true, allowAll: true };
|
|
||||||
const wrapper = mount(<SelectControl {...props} />);
|
|
||||||
expect(wrapper.instance().getOptions(props).length).toEqual(3);
|
|
||||||
expect(wrapper.instance().optionsRemaining()).toEqual(2);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getOptions', () => {
|
describe('getOptions', () => {
|
||||||
it('returns the correct options', () => {
|
it('returns the correct options', () => {
|
||||||
wrapper.setProps(defaultProps);
|
wrapper.setProps(defaultProps);
|
||||||
expect(wrapper.instance().getOptions(defaultProps)).toEqual(options);
|
expect(wrapper.instance().getOptions(defaultProps)).toEqual(options);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows Select-All when enabled', () => {
|
|
||||||
const selectAllProps = {
|
|
||||||
choices: ['one', 'two'],
|
|
||||||
name: 'name',
|
|
||||||
freeForm: true,
|
|
||||||
allowAll: true,
|
|
||||||
multi: true,
|
|
||||||
valueKey: 'value',
|
|
||||||
};
|
|
||||||
wrapper.setProps(selectAllProps);
|
|
||||||
expect(wrapper.instance().getOptions(selectAllProps)).toContainEqual({
|
|
||||||
label: 'Select all',
|
|
||||||
meta: true,
|
|
||||||
value: 'Select all',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the correct options when freeform is set to true', () => {
|
|
||||||
const freeFormProps = {
|
|
||||||
choices: [],
|
|
||||||
freeForm: true,
|
|
||||||
value: ['one', 'two'],
|
|
||||||
name: 'row_limit',
|
|
||||||
label: 'Row Limit',
|
|
||||||
valueKey: 'custom_value_key',
|
|
||||||
onChange: sinon.spy(),
|
|
||||||
};
|
|
||||||
// the last added option is at the top
|
|
||||||
const expectedNewOptions = [
|
|
||||||
{ custom_value_key: 'two', label: 'two' },
|
|
||||||
{ custom_value_key: 'one', label: 'one' },
|
|
||||||
];
|
|
||||||
wrapper.setProps(freeFormProps);
|
|
||||||
expect(wrapper.instance().getOptions(freeFormProps)).toEqual(
|
|
||||||
expectedNewOptions,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('UNSAFE_componentWillReceiveProps', () => {
|
describe('UNSAFE_componentWillReceiveProps', () => {
|
||||||
it('sets state.options if props.choices has changed', () => {
|
it('sets state.options if props.choices has changed', () => {
|
||||||
const updatedOptions = [
|
const updatedOptions = [
|
||||||
|
|
|
@ -31,7 +31,7 @@ const defaultProps = {
|
||||||
onChange: sinon.spy(),
|
onChange: sinon.spy(),
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('SelectControl', () => {
|
describe('TextArea', () => {
|
||||||
let wrapper;
|
let wrapper;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wrapper = shallow(<TextAreaControl {...defaultProps} />);
|
wrapper = shallow(<TextAreaControl {...defaultProps} />);
|
||||||
|
|
|
@ -93,13 +93,10 @@ export const controlPanelSectionsChartOptionsTable: ControlPanelSectionConfig[]
|
||||||
default: [],
|
default: [],
|
||||||
description: t('Columns to display'),
|
description: t('Columns to display'),
|
||||||
optionRenderer: c => <ColumnOption column={c} showType />,
|
optionRenderer: c => <ColumnOption column={c} showType />,
|
||||||
valueRenderer: c => <ColumnOption column={c} />,
|
|
||||||
valueKey: 'column_name',
|
valueKey: 'column_name',
|
||||||
allowAll: true,
|
|
||||||
mapStateToProps: stateRef => ({
|
mapStateToProps: stateRef => ({
|
||||||
options: stateRef.datasource ? stateRef.datasource.columns : [],
|
options: stateRef.datasource ? stateRef.datasource.columns : [],
|
||||||
}),
|
}),
|
||||||
commaChoosesOption: false,
|
|
||||||
freeForm: true,
|
freeForm: true,
|
||||||
} as ControlConfig<'SelectControl', ColumnMeta>,
|
} as ControlConfig<'SelectControl', ColumnMeta>,
|
||||||
},
|
},
|
||||||
|
|
|
@ -40,7 +40,7 @@ import { getClientErrorObject } from 'src/utils/getClientErrorObject';
|
||||||
|
|
||||||
import CheckboxControl from 'src/explore/components/controls/CheckboxControl';
|
import CheckboxControl from 'src/explore/components/controls/CheckboxControl';
|
||||||
import TextControl from 'src/explore/components/controls/TextControl';
|
import TextControl from 'src/explore/components/controls/TextControl';
|
||||||
import SelectControl from 'src/explore/components/controls/SelectControl';
|
import { Select } from 'src/components';
|
||||||
import TextAreaControl from 'src/explore/components/controls/TextAreaControl';
|
import TextAreaControl from 'src/explore/components/controls/TextAreaControl';
|
||||||
import SelectAsyncControl from 'src/explore/components/controls/SelectAsyncControl';
|
import SelectAsyncControl from 'src/explore/components/controls/SelectAsyncControl';
|
||||||
import SpatialControl from 'src/explore/components/controls/SpatialControl';
|
import SpatialControl from 'src/explore/components/controls/SpatialControl';
|
||||||
|
@ -121,7 +121,12 @@ const StyledLabelWrapper = styled.div`
|
||||||
const checkboxGenerator = (d, onChange) => (
|
const checkboxGenerator = (d, onChange) => (
|
||||||
<CheckboxControl value={d} onChange={onChange} />
|
<CheckboxControl value={d} onChange={onChange} />
|
||||||
);
|
);
|
||||||
const DATA_TYPES = ['STRING', 'NUMERIC', 'DATETIME', 'BOOLEAN'];
|
const DATA_TYPES = [
|
||||||
|
{ value: 'STRING', label: 'STRING' },
|
||||||
|
{ value: 'NUMERIC', label: 'NUMERIC' },
|
||||||
|
{ value: 'DATETIME', label: 'DATETIME' },
|
||||||
|
{ value: 'BOOLEAN', label: 'BOOLEAN' },
|
||||||
|
];
|
||||||
|
|
||||||
const DATASOURCE_TYPES_ARR = [
|
const DATASOURCE_TYPES_ARR = [
|
||||||
{ key: 'physical', label: t('Physical (table or view)') },
|
{ key: 'physical', label: t('Physical (table or view)') },
|
||||||
|
@ -207,7 +212,13 @@ function ColumnCollectionTable({
|
||||||
fieldKey="type"
|
fieldKey="type"
|
||||||
label={t('Data type')}
|
label={t('Data type')}
|
||||||
control={
|
control={
|
||||||
<SelectControl choices={DATA_TYPES} name="type" freeForm />
|
<Select
|
||||||
|
ariaLabel={t('Data type')}
|
||||||
|
options={DATA_TYPES}
|
||||||
|
name="type"
|
||||||
|
allowNewOptions
|
||||||
|
allowClear
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -70,8 +70,6 @@ const propTypes = {
|
||||||
addAnnotationLayer: PropTypes.func,
|
addAnnotationLayer: PropTypes.func,
|
||||||
removeAnnotationLayer: PropTypes.func,
|
removeAnnotationLayer: PropTypes.func,
|
||||||
close: PropTypes.func,
|
close: PropTypes.func,
|
||||||
|
|
||||||
onPopoverClear: PropTypes.func,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
|
@ -95,7 +93,6 @@ const defaultProps = {
|
||||||
addAnnotationLayer: () => {},
|
addAnnotationLayer: () => {},
|
||||||
removeAnnotationLayer: () => {},
|
removeAnnotationLayer: () => {},
|
||||||
close: () => {},
|
close: () => {},
|
||||||
onPopoverClear: () => {},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class AnnotationLayer extends React.PureComponent {
|
export default class AnnotationLayer extends React.PureComponent {
|
||||||
|
@ -172,7 +169,6 @@ export default class AnnotationLayer extends React.PureComponent {
|
||||||
);
|
);
|
||||||
this.handleValue = this.handleValue.bind(this);
|
this.handleValue = this.handleValue.bind(this);
|
||||||
this.isValidForm = this.isValidForm.bind(this);
|
this.isValidForm = this.isValidForm.bind(this);
|
||||||
this.popoverClearWrapper = this.popoverClearWrapper.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -238,15 +234,6 @@ export default class AnnotationLayer extends React.PureComponent {
|
||||||
return !errors.filter(x => x).length;
|
return !errors.filter(x => x).length;
|
||||||
}
|
}
|
||||||
|
|
||||||
popoverClearWrapper(value, actionMeta, callback) {
|
|
||||||
if (callback) {
|
|
||||||
callback(value);
|
|
||||||
}
|
|
||||||
if (actionMeta?.action === 'clear') {
|
|
||||||
this.props.onPopoverClear(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleAnnotationType(annotationType) {
|
handleAnnotationType(annotationType) {
|
||||||
this.setState({
|
this.setState({
|
||||||
annotationType,
|
annotationType,
|
||||||
|
@ -266,7 +253,7 @@ export default class AnnotationLayer extends React.PureComponent {
|
||||||
handleValue(value) {
|
handleValue(value) {
|
||||||
this.setState({
|
this.setState({
|
||||||
value,
|
value,
|
||||||
descriptionColumns: null,
|
descriptionColumns: [],
|
||||||
intervalEndColumn: null,
|
intervalEndColumn: null,
|
||||||
timeColumn: null,
|
timeColumn: null,
|
||||||
titleColumn: null,
|
titleColumn: null,
|
||||||
|
@ -409,6 +396,7 @@ export default class AnnotationLayer extends React.PureComponent {
|
||||||
if (requiresQuery(sourceType)) {
|
if (requiresQuery(sourceType)) {
|
||||||
return (
|
return (
|
||||||
<SelectControl
|
<SelectControl
|
||||||
|
ariaLabel={t('Annotation layer value')}
|
||||||
name="annotation-layer-value"
|
name="annotation-layer-value"
|
||||||
showHeader
|
showHeader
|
||||||
hovered
|
hovered
|
||||||
|
@ -418,9 +406,7 @@ export default class AnnotationLayer extends React.PureComponent {
|
||||||
options={valueOptions}
|
options={valueOptions}
|
||||||
isLoading={isLoadingOptions}
|
isLoading={isLoadingOptions}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(value, _, actionMeta) =>
|
onChange={this.handleValue}
|
||||||
this.popoverClearWrapper(value, actionMeta, this.handleValue)
|
|
||||||
}
|
|
||||||
validationErrors={!value ? ['Mandatory'] : []}
|
validationErrors={!value ? ['Mandatory'] : []}
|
||||||
optionRenderer={this.renderOption}
|
optionRenderer={this.renderOption}
|
||||||
/>
|
/>
|
||||||
|
@ -479,14 +465,17 @@ export default class AnnotationLayer extends React.PureComponent {
|
||||||
{(annotationType === ANNOTATION_TYPES.EVENT ||
|
{(annotationType === ANNOTATION_TYPES.EVENT ||
|
||||||
annotationType === ANNOTATION_TYPES.INTERVAL) && (
|
annotationType === ANNOTATION_TYPES.INTERVAL) && (
|
||||||
<SelectControl
|
<SelectControl
|
||||||
|
ariaLabel={t('Annotation layer time column')}
|
||||||
hovered
|
hovered
|
||||||
name="annotation-layer-time-column"
|
name="annotation-layer-time-column"
|
||||||
label={
|
label={
|
||||||
annotationType === ANNOTATION_TYPES.INTERVAL
|
annotationType === ANNOTATION_TYPES.INTERVAL
|
||||||
? 'Interval start column'
|
? t('Interval start column')
|
||||||
: 'Event time column'
|
: t('Event time column')
|
||||||
}
|
}
|
||||||
description="This column must contain date/time information."
|
description={t(
|
||||||
|
'This column must contain date/time information.',
|
||||||
|
)}
|
||||||
validationErrors={!timeColumn ? ['Mandatory'] : []}
|
validationErrors={!timeColumn ? ['Mandatory'] : []}
|
||||||
clearable={false}
|
clearable={false}
|
||||||
options={timeColumnOptions}
|
options={timeColumnOptions}
|
||||||
|
@ -496,48 +485,42 @@ export default class AnnotationLayer extends React.PureComponent {
|
||||||
)}
|
)}
|
||||||
{annotationType === ANNOTATION_TYPES.INTERVAL && (
|
{annotationType === ANNOTATION_TYPES.INTERVAL && (
|
||||||
<SelectControl
|
<SelectControl
|
||||||
|
ariaLabel={t('Annotation layer interval end')}
|
||||||
hovered
|
hovered
|
||||||
name="annotation-layer-intervalEnd"
|
name="annotation-layer-intervalEnd"
|
||||||
label="Interval End column"
|
label={t('Interval End column')}
|
||||||
description="This column must contain date/time information."
|
description={t(
|
||||||
|
'This column must contain date/time information.',
|
||||||
|
)}
|
||||||
validationErrors={!intervalEndColumn ? ['Mandatory'] : []}
|
validationErrors={!intervalEndColumn ? ['Mandatory'] : []}
|
||||||
options={columns}
|
options={columns}
|
||||||
value={intervalEndColumn}
|
value={intervalEndColumn}
|
||||||
onChange={(value, _, actionMeta) =>
|
onChange={value => this.setState({ intervalEndColumn: value })}
|
||||||
this.popoverClearWrapper(value, actionMeta, v =>
|
|
||||||
this.setState({ intervalEndColumn: v }),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<SelectControl
|
<SelectControl
|
||||||
|
ariaLabel={t('Annotation layer title column')}
|
||||||
hovered
|
hovered
|
||||||
name="annotation-layer-title"
|
name="annotation-layer-title"
|
||||||
label="Title Column"
|
label={t('Title Column')}
|
||||||
description="Pick a title for you annotation."
|
description={t('Pick a title for you annotation.')}
|
||||||
options={[{ value: '', label: 'None' }].concat(columns)}
|
options={[{ value: '', label: 'None' }].concat(columns)}
|
||||||
value={titleColumn}
|
value={titleColumn}
|
||||||
onChange={(value, _, actionMeta) =>
|
onChange={value => this.setState({ titleColumn: value })}
|
||||||
this.popoverClearWrapper(value, actionMeta, v =>
|
|
||||||
this.setState({ titleColumn: v }),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
{annotationType !== ANNOTATION_TYPES.TIME_SERIES && (
|
{annotationType !== ANNOTATION_TYPES.TIME_SERIES && (
|
||||||
<SelectControl
|
<SelectControl
|
||||||
|
ariaLabel={t('Annotation layer description columns')}
|
||||||
hovered
|
hovered
|
||||||
name="annotation-layer-title"
|
name="annotation-layer-title"
|
||||||
label="Description Columns"
|
label={t('Description Columns')}
|
||||||
description={`Pick one or more columns that should be shown in the
|
description={t(
|
||||||
annotation. If you don't select a column all of them will be shown.`}
|
"Pick one or more columns that should be shown in the annotation. If you don't select a column all of them will be shown.",
|
||||||
|
)}
|
||||||
multi
|
multi
|
||||||
options={columns}
|
options={columns}
|
||||||
value={descriptionColumns}
|
value={descriptionColumns}
|
||||||
onChange={(value, _, actionMeta) =>
|
onChange={value => this.setState({ descriptionColumns: value })}
|
||||||
this.popoverClearWrapper(value, actionMeta, v =>
|
|
||||||
this.setState({ descriptionColumns: v }),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div style={{ marginTop: '1rem' }}>
|
<div style={{ marginTop: '1rem' }}>
|
||||||
|
@ -629,6 +612,7 @@ export default class AnnotationLayer extends React.PureComponent {
|
||||||
info={t('Configure your how you overlay is displayed here.')}
|
info={t('Configure your how you overlay is displayed here.')}
|
||||||
>
|
>
|
||||||
<SelectControl
|
<SelectControl
|
||||||
|
ariaLabel={t('Annotation layer stroke')}
|
||||||
name="annotation-layer-stroke"
|
name="annotation-layer-stroke"
|
||||||
label={t('Style')}
|
label={t('Style')}
|
||||||
// see '../../../visualizations/nvd3_vis.css'
|
// see '../../../visualizations/nvd3_vis.css'
|
||||||
|
@ -643,6 +627,7 @@ export default class AnnotationLayer extends React.PureComponent {
|
||||||
onChange={v => this.setState({ style: v })}
|
onChange={v => this.setState({ style: v })}
|
||||||
/>
|
/>
|
||||||
<SelectControl
|
<SelectControl
|
||||||
|
ariaLabel={t('Annotation layer opacity')}
|
||||||
name="annotation-layer-opacity"
|
name="annotation-layer-opacity"
|
||||||
label={t('Opacity')}
|
label={t('Opacity')}
|
||||||
// see '../../../visualizations/nvd3_vis.css'
|
// see '../../../visualizations/nvd3_vis.css'
|
||||||
|
@ -653,11 +638,7 @@ export default class AnnotationLayer extends React.PureComponent {
|
||||||
{ value: 'opacityHigh', label: '0.8' },
|
{ value: 'opacityHigh', label: '0.8' },
|
||||||
]}
|
]}
|
||||||
value={opacity}
|
value={opacity}
|
||||||
onChange={(value, _, actionMeta) =>
|
onChange={value => this.setState({ opacity: value })}
|
||||||
this.popoverClearWrapper(value, actionMeta, v =>
|
|
||||||
this.setState({ opacity: v }),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<ControlHeader label={t('Color')} />
|
<ControlHeader label={t('Color')} />
|
||||||
|
@ -746,6 +727,7 @@ export default class AnnotationLayer extends React.PureComponent {
|
||||||
onChange={v => this.setState({ show: !v })}
|
onChange={v => this.setState({ show: !v })}
|
||||||
/>
|
/>
|
||||||
<SelectControl
|
<SelectControl
|
||||||
|
ariaLabel={t('Annotation layer type')}
|
||||||
hovered
|
hovered
|
||||||
description={t('Choose the annotation layer type')}
|
description={t('Choose the annotation layer type')}
|
||||||
label={t('Annotation layer type')}
|
label={t('Annotation layer type')}
|
||||||
|
@ -757,19 +739,14 @@ export default class AnnotationLayer extends React.PureComponent {
|
||||||
/>
|
/>
|
||||||
{supportedSourceTypes.length > 0 && (
|
{supportedSourceTypes.length > 0 && (
|
||||||
<SelectControl
|
<SelectControl
|
||||||
|
ariaLabel={t('Annotation source type')}
|
||||||
hovered
|
hovered
|
||||||
description="Choose the source of your annotations"
|
description={t('Choose the source of your annotations')}
|
||||||
label="Annotation Source"
|
label={t('Annotation Source')}
|
||||||
name="annotation-source-type"
|
name="annotation-source-type"
|
||||||
options={supportedSourceTypes}
|
options={supportedSourceTypes}
|
||||||
value={sourceType}
|
value={sourceType}
|
||||||
onChange={(value, _, actionMeta) =>
|
onChange={this.handleAnnotationSourceType}
|
||||||
this.popoverClearWrapper(
|
|
||||||
value,
|
|
||||||
actionMeta,
|
|
||||||
this.handleAnnotationSourceType,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
validationErrors={!sourceType ? [t('Mandatory')] : []}
|
validationErrors={!sourceType ? [t('Mandatory')] : []}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -84,10 +84,22 @@ test('renders extra checkboxes when type is time series', async () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('enables apply and ok buttons', async () => {
|
test('enables apply and ok buttons', async () => {
|
||||||
await waitForRender();
|
const { container } = render(<AnnotationLayer {...defaultProps} />);
|
||||||
userEvent.type(screen.getByLabelText('Name'), 'Test');
|
|
||||||
userEvent.type(screen.getByLabelText('Formula'), '2x');
|
waitFor(() => {
|
||||||
await waitFor(() => {
|
expect(container).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const nameInput = screen.getByRole('textbox', { name: 'Name' });
|
||||||
|
const formulaInput = screen.getByRole('textbox', { name: 'Formula' });
|
||||||
|
|
||||||
|
expect(nameInput).toBeInTheDocument();
|
||||||
|
expect(formulaInput).toBeInTheDocument();
|
||||||
|
|
||||||
|
userEvent.type(nameInput, 'Name');
|
||||||
|
userEvent.type(formulaInput, '2x');
|
||||||
|
|
||||||
|
waitFor(() => {
|
||||||
expect(screen.getByRole('button', { name: 'Apply' })).toBeEnabled();
|
expect(screen.getByRole('button', { name: 'Apply' })).toBeEnabled();
|
||||||
expect(screen.getByRole('button', { name: 'OK' })).toBeEnabled();
|
expect(screen.getByRole('button', { name: 'OK' })).toBeEnabled();
|
||||||
});
|
});
|
||||||
|
@ -134,12 +146,17 @@ test('renders chart options', async () => {
|
||||||
await waitForRender({
|
await waitForRender({
|
||||||
annotationType: ANNOTATION_TYPES_METADATA.EVENT.value,
|
annotationType: ANNOTATION_TYPES_METADATA.EVENT.value,
|
||||||
});
|
});
|
||||||
userEvent.click(screen.getByText('2 option(s)'));
|
userEvent.click(
|
||||||
userEvent.click(screen.getByText('Superset annotation'));
|
screen.getByRole('combobox', { name: 'Annotation source type' }),
|
||||||
expect(await screen.findByLabelText('Annotation layer')).toBeInTheDocument();
|
);
|
||||||
userEvent.click(screen.getByText('Superset annotation'));
|
userEvent.click(screen.getByText('Superset annotation'));
|
||||||
|
expect(screen.getByText('Annotation layer')).toBeInTheDocument();
|
||||||
|
|
||||||
|
userEvent.click(
|
||||||
|
screen.getByRole('combobox', { name: 'Annotation source type' }),
|
||||||
|
);
|
||||||
userEvent.click(screen.getByText('Table'));
|
userEvent.click(screen.getByText('Table'));
|
||||||
expect(await screen.findByLabelText('Chart')).toBeInTheDocument();
|
expect(screen.getByText('Chart')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('keeps apply disabled when missing required fields', async () => {
|
test('keeps apply disabled when missing required fields', async () => {
|
||||||
|
@ -147,18 +164,28 @@ test('keeps apply disabled when missing required fields', async () => {
|
||||||
annotationType: ANNOTATION_TYPES_METADATA.EVENT.value,
|
annotationType: ANNOTATION_TYPES_METADATA.EVENT.value,
|
||||||
sourceType: 'Table',
|
sourceType: 'Table',
|
||||||
});
|
});
|
||||||
userEvent.click(screen.getByText('1 option(s)'));
|
userEvent.click(
|
||||||
await waitFor(() => userEvent.click(screen.getByText('Chart A')));
|
screen.getByRole('combobox', { name: 'Annotation layer value' }),
|
||||||
|
);
|
||||||
|
userEvent.click(await screen.findByText('Chart A'));
|
||||||
expect(
|
expect(
|
||||||
screen.getByText('Annotation Slice Configuration'),
|
screen.getByText('Annotation Slice Configuration'),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
|
|
||||||
userEvent.click(screen.getByRole('button', { name: 'Automatic Color' }));
|
userEvent.click(screen.getByRole('button', { name: 'Automatic Color' }));
|
||||||
userEvent.click(screen.getByLabelText('Title Column'));
|
userEvent.click(
|
||||||
|
screen.getByRole('combobox', { name: 'Annotation layer title column' }),
|
||||||
|
);
|
||||||
userEvent.click(screen.getByText('None'));
|
userEvent.click(screen.getByText('None'));
|
||||||
userEvent.click(screen.getByLabelText('Style'));
|
userEvent.click(screen.getByText('Style'));
|
||||||
|
userEvent.click(
|
||||||
|
screen.getByRole('combobox', { name: 'Annotation layer stroke' }),
|
||||||
|
);
|
||||||
userEvent.click(screen.getByText('Dashed'));
|
userEvent.click(screen.getByText('Dashed'));
|
||||||
userEvent.click(screen.getByLabelText('Opacity'));
|
userEvent.click(screen.getByText('Opacity'));
|
||||||
|
userEvent.click(
|
||||||
|
screen.getByRole('combobox', { name: 'Annotation layer opacity' }),
|
||||||
|
);
|
||||||
userEvent.click(screen.getByText('0.5'));
|
userEvent.click(screen.getByText('0.5'));
|
||||||
|
|
||||||
const checkboxes = screen.getAllByRole('checkbox');
|
const checkboxes = screen.getAllByRole('checkbox');
|
||||||
|
|
|
@ -62,12 +62,10 @@ class AnnotationLayerControl extends React.PureComponent {
|
||||||
this.state = {
|
this.state = {
|
||||||
popoverVisible: {},
|
popoverVisible: {},
|
||||||
addedAnnotationIndex: null,
|
addedAnnotationIndex: null,
|
||||||
popoverClear: false,
|
|
||||||
};
|
};
|
||||||
this.addAnnotationLayer = this.addAnnotationLayer.bind(this);
|
this.addAnnotationLayer = this.addAnnotationLayer.bind(this);
|
||||||
this.removeAnnotationLayer = this.removeAnnotationLayer.bind(this);
|
this.removeAnnotationLayer = this.removeAnnotationLayer.bind(this);
|
||||||
this.handleVisibleChange = this.handleVisibleChange.bind(this);
|
this.handleVisibleChange = this.handleVisibleChange.bind(this);
|
||||||
this.handlePopoverClear = this.handlePopoverClear.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
|
@ -105,19 +103,9 @@ class AnnotationLayerControl extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleVisibleChange(visible, popoverKey) {
|
handleVisibleChange(visible, popoverKey) {
|
||||||
if (!this.state.popoverClear) {
|
this.setState(prevState => ({
|
||||||
this.setState(prevState => ({
|
popoverVisible: { ...prevState.popoverVisible, [popoverKey]: visible },
|
||||||
popoverVisible: { ...prevState.popoverVisible, [popoverKey]: visible },
|
}));
|
||||||
}));
|
|
||||||
} else {
|
|
||||||
this.handlePopoverClear(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePopoverClear(popoverClear) {
|
|
||||||
this.setState({
|
|
||||||
popoverClear,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
removeAnnotationLayer(annotation) {
|
removeAnnotationLayer(annotation) {
|
||||||
|
@ -143,7 +131,6 @@ class AnnotationLayerControl extends React.PureComponent {
|
||||||
this.handleVisibleChange(false, popoverKey);
|
this.handleVisibleChange(false, popoverKey);
|
||||||
this.setState({ addedAnnotationIndex: null });
|
this.setState({ addedAnnotationIndex: null });
|
||||||
}}
|
}}
|
||||||
onPopoverClear={this.handlePopoverClear}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -23,7 +23,7 @@ import { InfoTooltipWithTrigger } from '@superset-ui/chart-controls';
|
||||||
|
|
||||||
import Popover from 'src/components/Popover';
|
import Popover from 'src/components/Popover';
|
||||||
import FormRow from 'src/components/FormRow';
|
import FormRow from 'src/components/FormRow';
|
||||||
import SelectControl from 'src/explore/components/controls/SelectControl';
|
import { Select } from 'src/components';
|
||||||
import CheckboxControl from 'src/explore/components/controls/CheckboxControl';
|
import CheckboxControl from 'src/explore/components/controls/CheckboxControl';
|
||||||
import TextControl from 'src/explore/components/controls/TextControl';
|
import TextControl from 'src/explore/components/controls/TextControl';
|
||||||
import { FILTER_CONFIG_ATTRIBUTES } from 'src/explore/constants';
|
import { FILTER_CONFIG_ATTRIBUTES } from 'src/explore/constants';
|
||||||
|
@ -136,12 +136,12 @@ export default class FilterBoxItemControl extends React.Component {
|
||||||
<FormRow
|
<FormRow
|
||||||
label={t('Column')}
|
label={t('Column')}
|
||||||
control={
|
control={
|
||||||
<SelectControl
|
<Select
|
||||||
|
ariaLabel={t('Column')}
|
||||||
value={this.state.column}
|
value={this.state.column}
|
||||||
name="column"
|
name="column"
|
||||||
clearable={false}
|
|
||||||
options={this.props.datasource.columns
|
options={this.props.datasource.columns
|
||||||
.filter(col => col !== this.state.column)
|
.filter(col => col.column_name !== this.state.column)
|
||||||
.map(col => ({
|
.map(col => ({
|
||||||
value: col.column_name,
|
value: col.column_name,
|
||||||
label: col.column_name,
|
label: col.column_name,
|
||||||
|
@ -184,11 +184,12 @@ export default class FilterBoxItemControl extends React.Component {
|
||||||
label={t('Sort metric')}
|
label={t('Sort metric')}
|
||||||
tooltip={t('Metric to sort the results by')}
|
tooltip={t('Metric to sort the results by')}
|
||||||
control={
|
control={
|
||||||
<SelectControl
|
<Select
|
||||||
|
ariaLabel={t('Sort metric')}
|
||||||
value={this.state.metric}
|
value={this.state.metric}
|
||||||
name="column"
|
name="column"
|
||||||
options={this.props.datasource.metrics
|
options={this.props.datasource.metrics
|
||||||
.filter(metric => metric !== this.state.metric)
|
.filter(m => m.metric_name !== this.state.metric)
|
||||||
.map(m => ({
|
.map(m => ({
|
||||||
value: m.metric_name,
|
value: m.metric_name,
|
||||||
label: m.metric_name,
|
label: m.metric_name,
|
||||||
|
|
|
@ -18,11 +18,12 @@
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { t, css } from '@superset-ui/core';
|
import { css, t } from '@superset-ui/core';
|
||||||
import { Select, CreatableSelect, OnPasteSelect } from 'src/components/Select';
|
import { Select } from 'src/components';
|
||||||
import ControlHeader from 'src/explore/components/ControlHeader';
|
import ControlHeader from 'src/explore/components/ControlHeader';
|
||||||
|
|
||||||
const propTypes = {
|
const propTypes = {
|
||||||
|
ariaLabel: PropTypes.string,
|
||||||
autoFocus: PropTypes.bool,
|
autoFocus: PropTypes.bool,
|
||||||
choices: PropTypes.array,
|
choices: PropTypes.array,
|
||||||
clearable: PropTypes.bool,
|
clearable: PropTypes.bool,
|
||||||
|
@ -30,10 +31,8 @@ const propTypes = {
|
||||||
disabled: PropTypes.bool,
|
disabled: PropTypes.bool,
|
||||||
freeForm: PropTypes.bool,
|
freeForm: PropTypes.bool,
|
||||||
isLoading: PropTypes.bool,
|
isLoading: PropTypes.bool,
|
||||||
label: PropTypes.string,
|
|
||||||
multi: PropTypes.bool,
|
multi: PropTypes.bool,
|
||||||
isMulti: PropTypes.bool,
|
isMulti: PropTypes.bool,
|
||||||
allowAll: PropTypes.bool,
|
|
||||||
name: PropTypes.string.isRequired,
|
name: PropTypes.string.isRequired,
|
||||||
onChange: PropTypes.func,
|
onChange: PropTypes.func,
|
||||||
onFocus: PropTypes.func,
|
onFocus: PropTypes.func,
|
||||||
|
@ -42,21 +41,29 @@ const propTypes = {
|
||||||
PropTypes.number,
|
PropTypes.number,
|
||||||
PropTypes.array,
|
PropTypes.array,
|
||||||
]),
|
]),
|
||||||
|
default: PropTypes.oneOfType([
|
||||||
|
PropTypes.string,
|
||||||
|
PropTypes.number,
|
||||||
|
PropTypes.array,
|
||||||
|
]),
|
||||||
showHeader: PropTypes.bool,
|
showHeader: PropTypes.bool,
|
||||||
optionRenderer: PropTypes.func,
|
optionRenderer: PropTypes.func,
|
||||||
valueRenderer: PropTypes.func,
|
|
||||||
valueKey: PropTypes.string,
|
valueKey: PropTypes.string,
|
||||||
options: PropTypes.array,
|
options: PropTypes.array,
|
||||||
placeholder: PropTypes.string,
|
placeholder: PropTypes.string,
|
||||||
noResultsText: PropTypes.string,
|
|
||||||
selectRef: PropTypes.func,
|
|
||||||
filterOption: PropTypes.func,
|
filterOption: PropTypes.func,
|
||||||
promptTextCreator: PropTypes.func,
|
|
||||||
commaChoosesOption: PropTypes.bool,
|
// ControlHeader props
|
||||||
menuPortalTarget: PropTypes.element,
|
label: PropTypes.string,
|
||||||
menuPosition: PropTypes.string,
|
renderTrigger: PropTypes.bool,
|
||||||
menuPlacement: PropTypes.string,
|
validationErrors: PropTypes.array,
|
||||||
forceOverflow: PropTypes.bool,
|
rightNode: PropTypes.node,
|
||||||
|
leftNode: PropTypes.node,
|
||||||
|
onClick: PropTypes.func,
|
||||||
|
hovered: PropTypes.bool,
|
||||||
|
tooltipOnClick: PropTypes.func,
|
||||||
|
warning: PropTypes.string,
|
||||||
|
danger: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
|
@ -73,10 +80,6 @@ const defaultProps = {
|
||||||
onFocus: () => {},
|
onFocus: () => {},
|
||||||
showHeader: true,
|
showHeader: true,
|
||||||
valueKey: 'value',
|
valueKey: 'value',
|
||||||
noResultsText: t('No results found'),
|
|
||||||
promptTextCreator: label => `Create Option ${label}`,
|
|
||||||
commaChoosesOption: true,
|
|
||||||
allowAll: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default class SelectControl extends React.PureComponent {
|
export default class SelectControl extends React.PureComponent {
|
||||||
|
@ -84,13 +87,9 @@ export default class SelectControl extends React.PureComponent {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
options: this.getOptions(props),
|
options: this.getOptions(props),
|
||||||
value: this.props.value,
|
|
||||||
};
|
};
|
||||||
this.onChange = this.onChange.bind(this);
|
this.onChange = this.onChange.bind(this);
|
||||||
this.createMetaSelectAllOption = this.createMetaSelectAllOption.bind(this);
|
this.handleFilterOptions = this.handleFilterOptions.bind(this);
|
||||||
this.select = null; // pointer to the react-select instance
|
|
||||||
this.getSelectRef = this.getSelectRef.bind(this);
|
|
||||||
this.handleKeyDownForCreate = this.handleKeyDownForCreate.bind(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
UNSAFE_componentWillReceiveProps(nextProps) {
|
UNSAFE_componentWillReceiveProps(nextProps) {
|
||||||
|
@ -105,194 +104,130 @@ export default class SelectControl extends React.PureComponent {
|
||||||
|
|
||||||
// Beware: This is acting like an on-click instead of an on-change
|
// Beware: This is acting like an on-click instead of an on-change
|
||||||
// (firing every time user chooses vs firing only if a new option is chosen).
|
// (firing every time user chooses vs firing only if a new option is chosen).
|
||||||
onChange(opt, actionMeta) {
|
onChange(val) {
|
||||||
let optionValue = this.props.multi ? [] : null;
|
|
||||||
if (opt) {
|
|
||||||
if (this.props.multi) {
|
|
||||||
opt.forEach(o => {
|
|
||||||
// select all options
|
|
||||||
if (o.meta === true) {
|
|
||||||
optionValue = this.getOptions(this.props)
|
|
||||||
.filter(x => !x.meta)
|
|
||||||
.map(x => x[this.props.valueKey]);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
optionValue.push(o[this.props.valueKey] || o);
|
|
||||||
});
|
|
||||||
} else if (opt.meta === true) {
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
optionValue = opt[this.props.valueKey];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// will eventually call `exploreReducer`: SET_FIELD_VALUE
|
// will eventually call `exploreReducer`: SET_FIELD_VALUE
|
||||||
this.props.onChange(optionValue, [], actionMeta);
|
const { valueKey } = this.props;
|
||||||
}
|
let onChangeVal = val;
|
||||||
|
|
||||||
getSelectRef(instance) {
|
if (Array.isArray(val)) {
|
||||||
this.select = instance;
|
const values = val.map(v => v?.[valueKey] || v);
|
||||||
if (this.props.selectRef) {
|
onChangeVal = values;
|
||||||
this.props.selectRef(instance);
|
|
||||||
}
|
}
|
||||||
|
if (typeof val === 'object' && val?.[valueKey]) {
|
||||||
|
onChangeVal = val[valueKey];
|
||||||
|
}
|
||||||
|
this.props.onChange(onChangeVal, []);
|
||||||
}
|
}
|
||||||
|
|
||||||
getOptions(props) {
|
getOptions(props) {
|
||||||
|
const { choices, optionRenderer, valueKey } = props;
|
||||||
let options = [];
|
let options = [];
|
||||||
if (props.options) {
|
if (props.options) {
|
||||||
options = props.options.map(x => x);
|
options = props.options.map(o => ({
|
||||||
} else if (props.choices) {
|
...o,
|
||||||
|
value: o[valueKey],
|
||||||
|
label: o.label || o[valueKey],
|
||||||
|
customLabel: optionRenderer ? optionRenderer(o) : undefined,
|
||||||
|
}));
|
||||||
|
} else if (choices) {
|
||||||
// Accepts different formats of input
|
// Accepts different formats of input
|
||||||
options = props.choices.map(c => {
|
options = choices.map(c => {
|
||||||
if (Array.isArray(c)) {
|
if (Array.isArray(c)) {
|
||||||
const [value, label] = c.length > 1 ? c : [c[0], c[0]];
|
const [value, label] = c.length > 1 ? c : [c[0], c[0]];
|
||||||
return { label, [props.valueKey]: value };
|
return {
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
if (Object.is(c)) {
|
if (Object.is(c)) {
|
||||||
return c;
|
return {
|
||||||
|
...c,
|
||||||
|
value: c[valueKey],
|
||||||
|
label: c.label || c[valueKey],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return { label: c, [props.valueKey]: c };
|
return { value: c, label: c };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// For FreeFormSelect, insert newly created values into options
|
|
||||||
if (props.freeForm && props.value) {
|
|
||||||
const existingOptionValues = new Set(options.map(c => c[props.valueKey]));
|
|
||||||
const selectedValues = Array.isArray(props.value)
|
|
||||||
? props.value
|
|
||||||
: [props.value];
|
|
||||||
selectedValues.forEach(v => {
|
|
||||||
if (!existingOptionValues.has(v)) {
|
|
||||||
// place the newly created options at the top
|
|
||||||
options.unshift({ label: v, [props.valueKey]: v });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (props.allowAll === true && props.multi === true) {
|
|
||||||
if (!this.optionsIncludesSelectAll(options)) {
|
|
||||||
options.unshift(this.createMetaSelectAllOption());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
options = options.filter(o => !this.isMetaSelectAllOption(o));
|
|
||||||
}
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleKeyDownForCreate(event) {
|
handleFilterOptions(text, option) {
|
||||||
const { key } = event;
|
const { filterOption } = this.props;
|
||||||
if (key === 'Tab' || (this.props.commaChoosesOption && key === ',')) {
|
return filterOption({ data: option }, text);
|
||||||
// simulate an Enter event
|
|
||||||
if (this.select) {
|
|
||||||
this.select.onKeyDown({ ...event, key: 'Enter' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isMetaSelectAllOption(o) {
|
|
||||||
return o.meta && o.meta === true && o.label === 'Select all';
|
|
||||||
}
|
|
||||||
|
|
||||||
optionsIncludesSelectAll(o) {
|
|
||||||
return o.findIndex(o => this.isMetaSelectAllOption(o)) >= 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
optionsRemaining() {
|
|
||||||
const { options } = this.state;
|
|
||||||
const { value } = this.props;
|
|
||||||
// if select is multi/value is array, we show the options not selected
|
|
||||||
let remainingOptions = Array.isArray(value)
|
|
||||||
? options.length - value.length
|
|
||||||
: options.length;
|
|
||||||
if (this.optionsIncludesSelectAll(options)) {
|
|
||||||
remainingOptions -= 1;
|
|
||||||
}
|
|
||||||
return remainingOptions < 0 ? 0 : remainingOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
createMetaSelectAllOption() {
|
|
||||||
const option = { label: 'Select all', meta: true };
|
|
||||||
option[this.props.valueKey] = 'Select all';
|
|
||||||
return option;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
// Tab, comma or Enter will trigger a new option created for FreeFormSelect
|
|
||||||
const {
|
const {
|
||||||
|
ariaLabel,
|
||||||
autoFocus,
|
autoFocus,
|
||||||
clearable,
|
clearable,
|
||||||
disabled,
|
disabled,
|
||||||
filterOption,
|
filterOption,
|
||||||
isLoading,
|
freeForm,
|
||||||
label,
|
|
||||||
menuPlacement,
|
|
||||||
name,
|
|
||||||
noResultsText,
|
|
||||||
onFocus,
|
|
||||||
optionRenderer,
|
|
||||||
promptTextCreator,
|
|
||||||
value,
|
|
||||||
valueKey,
|
|
||||||
valueRenderer,
|
|
||||||
forceOverflow,
|
|
||||||
menuPortalTarget,
|
|
||||||
menuPosition,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
const optionsRemaining = this.optionsRemaining();
|
|
||||||
const optionRemaingText = optionsRemaining
|
|
||||||
? t('%s option(s)', optionsRemaining)
|
|
||||||
: '';
|
|
||||||
const placeholder = this.props.placeholder || optionRemaingText;
|
|
||||||
const isMulti = this.props.isMulti || this.props.multi;
|
|
||||||
|
|
||||||
let assistiveText;
|
|
||||||
if (
|
|
||||||
isMulti &&
|
|
||||||
optionsRemaining &&
|
|
||||||
Array.isArray(this.state.value) &&
|
|
||||||
Array.isArray(value) &&
|
|
||||||
!!value.length
|
|
||||||
) {
|
|
||||||
assistiveText = optionRemaingText;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectProps = {
|
|
||||||
autoFocus,
|
|
||||||
'aria-label': label,
|
|
||||||
clearable,
|
|
||||||
disabled,
|
|
||||||
filterOption,
|
|
||||||
ignoreAccents: false,
|
|
||||||
isLoading,
|
isLoading,
|
||||||
isMulti,
|
isMulti,
|
||||||
labelKey: 'label',
|
label,
|
||||||
menuPlacement,
|
multi,
|
||||||
forceOverflow,
|
name,
|
||||||
menuPortalTarget,
|
placeholder,
|
||||||
menuPosition,
|
onFocus,
|
||||||
|
optionRenderer,
|
||||||
|
showHeader,
|
||||||
|
value,
|
||||||
|
// ControlHeader props
|
||||||
|
description,
|
||||||
|
renderTrigger,
|
||||||
|
rightNode,
|
||||||
|
leftNode,
|
||||||
|
validationErrors,
|
||||||
|
onClick,
|
||||||
|
hovered,
|
||||||
|
tooltipOnClick,
|
||||||
|
warning,
|
||||||
|
danger,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
|
const headerProps = {
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
description,
|
||||||
|
renderTrigger,
|
||||||
|
rightNode,
|
||||||
|
leftNode,
|
||||||
|
validationErrors,
|
||||||
|
onClick,
|
||||||
|
hovered,
|
||||||
|
tooltipOnClick,
|
||||||
|
warning,
|
||||||
|
danger,
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectProps = {
|
||||||
|
allowNewOptions: freeForm,
|
||||||
|
autoFocus,
|
||||||
|
ariaLabel:
|
||||||
|
ariaLabel || (typeof label === 'string' ? label : t('Select ...')),
|
||||||
|
allowClear: clearable,
|
||||||
|
disabled,
|
||||||
|
filterOption:
|
||||||
|
filterOption && typeof filterOption === 'function'
|
||||||
|
? this.handleFilterOptions
|
||||||
|
: true,
|
||||||
|
header: showHeader && <ControlHeader {...headerProps} />,
|
||||||
|
loading: isLoading,
|
||||||
|
mode: isMulti || multi ? 'multiple' : 'single',
|
||||||
name: `select-${name}`,
|
name: `select-${name}`,
|
||||||
noResultsText,
|
|
||||||
onChange: this.onChange,
|
onChange: this.onChange,
|
||||||
onFocus,
|
onFocus,
|
||||||
optionRenderer,
|
optionRenderer,
|
||||||
value,
|
|
||||||
options: this.state.options,
|
options: this.state.options,
|
||||||
placeholder,
|
placeholder,
|
||||||
assistiveText,
|
value:
|
||||||
promptTextCreator,
|
value ||
|
||||||
selectRef: this.getSelectRef,
|
(this.props.default !== undefined ? this.props.default : undefined),
|
||||||
valueKey,
|
|
||||||
valueRenderer,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let SelectComponent;
|
|
||||||
if (this.props.freeForm) {
|
|
||||||
SelectComponent = CreatableSelect;
|
|
||||||
// Don't create functions in `render` because React relies on shallow
|
|
||||||
// compare to decide weathere to rerender child components.
|
|
||||||
selectProps.onKeyDown = this.handleKeyDownForCreate;
|
|
||||||
} else {
|
|
||||||
SelectComponent = Select;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
css={theme => css`
|
css={theme => css`
|
||||||
|
@ -307,12 +242,7 @@ export default class SelectControl extends React.PureComponent {
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{this.props.showHeader && <ControlHeader {...this.props} />}
|
<Select {...selectProps} />
|
||||||
{isMulti ? (
|
|
||||||
<OnPasteSelect {...selectProps} selectWrap={SelectComponent} />
|
|
||||||
) : (
|
|
||||||
<SelectComponent {...selectProps} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -134,6 +134,7 @@ export default class SpatialControl extends React.Component {
|
||||||
renderSelect(name, type) {
|
renderSelect(name, type) {
|
||||||
return (
|
return (
|
||||||
<SelectControl
|
<SelectControl
|
||||||
|
ariaLabel={name}
|
||||||
name={name}
|
name={name}
|
||||||
choices={this.props.choices}
|
choices={this.props.choices}
|
||||||
value={this.state[name]}
|
value={this.state[name]}
|
||||||
|
|
|
@ -125,15 +125,12 @@ const groupByControl = {
|
||||||
includeTime: false,
|
includeTime: false,
|
||||||
description: t('One or many controls to group by'),
|
description: t('One or many controls to group by'),
|
||||||
optionRenderer: c => <StyledColumnOption column={c} showType />,
|
optionRenderer: c => <StyledColumnOption column={c} showType />,
|
||||||
valueRenderer: c => <StyledColumnOption column={c} />,
|
|
||||||
valueKey: 'column_name',
|
valueKey: 'column_name',
|
||||||
allowAll: true,
|
|
||||||
filterOption: ({ data: opt }, text) =>
|
filterOption: ({ data: opt }, text) =>
|
||||||
(opt.column_name &&
|
(opt.column_name &&
|
||||||
opt.column_name.toLowerCase().indexOf(text.toLowerCase()) >= 0) ||
|
opt.column_name.toLowerCase().indexOf(text.toLowerCase()) >= 0) ||
|
||||||
(opt.verbose_name &&
|
(opt.verbose_name &&
|
||||||
opt.verbose_name.toLowerCase().indexOf(text.toLowerCase()) >= 0),
|
opt.verbose_name.toLowerCase().indexOf(text.toLowerCase()) >= 0),
|
||||||
promptTextCreator: label => label,
|
|
||||||
mapStateToProps: (state, control) => {
|
mapStateToProps: (state, control) => {
|
||||||
const newState = {};
|
const newState = {};
|
||||||
if (state.datasource) {
|
if (state.datasource) {
|
||||||
|
@ -144,7 +141,6 @@ const groupByControl = {
|
||||||
}
|
}
|
||||||
return newState;
|
return newState;
|
||||||
},
|
},
|
||||||
commaChoosesOption: false,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const metrics = {
|
const metrics = {
|
||||||
|
@ -266,7 +262,7 @@ export const controls = {
|
||||||
type: 'SelectControl',
|
type: 'SelectControl',
|
||||||
freeForm: true,
|
freeForm: true,
|
||||||
label: TIME_FILTER_LABELS.granularity,
|
label: TIME_FILTER_LABELS.granularity,
|
||||||
default: 'one day',
|
default: 'P1D',
|
||||||
choices: [
|
choices: [
|
||||||
[null, 'all'],
|
[null, 'all'],
|
||||||
['PT5S', '5 seconds'],
|
['PT5S', '5 seconds'],
|
||||||
|
@ -304,7 +300,6 @@ export const controls = {
|
||||||
),
|
),
|
||||||
clearable: false,
|
clearable: false,
|
||||||
optionRenderer: c => <StyledColumnOption column={c} showType />,
|
optionRenderer: c => <StyledColumnOption column={c} showType />,
|
||||||
valueRenderer: c => <StyledColumnOption column={c} />,
|
|
||||||
valueKey: 'column_name',
|
valueKey: 'column_name',
|
||||||
mapStateToProps: state => {
|
mapStateToProps: state => {
|
||||||
const props = {};
|
const props = {};
|
||||||
|
|
Loading…
Reference in New Issue