diff --git a/superset-frontend/src/components/Select/utils.ts b/superset-frontend/src/components/Select/utils.ts index 18f1e16c05f..54a7caf9b18 100644 --- a/superset-frontend/src/components/Select/utils.ts +++ b/superset-frontend/src/components/Select/utils.ts @@ -62,10 +62,13 @@ export function findValue( export function hasOption(search: string, options: AntdOptionsType) { const searchOption = search.trim().toLowerCase(); - return options.find( - opt => - opt.value.toLowerCase().includes(searchOption) || - (typeof opt.label === 'string' && - opt.label.toLowerCase().includes(searchOption)), - ); + return options.find(opt => { + const { label, value } = opt; + const labelText = String(label); + const valueText = String(value); + return ( + valueText.toLowerCase().includes(searchOption) || + labelText.toLowerCase().includes(searchOption) + ); + }); } diff --git a/superset-frontend/src/components/SupersetResourceSelect/SupersetResourceSelect.test.tsx b/superset-frontend/src/components/SupersetResourceSelect/SupersetResourceSelect.test.tsx deleted file mode 100644 index ad5d688030a..00000000000 --- a/superset-frontend/src/components/SupersetResourceSelect/SupersetResourceSelect.test.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import React from 'react'; -import { render, screen } from 'spec/helpers/testing-library'; -import userEvent from '@testing-library/user-event'; -import fetchMock from 'fetch-mock'; -import SupersetResourceSelect from '.'; - -const mockedProps = { - resource: 'dataset', - searchColumn: 'table_name', - onError: () => {}, -}; - -fetchMock.get('glob:*/api/v1/dataset/?q=*', {}); - -test('should render', () => { - const { container } = render(); - expect(container).toBeInTheDocument(); -}); - -test('should render the Select... placeholder', () => { - render(); - expect(screen.getByText('Select...')).toBeInTheDocument(); -}); - -test('should render the Loading... message', () => { - render(); - const select = screen.getByText('Select...'); - userEvent.click(select); - expect(screen.getByText('Loading...')).toBeInTheDocument(); -}); - -test('should render the No options message', async () => { - render(); - const select = screen.getByText('Select...'); - userEvent.click(select); - expect(await screen.findByText('No options')).toBeInTheDocument(); -}); - -test('should render the typed text', async () => { - render(); - const select = screen.getByText('Select...'); - userEvent.click(select); - userEvent.type(select, 'typed text'); - expect(await screen.findByText('typed text')).toBeInTheDocument(); -}); diff --git a/superset-frontend/src/components/SupersetResourceSelect/index.tsx b/superset-frontend/src/components/SupersetResourceSelect/index.tsx deleted file mode 100644 index d11b365059e..00000000000 --- a/superset-frontend/src/components/SupersetResourceSelect/index.tsx +++ /dev/null @@ -1,121 +0,0 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -import React, { useEffect } from 'react'; -import rison from 'rison'; -import { SupersetClient } from '@superset-ui/core'; -import { AsyncSelect } from 'src/components/Select'; -import { - ClientErrorObject, - getClientErrorObject, -} from 'src/utils/getClientErrorObject'; -import { cacheWrapper } from 'src/utils/cacheWrapper'; - -export type Value = { value: V; label: string }; - -export interface SupersetResourceSelectProps { - value?: Value | null; - initialId?: number | string; - onChange?: (value?: Value) => void; - isMulti?: boolean; - searchColumn?: string; - resource?: string; // e.g. "dataset", "dashboard/related/owners" - transformItem?: (item: T) => Value; - onError: (error: ClientErrorObject) => void; - defaultOptions?: { value: number; label: string }[] | boolean; -} - -/** - * This is a special-purpose select component for when you're selecting - * items from one of the standard Superset resource APIs. - * Such as selecting a datasource, a chart, or users. - * - * If you're selecting a "related" resource (such as dashboard/related/owners), - * leave the searchColumn prop unset. - * The api doesn't do columns on related resources for some reason. - * - * If you're doing anything more complex than selecting a standard resource, - * we'll all be better off if you use AsyncSelect directly instead. - */ - -const localCache = new Map(); - -export const cachedSupersetGet = cacheWrapper( - SupersetClient.get, - localCache, - ({ endpoint }) => endpoint || '', -); - -export default function SupersetResourceSelect({ - value, - initialId, - onChange, - isMulti, - resource, - searchColumn, - transformItem, - onError, - defaultOptions = true, -}: SupersetResourceSelectProps) { - useEffect(() => { - if (initialId == null) return; - cachedSupersetGet({ - endpoint: `/api/v1/${resource}/${initialId}`, - }) - .then(response => { - const { result } = response.json; - const value = transformItem ? transformItem(result) : result; - if (onChange) onChange(value); - }) - .catch(response => { - if (response?.status === 404 && onChange) onChange(undefined); - }); - }, [resource, initialId]); // eslint-disable-line react-hooks/exhaustive-deps - - function loadOptions(input: string) { - const query = searchColumn - ? rison.encode({ - filters: [{ col: searchColumn, opr: 'ct', value: input }], - }) - : rison.encode({ filter: value }); - return cachedSupersetGet({ - endpoint: `/api/v1/${resource}/?q=${query}`, - }).then( - response => - response.json.result - .map(transformItem) - .sort((a: Value, b: Value) => a.label.localeCompare(b.label)), - async badResponse => { - onError(await getClientErrorObject(badResponse)); - return []; - }, - ); - } - - return ( - - ); -} diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.tsx index 5a05119e54c..8fc4cc4b9cd 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/ColumnSelect.tsx @@ -20,7 +20,7 @@ import React, { useCallback, useState, useMemo, useEffect } from 'react'; import { FormInstance } from 'antd/lib/form'; import { Column, ensureIsArray, SupersetClient, t } from '@superset-ui/core'; import { useChangeEffect } from 'src/common/hooks/useChangeEffect'; -import { Select } from 'src/common/components'; +import { Select } from 'src/components'; import { useToasts } from 'src/messageToasts/enhancers/withToasts'; import { getClientErrorObject } from 'src/utils/getClientErrorObject'; import { cacheWrapper } from 'src/utils/cacheWrapper'; @@ -36,7 +36,7 @@ interface ColumnSelectProps { datasetId?: number; value?: string | string[]; onChange?: (value: string) => void; - mode?: 'multiple' | 'tags'; + mode?: 'multiple'; } const localCache = new Map(); @@ -128,6 +128,7 @@ export function ColumnSelect({ + ); +}; + +const MemoizedSelect = (props: DatasetSelectProps) => + // eslint-disable-next-line react-hooks/exhaustive-deps + useMemo(() => , []); + +export default MemoizedSelect; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx index 61115312dda..ae27d16e097 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx @@ -27,6 +27,7 @@ import { styled, SupersetApiError, t, + SupersetClient, } from '@superset-ui/core'; import { ColumnMeta, @@ -45,15 +46,12 @@ import React, { import { useSelector } from 'react-redux'; import { FormItem } from 'src/components/Form'; import { Input } from 'src/common/components'; -import { Select } from 'src/components/Select'; -import SupersetResourceSelect, { - cachedSupersetGet, -} from 'src/components/SupersetResourceSelect'; +import { Select } from 'src/components'; +import { cacheWrapper } from 'src/utils/cacheWrapper'; import AdhocFilterControl from 'src/explore/components/controls/FilterControl/AdhocFilterControl'; import DateFilterControl from 'src/explore/components/controls/DateFilterControl'; import { addDangerToast } from 'src/messageToasts/actions'; import { ClientErrorObject } from 'src/utils/getClientErrorObject'; -import SelectControl from 'src/explore/components/controls/SelectControl'; import Collapse from 'src/components/Collapse'; import { getChartDataRequest } from 'src/chart/chartAction'; import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; @@ -61,6 +59,7 @@ import { waitForAsyncData } from 'src/middleware/asyncEvent'; import Tabs from 'src/components/Tabs'; import Icons from 'src/components/Icons'; import { Tooltip } from 'src/components/Tooltip'; +import { Radio } from 'src/components/Radio'; import BasicErrorAlert from 'src/components/ErrorMessage/BasicErrorAlert'; import { Chart, @@ -68,14 +67,15 @@ import { DatasourcesState, RootState, } from 'src/dashboard/types'; +import Loading from 'src/components/Loading'; import { ColumnSelect } from './ColumnSelect'; import { NativeFiltersForm } from '../types'; import { - datasetToSelectOption, FILTER_SUPPORTED_TYPES, hasTemporalColumns, setNativeFilterFieldValues, useForceUpdate, + mostUsedDataset, } from './utils'; import { useBackendFormUpdate, useDefaultValue } from './state'; import { getFormData } from '../../utils'; @@ -89,6 +89,7 @@ import { CASCADING_FILTERS, getFiltersConfigModalTestId, } from '../FiltersConfigModal'; +import DatasetSelect from './DatasetSelect'; const { TabPane } = Tabs; @@ -282,6 +283,14 @@ const FILTER_TYPE_NAME_MAPPING = { [t('Group By')]: t('Group by'), }; +const localCache = new Map(); + +const cachedSupersetGet = cacheWrapper( + SupersetClient.get, + localCache, + ({ endpoint }) => endpoint || '', +); + /** * The configuration form for a specific filter. * Assigns field values to `filters[filterId]` in the form. @@ -323,6 +332,7 @@ const FiltersConfigForm = ( const loadedDatasets = useSelector( ({ datasources }) => datasources, ); + const charts = useSelector(({ charts }) => charts); const doLoadedDatasetsHaveTemporalColumns = useMemo( @@ -481,14 +491,10 @@ const FiltersConfigForm = ( [filterId, forceUpdate, form, formFilter, hasDataset], ); - const defaultDatasetSelectOptions = Object.values(loadedDatasets).map( - datasetToSelectOption, - ); const initialDatasetId = filterToEdit?.targets[0]?.datasetId ?? - (defaultDatasetSelectOptions.length === 1 - ? defaultDatasetSelectOptions[0].value - : undefined); + mostUsedDataset(loadedDatasets, charts); + const newFormData = getFormData({ datasetId, groupby: hasColumn ? formFilter?.column : undefined, @@ -515,17 +521,6 @@ const FiltersConfigForm = ( refreshHandler, ]); - const onDatasetSelectError = useCallback( - ({ error, message }: ClientErrorObject) => { - let errorText = message || error || t('An error has occurred'); - if (message === 'Forbidden') { - errorText = t('You do not have permission to edit this dashboard'); - } - addDangerToast(errorText); - }, - [], - ); - const updateFormValues = useCallback( (values: any) => setNativeFilterFieldValues(form, filterId, values), [filterId, form], @@ -548,6 +543,11 @@ const FiltersConfigForm = ( const hasSorting = typeof filterToEdit?.controlValues?.sortAscending === 'boolean'; + let sort = filterToEdit?.controlValues?.sortAscending; + if (typeof formFilter?.controlValues?.sortAscending === 'boolean') { + sort = formFilter.controlValues.sortAscending; + } + const showDefaultValue = !hasDataset || (!isDataDirty && hasFilledDataset) || @@ -625,6 +625,22 @@ const FiltersConfigForm = ( JSON.stringify(loadedDatasets), ]); + const ParentSelect = ({ + value, + ...rest + }: { + value?: { value: string | number }; + }) => ( + { // @ts-ignore const name = nativeFilterItems[filterType]?.value.name; @@ -680,10 +697,10 @@ const FiltersConfigForm = ( ) : ( mappedName || name ), - isDisabled, + disabled: isDisabled, }; })} - onChange={({ value }: { value: string }) => { + onChange={value => { setNativeFilterFieldValues(form, filterId, { filterType: value, defaultDataMask: null, @@ -705,27 +722,25 @@ const FiltersConfigForm = ( ]} {...getFiltersConfigModalTestId('datasource-input')} > - { - // We need reset column when dataset changed - if (datasetId && e?.value !== datasetId) { - setNativeFilterFieldValues(form, filterId, { - defaultDataMask: null, - column: null, - }); - } - forceUpdate(); - }} - /> + {!datasetId || !hasColumn || datasetDetails ? ( + { + // We need to reset the column when the dataset has changed + if (value !== datasetId) { + setNativeFilterFieldValues(form, filterId, { + dataset: { value }, + defaultDataMask: null, + column: null, + }); + } + forceUpdate(); + }} + /> + ) : ( + + )} {hasDataset && Object.keys(mainControlItems).map( @@ -851,6 +866,7 @@ const FiltersConfigForm = ( name={['filters', filterId, 'parentFilter']} label={{t('Parent filter')}} initialValue={parentFilter} + normalize={value => (value ? { value } : undefined)} data-test="parent-filter-input" required rules={[ @@ -860,11 +876,7 @@ const FiltersConfigForm = ( }, ]} > - - onSortChanged(value) - } - /> + { + onSortChanged(value.target.value); + }} + > + {t('Sort ascending')} + {t('Sort descending')} + {hasMetrics && ( - ({ value: metric.metric_name, label: metric.verbose_name ?? metric.metric_name, }))} - onChange={(value: string | null): void => { + onChange={value => { if (value !== undefined) { setNativeFilterFieldValues(form, filterId, { sortMetric: value, diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx index 45c685de23f..24a938cb5f6 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx @@ -101,7 +101,6 @@ export default function getControlItemsMap({ /> !column.type_generic || !(filterType in FILTER_SUPPORTED_TYPES) || FILTER_SUPPORTED_TYPES[filterType]?.includes(column.type_generic); + +export const mostUsedDataset = ( + datasets: DatasourcesState, + charts: ChartsState, +) => { + const map = new Map(); + let mostUsedDataset = ''; + let maxCount = 0; + + Object.values(charts).forEach(chart => { + const { formData } = chart; + if (formData) { + const { datasource } = formData; + const count = (map.get(datasource) || 0) + 1; + map.set(datasource, count); + + if (count > maxCount) { + maxCount = count; + mostUsedDataset = datasource; + } + } + }); + + return datasets[mostUsedDataset]?.id; +}; diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx index c9982a7b920..e3b04c254f0 100644 --- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx +++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.test.tsx @@ -29,6 +29,7 @@ import { } from 'src/filters/components'; import { render, screen, waitFor } from 'spec/helpers/testing-library'; import mockDatasource, { datasourceId } from 'spec/fixtures/mockDatasource'; +import chartQueries from 'spec/fixtures/mockChartQueries'; import { FiltersConfigModal, FiltersConfigModalProps, @@ -49,19 +50,25 @@ class MainPreset extends Preset { } } -const initialStoreState = { - datasources: mockDatasource, -}; +const defaultState = () => ({ + datasources: { ...mockDatasource }, + charts: chartQueries, +}); -const storeWithDatasourceWithoutTemporalColumns = { - ...initialStoreState, - datasources: { - ...initialStoreState.datasources, - [datasourceId]: { - ...initialStoreState.datasources[datasourceId], - column_types: [0, 1], +const noTemporalColumnsState = () => { + const state = defaultState(); + return { + charts: { + ...state.charts, }, - }, + datasources: { + ...state.datasources, + [datasourceId]: { + ...state.datasources[datasourceId], + column_types: [0, 1], + }, + }, + }; }; fetchMock.get('glob:*/api/v1/dataset/1', { @@ -138,11 +145,8 @@ beforeAll(() => { new MainPreset().register(); }); -function defaultRender( - overrides?: Partial, - initialState = initialStoreState, -) { - return render(, { +function defaultRender(initialState = defaultState()) { + return render(, { useRedux: true, initialState, }); @@ -178,11 +182,13 @@ test('renders a value filter type', () => { expect(getCheckbox(MULTIPLE_REGEX)).toBeChecked(); }); -test('renders a numerical range filter type', () => { +test('renders a numerical range filter type', async () => { defaultRender(); userEvent.click(screen.getByText(VALUE_REGEX)); - userEvent.click(screen.getByText(NUMERICAL_RANGE_REGEX)); + + await waitFor(() => userEvent.click(screen.getByText(NUMERICAL_RANGE_REGEX))); + userEvent.click(screen.getByText(ADVANCED_REGEX)); expect(screen.getByText(FILTER_TYPE_REGEX)).toBeInTheDocument(); @@ -202,11 +208,12 @@ test('renders a numerical range filter type', () => { expect(queryCheckbox(SORT_REGEX)).not.toBeInTheDocument(); }); -test('renders a time range filter type', () => { +test('renders a time range filter type', async () => { defaultRender(); userEvent.click(screen.getByText(VALUE_REGEX)); - userEvent.click(screen.getByText(TIME_RANGE_REGEX)); + + await waitFor(() => userEvent.click(screen.getByText(TIME_RANGE_REGEX))); expect(screen.getByText(FILTER_TYPE_REGEX)).toBeInTheDocument(); expect(screen.getByText(FILTER_NAME_REGEX)).toBeInTheDocument(); @@ -218,11 +225,12 @@ test('renders a time range filter type', () => { expect(screen.queryByText(ADVANCED_REGEX)).not.toBeInTheDocument(); }); -test('renders a time column filter type', () => { +test('renders a time column filter type', async () => { defaultRender(); userEvent.click(screen.getByText(VALUE_REGEX)); - userEvent.click(screen.getByText(TIME_COLUMN_REGEX)); + + await waitFor(() => userEvent.click(screen.getByText(TIME_COLUMN_REGEX))); expect(screen.getByText(FILTER_TYPE_REGEX)).toBeInTheDocument(); expect(screen.getByText(FILTER_NAME_REGEX)).toBeInTheDocument(); @@ -234,11 +242,12 @@ test('renders a time column filter type', () => { expect(screen.queryByText(ADVANCED_REGEX)).not.toBeInTheDocument(); }); -test('renders a time grain filter type', () => { +test('renders a time grain filter type', async () => { defaultRender(); userEvent.click(screen.getByText(VALUE_REGEX)); - userEvent.click(screen.getByText(TIME_GRAIN_REGEX)); + + await waitFor(() => userEvent.click(screen.getByText(TIME_GRAIN_REGEX))); expect(screen.getByText(FILTER_TYPE_REGEX)).toBeInTheDocument(); expect(screen.getByText(FILTER_NAME_REGEX)).toBeInTheDocument(); @@ -250,18 +259,19 @@ test('renders a time grain filter type', () => { expect(screen.queryByText(ADVANCED_REGEX)).not.toBeInTheDocument(); }); -test('render time filter types as disabled if there are no temporal columns in the dataset', () => { - defaultRender(undefined, storeWithDatasourceWithoutTemporalColumns); +test('render time filter types as disabled if there are no temporal columns in the dataset', async () => { + defaultRender(noTemporalColumnsState()); + userEvent.click(screen.getByText(VALUE_REGEX)); - expect(screen.getByText(TIME_RANGE_REGEX).closest('div')).toHaveClass( - 'Select__option--is-disabled', - ); - expect(screen.getByText(TIME_GRAIN_REGEX).closest('div')).toHaveClass( - 'Select__option--is-disabled', - ); - expect(screen.getByText(TIME_COLUMN_REGEX).closest('div')).toHaveClass( - 'Select__option--is-disabled', - ); + + const timeRange = await screen.findByText(TIME_RANGE_REGEX); + const timeGrain = await screen.findByText(TIME_GRAIN_REGEX); + const timeColumn = await screen.findByText(TIME_COLUMN_REGEX); + const disabledClass = '.ant-select-item-option-disabled'; + + expect(timeRange.closest(disabledClass)).toBeInTheDocument(); + expect(timeGrain.closest(disabledClass)).toBeInTheDocument(); + expect(timeColumn.closest(disabledClass)).toBeInTheDocument(); }); test('validates the name', async () => { @@ -278,7 +288,7 @@ test('validates the column', async () => { // eslint-disable-next-line jest/no-disabled-tests test.skip('validates the default value', async () => { - defaultRender(undefined, initialStoreState); + defaultRender(noTemporalColumnsState()); expect(await screen.findByText('birth_names')).toBeInTheDocument(); userEvent.type(screen.getByRole('combobox'), `Column A${specialChars.enter}`); userEvent.click(getCheckbox(DEFAULT_VALUE_REGEX)); @@ -309,12 +319,14 @@ test('validates the pre-filter value', async () => { }); test("doesn't render time range pre-filter if there are no temporal columns in datasource", async () => { - defaultRender(undefined, storeWithDatasourceWithoutTemporalColumns); + defaultRender(noTemporalColumnsState()); userEvent.click(screen.getByText(ADVANCED_REGEX)); userEvent.click(getCheckbox(PRE_FILTER_REGEX)); - expect( - screen.queryByText(TIME_RANGE_PREFILTER_REGEX), - ).not.toBeInTheDocument(); + await waitFor(() => + expect( + screen.queryByText(TIME_RANGE_PREFILTER_REGEX), + ).not.toBeInTheDocument(), + ); }); /* TODO diff --git a/superset-frontend/src/filters/components/GroupBy/GroupByFilterPlugin.tsx b/superset-frontend/src/filters/components/GroupBy/GroupByFilterPlugin.tsx index 32fa094891c..b8452cac442 100644 --- a/superset-frontend/src/filters/components/GroupBy/GroupByFilterPlugin.tsx +++ b/superset-frontend/src/filters/components/GroupBy/GroupByFilterPlugin.tsx @@ -18,13 +18,11 @@ */ import { ensureIsArray, ExtraFormData, t, tn } from '@superset-ui/core'; import React, { useEffect, useState } from 'react'; -import { Select } from 'src/common/components'; import { FormItemProps } from 'antd/lib/form'; -import { Styles, StyledSelect, StyledFormItem, StatusMessage } from '../common'; +import { Select } from 'src/components'; +import { FilterPluginStyle, StyledFormItem, StatusMessage } from '../common'; import { PluginFilterGroupByProps } from './types'; -const { Option } = Select; - export default function PluginFilterGroupBy(props: PluginFilterGroupByProps) { const { data, @@ -90,13 +88,22 @@ export default function PluginFilterGroupBy(props: PluginFilterGroupByProps) { ); } + const options = columns.map( + (row: { column_name: string; verbose_name: string | null }) => { + const { column_name: columnName, verbose_name: verboseName } = row; + return { + label: verboseName ?? columnName, + value: columnName, + }; + }, + ); return ( - + - - {columns.map( - (row: { column_name: string; verbose_name: string | null }) => { - const { - column_name: columnName, - verbose_name: verboseName, - } = row; - return ( - - ); - }, - )} - + options={options} + /> - + ); } diff --git a/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx index 462fc4a292f..2cd8129edcf 100644 --- a/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx +++ b/superset-frontend/src/filters/components/Range/RangeFilterPlugin.tsx @@ -27,7 +27,7 @@ import { Slider } from 'src/common/components'; import { rgba } from 'emotion-rgba'; import { FormItemProps } from 'antd/lib/form'; import { PluginFilterRangeProps } from './types'; -import { StatusMessage, StyledFormItem, Styles } from '../common'; +import { StatusMessage, StyledFormItem, FilterPluginStyle } from '../common'; import { getRangeExtraFormData } from '../../utils'; const Wrapper = styled.div<{ validateStatus?: 'error' | 'warning' | 'info' }>` @@ -169,7 +169,7 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) { ); } return ( - + {Number.isNaN(Number(min)) || Number.isNaN(Number(max)) ? (

{t('Chosen non-numeric column')}

) : ( @@ -196,6 +196,6 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
)} - + ); } diff --git a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx index cac3da8f540..4865501e1a8 100644 --- a/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx +++ b/superset-frontend/src/filters/components/Select/SelectFilterPlugin.tsx @@ -20,7 +20,6 @@ import { AppSection, DataMask, - DataRecord, ensureIsArray, ExtraFormData, GenericDataType, @@ -29,27 +28,16 @@ import { t, tn, } from '@superset-ui/core'; -import React, { - RefObject, - ReactElement, - useCallback, - useEffect, - useMemo, - useState, -} from 'react'; -import { Select } from 'src/common/components'; +import React, { useCallback, useEffect, useState, useMemo } from 'react'; +import { Select } from 'src/components'; import debounce from 'lodash/debounce'; import { SLOW_DEBOUNCE } from 'src/constants'; import { useImmerReducer } from 'use-immer'; -import Icons from 'src/components/Icons'; -import { usePrevious } from 'src/common/hooks/usePrevious'; import { FormItemProps } from 'antd/lib/form'; import { PluginFilterSelectProps, SelectValue } from './types'; -import { StyledFormItem, StyledSelect, Styles, StatusMessage } from '../common'; +import { StyledFormItem, FilterPluginStyle, StatusMessage } from '../common'; import { getDataRecordFormatter, getSelectExtraFormData } from '../../utils'; -const { Option } = Select; - type DataMaskAction = | { type: 'ownState'; ownState: JsonObject } | { @@ -107,25 +95,6 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) { const groupby = ensureIsArray(formData.groupby); const [col] = groupby; const [initialColtypeMap] = useState(coltypeMap); - const [selectedValues, setSelectedValues] = useState( - filterState.value, - ); - const sortedData = useMemo(() => { - const firstData: DataRecord[] = []; - const restData: DataRecord[] = []; - data.forEach(row => { - // @ts-ignore - if (selectedValues?.includes(row[col])) { - firstData.push(row); - } else { - restData.push(row); - } - }); - return [...firstData, ...restData]; - }, [col, selectedValues, data]); - const [isDropdownVisible, setIsDropdownVisible] = useState(false); - const wasDropdownVisible = usePrevious(isDropdownVisible); - const [currentSuggestionSearch, setCurrentSuggestionSearch] = useState(''); const [dataMask, dispatchDataMask] = useImmerReducer(reducer, { extraFormData: {}, filterState, @@ -171,9 +140,6 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) { ); useEffect(() => { - if (!isDropdownVisible) { - setSelectedValues(filterState.value); - } updateDataMask(filterState.value); }, [JSON.stringify(filterState.value)]); @@ -197,11 +163,9 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) { if (searchAllOptions) { debouncedOwnStateFunc(val); } - setCurrentSuggestionSearch(val); }; const clearSuggestionSearch = () => { - setCurrentSuggestionSearch(''); if (searchAllOptions) { dispatchDataMask({ type: 'ownState', @@ -216,13 +180,16 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) { const handleBlur = () => { clearSuggestionSearch(); unsetFocusedFilter(); - setSelectedValues(filterState.value); }; const datatype: GenericDataType = coltypeMap[col]; - const labelFormatter = getDataRecordFormatter({ - timeFormatter: smartDateDetailedFormatter, - }); + const labelFormatter = useMemo( + () => + getDataRecordFormatter({ + timeFormatter: smartDateDetailedFormatter, + }), + [], + ); const handleChange = (value?: SelectValue | number | string) => { const values = ensureIsArray(value); @@ -271,7 +238,6 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) { data.length === 0 ? t('No data') : tn('%s option', '%s options', data.length, data.length); - const Icon = inverseSelection ? Icons.StopOutlined : Icons.CheckOutlined; const formItemData: FormItemProps = {}; if (filterState.validateMessage) { @@ -282,32 +248,36 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) { ); } + const options = useMemo(() => { + const options: { label: string; value: string | number }[] = []; + data.forEach(row => { + const [value] = groupby.map(col => row[col]); + options.push({ + label: labelFormatter(value, datatype), + value: typeof value === 'number' ? value : String(value), + }); + }); + return options; + }, [data, datatype, groupby, labelFormatter]); + return ( - + - }, - ) => { - if (isDropdownVisible && !wasDropdownVisible) { - originNode.ref?.current?.scrollTo({ top: 0 }); - } - return originNode; - }} onMouseEnter={setFocusedFilter} onMouseLeave={unsetFocusedFilter} // @ts-ignore @@ -315,27 +285,10 @@ export default function PluginFilterSelect(props: PluginFilterSelectProps) { ref={inputRef} loading={isRefreshing} maxTagCount={5} - menuItemSelectedIcon={} - > - {sortedData.map(row => { - const [value] = groupby.map(col => row[col]); - return ( - // @ts-ignore - - ); - })} - {currentSuggestionSearch && - !ensureIsArray(filterState.value).some( - suggestion => suggestion === currentSuggestionSearch, - ) && ( - - )} - + invertSelection={inverseSelection} + options={options} + /> - + ); } diff --git a/superset-frontend/src/filters/components/Time/TimeFilterPlugin.tsx b/superset-frontend/src/filters/components/Time/TimeFilterPlugin.tsx index 0840a2e9d3a..be5d2d1ac8a 100644 --- a/superset-frontend/src/filters/components/Time/TimeFilterPlugin.tsx +++ b/superset-frontend/src/filters/components/Time/TimeFilterPlugin.tsx @@ -21,9 +21,9 @@ import React, { useEffect } from 'react'; import DateFilterControl from 'src/explore/components/controls/DateFilterControl'; import { NO_TIME_RANGE } from 'src/explore/constants'; import { PluginFilterTimeProps } from './types'; -import { Styles } from '../common'; +import { FilterPluginStyle } from '../common'; -const TimeFilterStyles = styled(Styles)` +const TimeFilterStyles = styled(FilterPluginStyle)` overflow-x: auto; `; diff --git a/superset-frontend/src/filters/components/TimeColumn/TimeColumnFilterPlugin.tsx b/superset-frontend/src/filters/components/TimeColumn/TimeColumnFilterPlugin.tsx index 62127da1be3..0f616a6a709 100644 --- a/superset-frontend/src/filters/components/TimeColumn/TimeColumnFilterPlugin.tsx +++ b/superset-frontend/src/filters/components/TimeColumn/TimeColumnFilterPlugin.tsx @@ -24,13 +24,11 @@ import { tn, } from '@superset-ui/core'; import React, { useEffect, useState } from 'react'; -import { Select } from 'src/common/components'; +import { Select } from 'src/components'; import { FormItemProps } from 'antd/lib/form'; -import { Styles, StyledSelect, StyledFormItem, StatusMessage } from '../common'; +import { FilterPluginStyle, StyledFormItem, StatusMessage } from '../common'; import { PluginFilterTimeColumnProps } from './types'; -const { Option } = Select; - export default function PluginFilterTimeColumn( props: PluginFilterTimeColumnProps, ) { @@ -91,13 +89,24 @@ export default function PluginFilterTimeColumn( ); } + + const options = timeColumns.map( + (row: { column_name: string; verbose_name: string | null }) => { + const { column_name: columnName, verbose_name: verboseName } = row; + return { + label: verboseName ?? columnName, + value: columnName, + }; + }, + ); + return ( - + - - {timeColumns.map( - (row: { column_name: string; verbose_name: string | null }) => { - const { - column_name: columnName, - verbose_name: verboseName, - } = row; - return ( - - ); - }, - )} - + options={options} + /> - + ); } diff --git a/superset-frontend/src/filters/components/TimeGrain/TimeGrainFilterPlugin.tsx b/superset-frontend/src/filters/components/TimeGrain/TimeGrainFilterPlugin.tsx index 22442be3a49..4bac5c47f71 100644 --- a/superset-frontend/src/filters/components/TimeGrain/TimeGrainFilterPlugin.tsx +++ b/superset-frontend/src/filters/components/TimeGrain/TimeGrainFilterPlugin.tsx @@ -24,13 +24,11 @@ import { tn, } from '@superset-ui/core'; import React, { useEffect, useMemo, useState } from 'react'; -import { Select } from 'src/common/components'; +import { Select } from 'src/components'; import { FormItemProps } from 'antd/lib/form'; -import { Styles, StyledSelect, StyledFormItem, StatusMessage } from '../common'; +import { FilterPluginStyle, StyledFormItem, StatusMessage } from '../common'; import { PluginFilterTimeGrainProps } from './types'; -const { Option } = Select; - export default function PluginFilterTimegrain( props: PluginFilterTimeGrainProps, ) { @@ -101,13 +99,24 @@ export default function PluginFilterTimegrain( ); } + + const options = (data || []).map( + (row: { name: string; duration: string }) => { + const { name, duration } = row; + return { + label: name, + value: duration, + }; + }, + ); + return ( - + - - {(data || []).map((row: { name: string; duration: string }) => { - const { name, duration } = row; - return ( - - ); - })} - + options={options} + /> - + ); } diff --git a/superset-frontend/src/filters/components/common.ts b/superset-frontend/src/filters/components/common.ts index b9bc5758046..af1fe9c7917 100644 --- a/superset-frontend/src/filters/components/common.ts +++ b/superset-frontend/src/filters/components/common.ts @@ -17,19 +17,14 @@ * under the License. */ import { styled } from '@superset-ui/core'; -import { Select } from 'src/common/components'; import { PluginFilterStylesProps } from './types'; import FormItem from '../../components/Form/FormItem'; -export const Styles = styled.div` +export const FilterPluginStyle = styled.div` min-height: ${({ height }) => height}px; width: ${({ width }) => width}px; `; -export const StyledSelect = styled(Select)` - width: 100%; -`; - export const StyledFormItem = styled(FormItem)` &.ant-row.ant-form-item { margin: 0;