From 6ec2feb164e16e4ed0188f3454570d032a354e97 Mon Sep 17 00:00:00 2001 From: Kamil Gabryjelski Date: Fri, 19 Apr 2024 17:56:46 +0200 Subject: [PATCH] Use antd select --- .../src/components/ComparisonRangeLabel.tsx | 133 ++++++++++++++++++ .../components/Select/AsyncSelect.stories.tsx | 115 +++------------ .../components/Select/AsyncSelect.test.tsx | 68 ++------- .../src/components/Select/AsyncSelect.tsx | 15 +- .../src/components/Select/Select.stories.tsx | 38 ----- .../src/components/Select/Select.test.tsx | 32 ----- .../src/components/Select/Select.tsx | 26 +--- .../src/components/Select/styles.tsx | 2 +- .../src/components/Select/types.ts | 3 +- .../src/components/Select/utils.tsx | 14 +- .../controls/ColorSchemeControl/index.tsx | 53 +++---- 11 files changed, 207 insertions(+), 292 deletions(-) create mode 100644 superset-frontend/packages/superset-ui-chart-controls/src/components/ComparisonRangeLabel.tsx diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/components/ComparisonRangeLabel.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/components/ComparisonRangeLabel.tsx new file mode 100644 index 0000000000000..0db2cbd93cd47 --- /dev/null +++ b/superset-frontend/packages/superset-ui-chart-controls/src/components/ComparisonRangeLabel.tsx @@ -0,0 +1,133 @@ +/** + * 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, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { isEmpty, isEqual } from 'lodash'; +import { + BinaryAdhocFilter, + ComparisonTimeRangeType, + css, + fetchTimeRange, + SimpleAdhocFilter, + t, +} from '@superset-ui/core'; +import { Tooltip } from './Tooltip'; + +const isTimeRangeEqual = ( + left: BinaryAdhocFilter[], + right: BinaryAdhocFilter[], +) => isEqual(left, right); + +const ComparisonRangeLabel = props => { + console.log(props); + const [labels, setLabels] = useState([]); + const currentTimeRangeFilters = useSelector( + state => + state.explore.form_data.adhoc_filters.filter( + (adhoc_filter: SimpleAdhocFilter) => + adhoc_filter.operator === 'TEMPORAL_RANGE', + ), + isTimeRangeEqual, + ); + + const customTimeRangeComparisonFilters = useSelector< + any, + BinaryAdhocFilter[] + >( + state => + state.explore.form_data.adhoc_custom.filter( + (adhoc_filter: SimpleAdhocFilter) => + adhoc_filter.operator === 'TEMPORAL_RANGE', + ), + isTimeRangeEqual, + ); + + const shift = useSelector( + state => state.explore.form_data.time_comparison, + ); + + const startDate = useSelector( + state => state.explore.form_data.start_date_offset, + ); + + useEffect(() => { + if (shift === ComparisonTimeRangeType.Custom) { + const promises = customTimeRangeComparisonFilters.map(filter => + fetchTimeRange(filter.comparator, filter.subject), + ); + Promise.all(promises).then(res => { + setLabels(res.map(r => r.value ?? '')); + }); + } + }, [customTimeRangeComparisonFilters, shift]); + + useEffect(() => { + if (shift !== ComparisonTimeRangeType.Custom) { + const promises = currentTimeRangeFilters.map(filter => + fetchTimeRange(filter.comparator, filter.subject, shift), + ); + Promise.all(promises).then(res => { + setLabels(res.map(r => r.value ?? '')); + }); + } + }, [currentTimeRangeFilters, shift]); + + useEffect(() => { + if (isEmpty(currentTimeRangeFilters) || (isEmpty(shifts) && !startDate)) { + setLabels([]); + } else if (!isEmpty(shifts) || startDate) { + const promises = currentTimeRangeFilters.map(filter => { + const startDateShift = moment( + (filter as any).comparator.split(' : ')[0], + ).diff(moment(startDate), 'days'); + const newshift = startDateShift + ? [`${startDateShift} days ago`] + : shifts + ? shifts.slice(0, 1) + : undefined; + + return fetchTimeRange( + filter.comparator, + filter.subject, + multi ? shifts : newshift, + ); + }); + Promise.all(promises).then(res => { + // access the value property inside the res and set the labels with it in the state + setLabels(res.map(r => r.value ?? '')); + }); + } + }, [currentTimeRangeFilters, shifts, startDate]); + + return label ? ( + + css` + font-size: ${theme.typography.sizes.m}px; + color: ${theme.colors.grayscale.base}; + `} + > + {label} + + + ) : null; +}; + +export default ComparisonRangeLabel; diff --git a/superset-frontend/src/components/Select/AsyncSelect.stories.tsx b/superset-frontend/src/components/Select/AsyncSelect.stories.tsx index 878892e3f901e..0abc4a9d572b9 100644 --- a/superset-frontend/src/components/Select/AsyncSelect.stories.tsx +++ b/superset-frontend/src/components/Select/AsyncSelect.stories.tsx @@ -151,99 +151,37 @@ const USERS = [ 'Claire', 'Benedetta', 'Ilenia', -] - .sort() - .map(u => ({ label: u, value: u })); - -const GROUPED_USERS = [ - { - label: 'Male', - title: 'Male', - options: [ - 'John', - 'Liam', - 'Noah', - 'Oliver', - 'Elijah', - 'Diego', - 'Evan', - 'Michael', - 'Giovanni', - 'Luca', - 'Paolo', - 'Mario', - 'Marco', - 'Luigi', - 'Quarto', - 'Quinto', - 'Sesto', - 'Franco', - 'Sandro', - 'Alehandro', - 'Johnny', - 'Igor', - 'Thami', - 'Munei', - 'Guilherme', - 'Umair', - 'Ashfaq', - 'Irfan', - 'George', - 'Naseer', - 'Mohammad', - 'Rick', - ], - }, - { - label: 'Female', - title: 'Female', - options: [ - 'Olivia', - 'Emma', - 'Ava', - 'Charlotte', - 'Francesca', - 'Chiara', - 'Sara', - 'Valentina', - 'Jessica', - 'Angelica', - 'Nikole', - 'Amna', - 'Saliya', - 'Claire', - 'Benedetta', - 'Ilenia', - ], - }, -].map(group => ({ - ...group, - options: group.options.sort().map(u => ({ label: u, value: u })), -})); - -type AsyncSelectStoryProps = AsyncSelectProps & { - withError: boolean; - withInitialValue: boolean; - responseTime: number; - data: { label: string; value: string }[]; -}; +].sort(); export const AsynchronousSelect = ({ fetchOnlyOnSearch, withError, withInitialValue, responseTime, - data, ...rest -}: AsyncSelectStoryProps) => { +}: AsyncSelectProps & { + withError: boolean; + withInitialValue: boolean; + responseTime: number; +}) => { const [requests, setRequests] = useState([]); const ref = useRef(null); const getResults = (username?: string) => { - let results = [...data]; + let results: { label: string; value: string }[] = []; - if (username) { - results = data.filter(u => u.value.toLowerCase().includes(username)); + if (!username) { + results = USERS.map(u => ({ + label: u, + value: u, + })); + } else { + const foundUsers = USERS.filter(u => u.toLowerCase().includes(username)); + if (foundUsers) { + results = foundUsers.map(u => ({ label: u, value: u })); + } else { + results = []; + } } return results; }; @@ -352,7 +290,6 @@ AsynchronousSelect.args = { withError: false, withInitialValue: false, tokenSeparators: ['\n', '\t', ';'], - data: USERS, }; AsynchronousSelect.argTypes = { @@ -387,17 +324,3 @@ AsynchronousSelect.argTypes = { }, }, }; - -export const AsyncSelectWithGroups = (props: AsyncSelectStoryProps) => ( - -); - -AsyncSelectWithGroups.args = { - allowClear: true, - allowNewOptions: true, - mode: 'multiple', - pageSize: 5, - tokenSeparators: ['\n', '\t', ';'], - data: GROUPED_USERS, -}; -AsyncSelectWithGroups.argTypes = AsynchronousSelect.argTypes; diff --git a/superset-frontend/src/components/Select/AsyncSelect.test.tsx b/superset-frontend/src/components/Select/AsyncSelect.test.tsx index ae50feea4ac81..652b1f0ea25ab 100644 --- a/superset-frontend/src/components/Select/AsyncSelect.test.tsx +++ b/superset-frontend/src/components/Select/AsyncSelect.test.tsx @@ -27,7 +27,6 @@ import { } from 'spec/helpers/testing-library'; import userEvent from '@testing-library/user-event'; import { AsyncSelect } from 'src/components'; -import { OptionData, OptionGroup } from './types'; const ARIA_LABEL = 'Test'; const NEW_OPTION = 'Kyle'; @@ -63,31 +62,24 @@ const NULL_OPTION = { label: '', value: null } as unknown as { value: number; }; -const loadOptions = async ( - search: string, - page: number, - pageSize: number, - options: OptionData | OptionGroup[] = OPTIONS, -) => { - const totalCount = options.length; +const loadOptions = async (search: string, page: number, pageSize: number) => { + const totalCount = OPTIONS.length; const start = page * pageSize; const deleteCount = start + pageSize < totalCount ? pageSize : totalCount - start; const searchValue = search.trim().toLowerCase(); const optionFilterProps = ['label', 'value', 'gender']; - const data = options - .filter((option: (typeof OPTIONS)[0]) => - optionFilterProps.some(prop => { - const optionProp = option?.[prop] - ? String(option[prop]).trim().toLowerCase() - : ''; - return optionProp.includes(searchValue); - }), - ) - .splice(start, deleteCount); + const data = OPTIONS.filter(option => + optionFilterProps.some(prop => { + const optionProp = option?.[prop] + ? String(option[prop]).trim().toLowerCase() + : ''; + return optionProp.includes(searchValue); + }), + ).splice(start, deleteCount); return { data, - totalCount: options.length, + totalCount: OPTIONS.length, }; }; @@ -1009,44 +1001,6 @@ test('does not fire onChange if the same value is selected in single mode', asyn expect(onChange).toHaveBeenCalledTimes(1); }); -test('correctly renders options group', async () => { - const groupedOptions = Object.values( - OPTIONS.slice(0, 10).reduce((acc, opt) => { - if (Array.isArray(acc[opt.gender]?.options)) { - acc[opt.gender].options.push(opt); - } else { - acc[opt.gender] = { - options: [opt], - title: opt.gender, - label: opt.gender, - }; - } - return acc; - }, {}), - ) as OptionGroup[]; - - render( - - loadOptions(search, page, pageSize, groupedOptions) - } - />, - ); - await open(); - - expect( - getElementsByClassName('.ant-select-item-option-grouped'), - ).toHaveLength(10); - expect(getElementsByClassName('.ant-select-item-group')).toHaveLength(2); - expect(getElementsByClassName('.ant-select-item-group')[0]).toHaveTextContent( - 'Female', - ); - expect(getElementsByClassName('.ant-select-item-group')[1]).toHaveTextContent( - 'Male', - ); -}); - /* TODO: Add tests that require scroll interaction. Needs further investigation. - Fetches more data when scrolling and more data is available diff --git a/superset-frontend/src/components/Select/AsyncSelect.tsx b/superset-frontend/src/components/Select/AsyncSelect.tsx index e613e4a0a443d..5b520f5e22ece 100644 --- a/superset-frontend/src/components/Select/AsyncSelect.tsx +++ b/superset-frontend/src/components/Select/AsyncSelect.tsx @@ -55,7 +55,6 @@ import { getOption, isObject, isEqual as utilsIsEqual, - isOptGroup, } from './utils'; import { AsyncSelectProps, @@ -225,17 +224,6 @@ const AsyncSelect = forwardRef( : selectOptions; }, [selectOptions, selectValue]); - const flattenedOptions = useMemo( - () => - fullSelectOptions.reduce((acc, option) => { - if (isOptGroup(option)) { - return [...acc, ...option.options]; - } - return [...acc, option]; - }, []), - [fullSelectOptions], - ); - const handleOnSelect: SelectProps['onSelect'] = (selectedItem, option) => { if (isSingleMode) { // on select is fired in single value mode if the same value is selected @@ -342,7 +330,6 @@ const AsyncSelect = forwardRef( const fetchOptions = options as SelectOptionsPagePromise; fetchOptions(search, page, pageSize) .then(({ data, totalCount }: SelectOptionsTypePage) => { - console.log(data); const mergedData = mergeData(data); fetchedQueries.current.set(key, totalCount); setTotalCount(totalCount); @@ -461,7 +448,7 @@ const AsyncSelect = forwardRef( originNode, isDropdownVisible, isLoading, - flattenedOptions.length, + fullSelectOptions.length, helperText, error ? : undefined, ); diff --git a/superset-frontend/src/components/Select/Select.stories.tsx b/superset-frontend/src/components/Select/Select.stories.tsx index f33748b6cc4cc..4d7e06c47ea26 100644 --- a/superset-frontend/src/components/Select/Select.stories.tsx +++ b/superset-frontend/src/components/Select/Select.stories.tsx @@ -55,30 +55,6 @@ const options: SelectOptionsType = [ { label: 'I', value: 'I' }, ]; -const groupedOptions = [ - { - title: 'Group 1', - label: Group 1, - options: [ - { label: 'A', value: 'A' }, - { label: 'B', value: 'B' }, - { label: 'C', value: 'C' }, - { label: 'D', value: 'D' }, - ], - }, - { - title: 'Group 2', - label: Group 2, - options: [ - { label: 'E', value: 'E' }, - { label: 'F', value: 'F' }, - { label: 'G', value: 'G' }, - { label: 'H', value: 'H' }, - { label: 'I', value: 'I' }, - ], - }, -]; - const selectPositions = [ { id: 'topLeft', @@ -269,20 +245,6 @@ export const InteractiveSelect: StoryObj = { }, }; -export const InteractiveSelectWithGroups: StoryObj = { - render: ({ header, ...args }: SelectProps & { header: string }) => ( -
- ); - await open(); - - expect( - getElementsByClassName('.ant-select-item-option-grouped'), - ).toHaveLength(10); - expect(getElementsByClassName('.ant-select-item-group')).toHaveLength(2); - expect(getElementsByClassName('.ant-select-item-group')[0]).toHaveTextContent( - 'Male', - ); - expect(getElementsByClassName('.ant-select-item-group')[1]).toHaveTextContent( - 'Female', - ); -}); - /* TODO: Add tests that require scroll interaction. Needs further investigation. - Fetches more data when scrolling and more data is available diff --git a/superset-frontend/src/components/Select/Select.tsx b/superset-frontend/src/components/Select/Select.tsx index 005d43a0a64ea..d92b86318d0a7 100644 --- a/superset-frontend/src/components/Select/Select.tsx +++ b/superset-frontend/src/components/Select/Select.tsx @@ -54,7 +54,6 @@ import { getOption, isObject, isEqual as utilsIsEqual, - isOptGroup, } from './utils'; import { RawValue, SelectOptionsType, SelectProps } from './types'; import { @@ -81,7 +80,7 @@ import { customTagRender } from './CustomTag'; * It is divided into two macro categories, Static and Async. * The Static type accepts a static array of options. * The Async type accepts a promise that will return the options. - * Each of the categories comes with different abilities. For a comprehensive guide please refer to + * Each of the categories come with different abilities. For a comprehensive guide please refer to * the storybook in src/components/Select/Select.stories.tsx. */ const Select = forwardRef( @@ -193,28 +192,17 @@ const Select = forwardRef( return result.filter(opt => opt.value !== SELECT_ALL_VALUE); }, [selectOptions, selectValue]); - const flattenedOptions = useMemo( - () => - fullSelectOptions.reduce((acc, option) => { - if (isOptGroup(option)) { - return [...acc, ...option.options]; - } - return [...acc, option]; - }, []), - [fullSelectOptions], - ); - const enabledOptions = useMemo( - () => flattenedOptions.filter(option => !option.disabled), - [flattenedOptions], + () => fullSelectOptions.filter(option => !option.disabled), + [fullSelectOptions], ); const selectAllEligible = useMemo( () => - flattenedOptions.filter( + fullSelectOptions.filter( option => hasOption(option.value, selectValue) || !option.disabled, ), - [flattenedOptions, selectValue], + [fullSelectOptions, selectValue], ); const selectAllEnabled = useMemo( @@ -291,7 +279,7 @@ const Select = forwardRef( setSelectValue(undefined); } else { setSelectValue( - flattenedOptions + fullSelectOptions .filter( option => option.disabled && hasOption(option.value, selectValue), ) @@ -384,7 +372,7 @@ const Select = forwardRef( originNode, isDropdownVisible, isLoading, - flattenedOptions.length, + fullSelectOptions.length, helperText, ); diff --git a/superset-frontend/src/components/Select/styles.tsx b/superset-frontend/src/components/Select/styles.tsx index 73db7ee7c039d..79bf67cb9b898 100644 --- a/superset-frontend/src/components/Select/styles.tsx +++ b/superset-frontend/src/components/Select/styles.tsx @@ -41,7 +41,7 @@ export const StyledContainer = styled.div<{ headerPosition: string }>` export const StyledSelect = styled(AntdSelect, { shouldForwardProp: prop => prop !== 'headerPosition' && prop !== 'oneLine', -})<{ headerPosition: string; oneLine?: boolean }>` +})<{ headerPosition?: string; oneLine?: boolean }>` ${({ theme, headerPosition, oneLine }) => ` flex: ${headerPosition === 'left' ? 1 : 0}; && .ant-select-selector { diff --git a/superset-frontend/src/components/Select/types.ts b/superset-frontend/src/components/Select/types.ts index d4eecf916eb78..c12b8a52b08f1 100644 --- a/superset-frontend/src/components/Select/types.ts +++ b/superset-frontend/src/components/Select/types.ts @@ -28,7 +28,6 @@ import { LabeledValue as AntdLabeledValue, } from 'antd/lib/select'; import { TagProps } from 'antd/lib/tag'; -import type * as React from 'react'; export type RawValue = string | number; @@ -73,7 +72,7 @@ export type SelectOptionsType = Exclude; export type OptionData = SelectOptionsType[number]['options'][number]; export type OptionGroup = { - label?: React.ReactNode; + label?: ReactNode; title?: string; options: OptionData[]; }; diff --git a/superset-frontend/src/components/Select/utils.tsx b/superset-frontend/src/components/Select/utils.tsx index 77a9f02db211e..7a94d72fa8c61 100644 --- a/superset-frontend/src/components/Select/utils.tsx +++ b/superset-frontend/src/components/Select/utils.tsx @@ -74,11 +74,9 @@ export function getOption( checkLabel = false, ): V | LabeledValue { const optionsArray = ensureIsArray(options); - return optionsArray.find(opt => - isOptGroup(opt) - ? getOption(value, opt.options, checkLabel) - : isEqual(opt, value, 'value') || - (checkLabel && isEqual(opt, value, 'label')), + return optionsArray.find( + x => + isEqual(x, value, 'value') || (checkLabel && isEqual(x, value, 'label')), ); } @@ -209,10 +207,8 @@ export const handleFilterOptionHelper = ( return false; }; -export const hasCustomLabels = (options: SelectOptionsType): boolean => - options?.some(opt => - isOptGroup(opt) ? hasCustomLabels(opt.options) : !!opt?.customLabel, - ); +export const hasCustomLabels = (options: SelectOptionsType) => + options?.some(opt => !!opt?.customLabel); export const renderSelectOptions = (options: SelectOptionsType) => options.map(opt => { diff --git a/superset-frontend/src/explore/components/controls/ColorSchemeControl/index.tsx b/superset-frontend/src/explore/components/controls/ColorSchemeControl/index.tsx index d90e62b4c0906..e94c275c303a0 100644 --- a/superset-frontend/src/explore/components/controls/ColorSchemeControl/index.tsx +++ b/superset-frontend/src/explore/components/controls/ColorSchemeControl/index.tsx @@ -18,6 +18,7 @@ */ import React, { useMemo } from 'react'; import { + css, ColorScheme, ColorSchemeGroup, SequentialScheme, @@ -25,11 +26,12 @@ import { t, } from '@superset-ui/core'; import { isFunction, sortBy } from 'lodash'; -import { Select } from 'src/components'; import ControlHeader from 'src/explore/components/ControlHeader'; import { Tooltip } from 'src/components/Tooltip'; import Icons from 'src/components/Icons'; import { OptionData } from 'src/components/Select/types'; +import { StyledSelect } from 'src/components/Select/styles'; +import { renderSelectOptions } from 'src/components/Select/utils'; import ColorSchemeLabel from './ColorSchemeLabel'; export interface ColorSchemes { @@ -92,7 +94,6 @@ const ColorSchemeControl = ({ hasCustomLabelColors = false, dashboardId, label = t('Color scheme'), - name, onChange = () => {}, value, clearable = false, @@ -204,28 +205,32 @@ const ColorSchemeControl = ({ const handleOnChange = (value: string) => onChange(value); return ( -