refactor: Migrate react-select to Antd Select in Metrics and Filters popovers (#12042)

* Migrate react-select to Antd select in Metrics popover

* Fix tests

* Migrate react-select to Antd select in Filters popover

* Migrate SelectControl to Antd Select

* Add label with number of options left
This commit is contained in:
Kamil Gabryjelski 2020-12-16 06:20:10 +01:00 committed by GitHub
parent 8bda6b0bd9
commit 794d318989
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 139 additions and 95 deletions

View File

@ -59,10 +59,10 @@ const simpleCustomFilter = new AdhocFilter({
});
const options = [
{ type: 'VARCHAR(255)', column_name: 'source' },
{ type: 'VARCHAR(255)', column_name: 'target' },
{ type: 'DOUBLE', column_name: 'value' },
{ saved_metric_name: 'my_custom_metric' },
{ type: 'VARCHAR(255)', column_name: 'source', id: 1 },
{ type: 'VARCHAR(255)', column_name: 'target', id: 2 },
{ type: 'DOUBLE', column_name: 'value', id: 3 },
{ saved_metric_name: 'my_custom_metric', id: 4 },
sumValueAdhocMetric,
];
@ -91,9 +91,7 @@ describe('AdhocFilterEditPopoverSimpleTabContent', () => {
it('passes the new adhocFilter to onChange after onSubjectChange', () => {
const { wrapper, onChange } = setup();
wrapper
.instance()
.onSubjectChange({ type: 'VARCHAR(255)', column_name: 'source' });
wrapper.instance().onSubjectChange(1);
expect(onChange.calledOnce).toBe(true);
expect(onChange.lastCall.args[0]).toEqual(
simpleAdhocFilter.duplicateWith({ subject: 'source' }),
@ -102,7 +100,7 @@ describe('AdhocFilterEditPopoverSimpleTabContent', () => {
it('may alter the clause in onSubjectChange if the old clause is not appropriate', () => {
const { wrapper, onChange } = setup();
wrapper.instance().onSubjectChange(sumValueAdhocMetric);
wrapper.instance().onSubjectChange(sumValueAdhocMetric.optionName);
expect(onChange.calledOnce).toBe(true);
expect(onChange.lastCall.args[0]).toEqual(
simpleAdhocFilter.duplicateWith({

View File

@ -28,9 +28,9 @@ import AdhocMetricEditPopover from 'src/explore/components/AdhocMetricEditPopove
import { AGGREGATES } from 'src/explore/constants';
const columns = [
{ type: 'VARCHAR(255)', column_name: 'source' },
{ type: 'VARCHAR(255)', column_name: 'target' },
{ type: 'DOUBLE', column_name: 'value' },
{ type: 'VARCHAR(255)', column_name: 'source', id: 1 },
{ type: 'VARCHAR(255)', column_name: 'target', id: 2 },
{ type: 'DOUBLE', column_name: 'value', id: 3 },
];
const sumValueAdhocMetric = new AdhocMetric({
@ -68,7 +68,7 @@ describe('AdhocMetricEditPopover', () => {
it('overwrites the adhocMetric in state with onColumnChange', () => {
const { wrapper } = setup();
wrapper.instance().onColumnChange(columns[0]);
wrapper.instance().onColumnChange(columns[0].id);
expect(wrapper.state('adhocMetric')).toEqual(
sumValueAdhocMetric.duplicateWith({ column: columns[0] }),
);
@ -95,7 +95,7 @@ describe('AdhocMetricEditPopover', () => {
expect(wrapper.find(Button).find({ disabled: true })).not.toExist();
wrapper.instance().onColumnChange(null);
expect(wrapper.find(Button).find({ disabled: true })).toExist();
wrapper.instance().onColumnChange({ column: columns[0] });
wrapper.instance().onColumnChange(columns[0].id);
expect(wrapper.find(Button).find({ disabled: true })).not.toExist();
wrapper.instance().onAggregateChange(null);
expect(wrapper.find(Button).find({ disabled: true })).toExist();
@ -104,7 +104,7 @@ describe('AdhocMetricEditPopover', () => {
it('highlights save if changes are present', () => {
const { wrapper } = setup();
expect(wrapper.find(Button).find({ buttonStyle: 'primary' })).not.toExist();
wrapper.instance().onColumnChange({ column: columns[1] });
wrapper.instance().onColumnChange(columns[1].id);
expect(wrapper.find(Button).find({ buttonStyle: 'primary' })).toExist();
});

View File

@ -20,6 +20,10 @@ import { styled } from '@superset-ui/core';
import { Select as BaseSelect } from 'src/common/components';
const StyledSelect = styled(BaseSelect)`
display: block;
`;
const StyledGraySelect = styled(StyledSelect)`
&.ant-select-single {
.ant-select-selector {
height: 36px;
@ -44,3 +48,7 @@ const StyledOption = BaseSelect.Option;
export const Select = Object.assign(StyledSelect, {
Option: StyledOption,
});
export const GraySelect = Object.assign(StyledGraySelect, {
Option: StyledOption,
});

View File

@ -19,8 +19,8 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormGroup } from 'react-bootstrap';
import { Select } from 'src/components/Select';
import { t, SupersetClient } from '@superset-ui/core';
import { Select } from 'src/common/components/Select';
import { t, SupersetClient, styled } from '@superset-ui/core';
import AdhocFilter, { EXPRESSION_TYPES, CLAUSES } from '../AdhocFilter';
import adhocMetricType from '../propTypes/adhocMetricType';
@ -36,7 +36,15 @@ import {
DISABLE_INPUT_OPERATORS,
} from '../constants';
import FilterDefinitionOption from './FilterDefinitionOption';
import SelectControl from './controls/SelectControl';
const SelectWithLabel = styled(Select)`
.ant-select-selector::after {
content: '${({ labelText }) => labelText || '\\A0'}';
display: inline-block;
white-space: nowrap;
color: ${({ theme }) => theme.colors.grayscale.light1};
}
`;
const propTypes = {
adhocFilter: PropTypes.instanceOf(AdhocFilter).isRequired,
@ -92,11 +100,8 @@ export default class AdhocFilterEditPopoverSimpleTabContent extends React.Compon
};
this.selectProps = {
isMulti: false,
name: 'select-column',
labelKey: 'label',
autosize: false,
clearable: false,
showSearch: true,
};
this.menuPortalProps = {
@ -116,7 +121,11 @@ export default class AdhocFilterEditPopoverSimpleTabContent extends React.Compon
}
}
onSubjectChange(option) {
onSubjectChange(id) {
const option = this.props.options.find(
option => option.id === id || option.optionName === id,
);
let subject;
let clause;
// infer the new clause based on what subject was selected.
@ -247,27 +256,38 @@ export default class AdhocFilterEditPopoverSimpleTabContent extends React.Compon
}
}
optionsRemaining() {
const { suggestions } = this.state;
const { comparator } = this.props.adhocFilter;
// if select is multi/value is array, we show the options not selected
const valuesFromSuggestionsLength = Array.isArray(comparator)
? comparator.filter(v => suggestions.includes(v)).length
: 0;
return suggestions?.length - valuesFromSuggestionsLength ?? 0;
}
createSuggestionsPlaceholder() {
const optionsRemaining = this.optionsRemaining();
const placeholder = t('%s option(s)', optionsRemaining);
return optionsRemaining ? placeholder : '';
}
renderSubjectOptionLabel(option) {
return <FilterDefinitionOption option={option} />;
}
renderSubjectOptionValue({ value }) {
return <span>{value}</span>;
}
render() {
const { adhocFilter, options: columns, datasource } = this.props;
const { adhocFilter, options, datasource } = this.props;
let columns = options;
const { subject, operator, comparator } = adhocFilter;
const subjectSelectProps = {
options: columns,
value: subject ? { value: subject } : undefined,
value: subject ?? undefined,
onChange: this.onSubjectChange,
optionRenderer: this.renderSubjectOptionLabel,
valueRenderer: this.renderSubjectOptionValue,
valueKey: 'filterOptionName',
noResultsText: t(
notFoundContent: t(
'No such column found. To filter on a metric, try the Custom SQL tab.',
),
filterOption: (input, option) =>
option.filterBy.toLowerCase().indexOf(input.toLowerCase()) >= 0,
};
if (datasource.type === 'druid') {
@ -283,19 +303,16 @@ export default class AdhocFilterEditPopoverSimpleTabContent extends React.Compon
adhocFilter.clause === CLAUSES.WHERE
? t('%s column(s)', columns.length)
: t('To filter on a metric, use Custom SQL tab.');
// make sure options have `column_name`
subjectSelectProps.options = columns.filter(option => option.column_name);
columns = options.filter(option => option.column_name);
}
const operatorSelectProps = {
placeholder: t('%s operators(s)', OPERATORS_OPTIONS.length),
// like AGGREGTES_OPTIONS, operator options are string
options: OPERATORS_OPTIONS.filter(op =>
this.isOperatorRelevant(op, subject),
),
value: operator,
onChange: this.onOperatorChange,
getOptionLabel: translateOperator,
filterOption: (input, option) =>
option.value.toLowerCase().indexOf(input.toLowerCase()) >= 0,
};
return (
@ -303,36 +320,63 @@ export default class AdhocFilterEditPopoverSimpleTabContent extends React.Compon
<FormGroup className="adhoc-filter-simple-column-dropdown">
<Select
{...this.selectProps}
{...this.menuPortalProps}
{...subjectSelectProps}
name="filter-column"
/>
>
{columns.map(column => (
<Select.Option
value={column.id || column.optionName}
filterBy={
column.saved_metric_name || column.column_name || column.label
}
key={column.id}
>
{this.renderSubjectOptionLabel(column)}
</Select.Option>
))}
</Select>
</FormGroup>
<FormGroup>
<Select
{...this.selectProps}
{...this.menuPortalProps}
{...operatorSelectProps}
name="filter-operator"
/>
>
{OPERATORS_OPTIONS.filter(op =>
this.isOperatorRelevant(op, subject),
).map(option => (
<Select.Option value={option} key={option}>
{translateOperator(option)}
</Select.Option>
))}
</Select>
</FormGroup>
<FormGroup data-test="adhoc-filter-simple-value">
{MULTI_OPERATORS.has(operator) ||
this.state.suggestions.length > 0 ? (
<SelectControl
{...this.menuPortalProps}
<SelectWithLabel
name="filter-value"
autoFocus
freeForm
multi={MULTI_OPERATORS.has(operator)}
allowClear
showSearch
mode={MULTI_OPERATORS.has(operator) && 'tags'}
tokenSeparators={[',', ' ', ';']}
loading={this.state.loading}
value={comparator}
isLoading={this.state.loading}
choices={this.state.suggestions}
onChange={this.onComparatorChange}
showHeader={false}
noResultsText={t('type a value here')}
notFoundContent={t('type a value here')}
disabled={DISABLE_INPUT_OPERATORS.includes(operator)}
/>
placeholder={this.createSuggestionsPlaceholder()}
labelText={
comparator?.length > 0 && this.createSuggestionsPlaceholder()
}
>
{this.state.suggestions.map(suggestion => (
<Select.Option value={suggestion} key={suggestion}>
{suggestion}
</Select.Option>
))}
</SelectWithLabel>
) : (
<input
name="filter-value"

View File

@ -19,7 +19,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormGroup } from 'react-bootstrap';
import Select from 'src/components/Select';
import { Select } from 'src/common/components/Select';
import { t } from '@superset-ui/core';
import { SQLEditor } from 'src/components/AsyncAceEditor';
import sqlKeywords from 'src/SqlLab/utils/sqlKeywords';
@ -51,11 +51,7 @@ export default class AdhocFilterEditPopoverSqlTabContent extends React.Component
this.handleAceEditorRef = this.handleAceEditorRef.bind(this);
this.selectProps = {
isMulti: false,
name: 'select-column',
labelKey: 'label',
autosize: false,
clearable: false,
};
}
@ -94,7 +90,6 @@ export default class AdhocFilterEditPopoverSqlTabContent extends React.Component
const clauseSelectProps = {
placeholder: t('choose WHERE or HAVING...'),
options: Object.keys(CLAUSES),
value: adhocFilter.clause,
onChange: this.onSqlExpressionClauseChange,
};
@ -121,7 +116,13 @@ export default class AdhocFilterEditPopoverSqlTabContent extends React.Component
{...this.selectProps}
{...clauseSelectProps}
className="filter-edit-clause-dropdown"
/>
>
{Object.keys(CLAUSES).map(clause => (
<Select.Option value={clause} key={clause}>
{clause}
</Select.Option>
))}
</Select>
<span className="filter-edit-clause-info">
<strong>WHERE</strong> {t('filters by columns')}
<br />

View File

@ -21,7 +21,7 @@ import PropTypes from 'prop-types';
import { FormGroup } from 'react-bootstrap';
import Tabs from 'src/common/components/Tabs';
import Button from 'src/components/Button';
import Select from 'src/components/Select';
import { Select } from 'src/common/components/Select';
import { styled, t } from '@superset-ui/core';
import { ColumnOption } from '@superset-ui/chart-controls';
@ -70,27 +70,12 @@ export default class AdhocMetricEditPopover extends React.Component {
this.handleAceEditorRef = this.handleAceEditorRef.bind(this);
this.refreshAceEditor = this.refreshAceEditor.bind(this);
this.popoverRef = React.createRef();
this.state = {
adhocMetric: this.props.adhocMetric,
width: startingWidth,
height: startingHeight,
};
this.selectProps = {
labelKey: 'label',
isMulti: false,
autosize: false,
clearable: true,
};
this.menuPortalProps = {
menuPosition: 'fixed',
menuPlacement: 'bottom',
menuPortalTarget: this.popoverRef.current,
};
document.addEventListener('mouseup', this.onMouseUp);
}
@ -118,7 +103,8 @@ export default class AdhocMetricEditPopover extends React.Component {
this.props.onClose();
}
onColumnChange(column) {
onColumnChange(columnId) {
const column = this.props.columns.find(column => column.id === columnId);
this.setState(prevState => ({
adhocMetric: prevState.adhocMetric.duplicateWith({
column,
@ -213,20 +199,23 @@ export default class AdhocMetricEditPopover extends React.Component {
const columnSelectProps = {
placeholder: t('%s column(s)', columns.length),
options: columns,
value:
(adhocMetric.column && adhocMetric.column.column_name) ||
adhocMetric.inferSqlExpressionColumn(),
onChange: this.onColumnChange,
optionRenderer: this.renderColumnOption,
valueKey: 'column_name',
allowClear: true,
showSearch: true,
filterOption: (input, option) =>
option.filterBy.toLowerCase().indexOf(input.toLowerCase()) >= 0,
};
const aggregateSelectProps = {
placeholder: t('%s aggregates(s)', AGGREGATES_OPTIONS.length),
options: AGGREGATES_OPTIONS,
value: adhocMetric.aggregate || adhocMetric.inferSqlExpressionAggregate(),
onChange: this.onAggregateChange,
allowClear: true,
autoFocus: true,
showSearch: true,
};
if (this.props.datasourceType === 'druid') {
@ -241,7 +230,6 @@ export default class AdhocMetricEditPopover extends React.Component {
<div
id="metrics-edit-popover"
data-test="metrics-edit-popover"
ref={this.popoverRef}
{...popoverProps}
>
<Tabs
@ -261,24 +249,29 @@ export default class AdhocMetricEditPopover extends React.Component {
<FormLabel>
<strong>column</strong>
</FormLabel>
<Select
name="select-column"
{...this.selectProps}
{...this.menuPortalProps}
{...columnSelectProps}
/>
<Select name="select-column" {...columnSelectProps}>
{columns.map(column => (
<Select.Option
value={column.id}
filterBy={column.verbose_name || column.column_name}
key={column.id}
>
{this.renderColumnOption(column)}
</Select.Option>
))}
</Select>
</FormGroup>
<FormGroup>
<FormLabel>
<strong>aggregate</strong>
</FormLabel>
<Select
name="select-aggregate"
{...this.selectProps}
{...this.menuPortalProps}
{...aggregateSelectProps}
autoFocus
/>
<Select name="select-aggregate" {...aggregateSelectProps}>
{AGGREGATES_OPTIONS.map(option => (
<Select.Option value={option} key={option}>
{option}
</Select.Option>
))}
</Select>
</FormGroup>
</Tabs.TabPane>
<Tabs.TabPane

View File

@ -24,7 +24,7 @@ import { useSingleViewResource } from 'src/views/CRUD/hooks';
import Icon from 'src/components/Icon';
import Modal from 'src/common/components/Modal';
import { Switch } from 'src/common/components/Switch';
import { Select } from 'src/common/components/Select';
import { GraySelect as Select } from 'src/common/components/Select';
import { Radio } from 'src/common/components/Radio';
import { AsyncSelect } from 'src/components/Select';
import withToasts from 'src/messageToasts/enhancers/withToasts';