mirror of
https://github.com/apache/superset.git
synced 2024-09-12 08:39:45 -04:00
chore: Adds lazy loading to the Select component (#15799)
This commit is contained in:
parent
04c0680f6e
commit
e660de6936
@ -144,6 +144,11 @@ InteractiveSelect.argTypes = {
|
|||||||
disable: true,
|
disable: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
fetchOnlyOnSearch: {
|
||||||
|
table: {
|
||||||
|
disable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
InteractiveSelect.story = {
|
InteractiveSelect.story = {
|
||||||
@ -296,10 +301,12 @@ const USERS = [
|
|||||||
|
|
||||||
export const AsyncSelect = ({
|
export const AsyncSelect = ({
|
||||||
withError,
|
withError,
|
||||||
|
withInitialValue,
|
||||||
responseTime,
|
responseTime,
|
||||||
...rest
|
...rest
|
||||||
}: SelectProps & {
|
}: SelectProps & {
|
||||||
withError: boolean;
|
withError: boolean;
|
||||||
|
withInitialValue: boolean;
|
||||||
responseTime: number;
|
responseTime: number;
|
||||||
}) => {
|
}) => {
|
||||||
const [requests, setRequests] = useState<ReactNode[]>([]);
|
const [requests, setRequests] = useState<ReactNode[]>([]);
|
||||||
@ -375,6 +382,11 @@ export const AsyncSelect = ({
|
|||||||
<Select
|
<Select
|
||||||
{...rest}
|
{...rest}
|
||||||
options={withError ? fetchUserListError : fetchUserListPage}
|
options={withError ? fetchUserListError : fetchUserListPage}
|
||||||
|
value={
|
||||||
|
withInitialValue
|
||||||
|
? { label: 'Valentina', value: 'Valentina' }
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@ -398,9 +410,11 @@ export const AsyncSelect = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
AsyncSelect.args = {
|
AsyncSelect.args = {
|
||||||
withError: false,
|
|
||||||
pageSize: 10,
|
|
||||||
allowNewOptions: false,
|
allowNewOptions: false,
|
||||||
|
fetchOnlyOnSearch: false,
|
||||||
|
pageSize: 10,
|
||||||
|
withError: false,
|
||||||
|
withInitialValue: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
AsyncSelect.argTypes = {
|
AsyncSelect.argTypes = {
|
||||||
@ -431,6 +445,7 @@ AsyncSelect.argTypes = {
|
|||||||
type: 'range',
|
type: 'range',
|
||||||
min: 0.5,
|
min: 0.5,
|
||||||
max: 5,
|
max: 5,
|
||||||
|
step: 0.5,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -28,16 +28,17 @@ import React, {
|
|||||||
useCallback,
|
useCallback,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { styled, t } from '@superset-ui/core';
|
import { styled, t } from '@superset-ui/core';
|
||||||
import { Select as AntdSelect } from 'antd';
|
import AntdSelect, {
|
||||||
import Icons from 'src/components/Icons';
|
|
||||||
import {
|
|
||||||
SelectProps as AntdSelectProps,
|
SelectProps as AntdSelectProps,
|
||||||
SelectValue as AntdSelectValue,
|
SelectValue as AntdSelectValue,
|
||||||
LabeledValue as AntdLabeledValue,
|
LabeledValue as AntdLabeledValue,
|
||||||
} from 'antd/lib/select';
|
} from 'antd/lib/select';
|
||||||
|
import { DownOutlined, SearchOutlined } from '@ant-design/icons';
|
||||||
import debounce from 'lodash/debounce';
|
import debounce from 'lodash/debounce';
|
||||||
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
|
|
||||||
import { isEqual } from 'lodash';
|
import { isEqual } from 'lodash';
|
||||||
|
import { Spin } from 'antd';
|
||||||
|
import Icons from 'src/components/Icons';
|
||||||
|
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
|
||||||
import { hasOption } from './utils';
|
import { hasOption } from './utils';
|
||||||
|
|
||||||
type AntdSelectAllProps = AntdSelectProps<AntdSelectValue>;
|
type AntdSelectAllProps = AntdSelectProps<AntdSelectValue>;
|
||||||
@ -47,7 +48,6 @@ type PickedSelectProps = Pick<
|
|||||||
| 'allowClear'
|
| 'allowClear'
|
||||||
| 'autoFocus'
|
| 'autoFocus'
|
||||||
| 'value'
|
| 'value'
|
||||||
| 'defaultValue'
|
|
||||||
| 'disabled'
|
| 'disabled'
|
||||||
| 'filterOption'
|
| 'filterOption'
|
||||||
| 'notFoundContent'
|
| 'notFoundContent'
|
||||||
@ -79,6 +79,7 @@ export interface SelectProps extends PickedSelectProps {
|
|||||||
options: OptionsType | OptionsPagePromise;
|
options: OptionsType | OptionsPagePromise;
|
||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
invertSelection?: boolean;
|
invertSelection?: boolean;
|
||||||
|
fetchOnlyOnSearch?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
@ -122,6 +123,10 @@ const StyledError = styled.div`
|
|||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const StyledSpin = styled(Spin)`
|
||||||
|
margin-top: ${({ theme }) => -theme.gridUnit}px;
|
||||||
|
`;
|
||||||
|
|
||||||
const MAX_TAG_COUNT = 4;
|
const MAX_TAG_COUNT = 4;
|
||||||
const TOKEN_SEPARATORS = [',', '\n', '\t', ';'];
|
const TOKEN_SEPARATORS = [',', '\n', '\t', ';'];
|
||||||
const DEBOUNCE_TIMEOUT = 500;
|
const DEBOUNCE_TIMEOUT = 500;
|
||||||
@ -137,15 +142,16 @@ const Error = ({ error }: { error: string }) => (
|
|||||||
const Select = ({
|
const Select = ({
|
||||||
allowNewOptions = false,
|
allowNewOptions = false,
|
||||||
ariaLabel,
|
ariaLabel,
|
||||||
|
fetchOnlyOnSearch,
|
||||||
filterOption = true,
|
filterOption = true,
|
||||||
header = null,
|
header = null,
|
||||||
|
invertSelection = false,
|
||||||
mode = 'single',
|
mode = 'single',
|
||||||
name,
|
name,
|
||||||
|
options,
|
||||||
pageSize = DEFAULT_PAGE_SIZE,
|
pageSize = DEFAULT_PAGE_SIZE,
|
||||||
placeholder = t('Select ...'),
|
placeholder = t('Select ...'),
|
||||||
options,
|
|
||||||
showSearch,
|
showSearch,
|
||||||
invertSelection = false,
|
|
||||||
value,
|
value,
|
||||||
...props
|
...props
|
||||||
}: SelectProps) => {
|
}: SelectProps) => {
|
||||||
@ -164,7 +170,8 @@ const Select = ({
|
|||||||
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
|
const [isDropdownVisible, setIsDropdownVisible] = useState(false);
|
||||||
const [page, setPage] = useState(0);
|
const [page, setPage] = useState(0);
|
||||||
const [totalCount, setTotalCount] = useState(0);
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
const fetchedQueries = useRef(new Set<string>());
|
const [loadingEnabled, setLoadingEnabled] = useState(false);
|
||||||
|
const fetchedQueries = useRef(new Map<string, number>());
|
||||||
const mappedMode = isSingleMode
|
const mappedMode = isSingleMode
|
||||||
? undefined
|
? undefined
|
||||||
: allowNewOptions
|
: allowNewOptions
|
||||||
@ -177,6 +184,26 @@ const Select = ({
|
|||||||
);
|
);
|
||||||
}, [options]);
|
}, [options]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isAsync && value) {
|
||||||
|
const array: AntdLabeledValue[] = Array.isArray(value)
|
||||||
|
? (value as AntdLabeledValue[])
|
||||||
|
: [value as AntdLabeledValue];
|
||||||
|
const options: AntdLabeledValue[] = [];
|
||||||
|
array.forEach(element => {
|
||||||
|
const found = selectOptions.find(
|
||||||
|
option => option.value === element.value,
|
||||||
|
);
|
||||||
|
if (!found) {
|
||||||
|
options.push(element);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (options.length > 0) {
|
||||||
|
setSelectOptions([...selectOptions, ...options]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isAsync, selectOptions, value]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectValue(value);
|
setSelectValue(value);
|
||||||
}, [value]);
|
}, [value]);
|
||||||
@ -185,24 +212,56 @@ const Select = ({
|
|||||||
(selectedValue: AntdSelectValue | undefined) => {
|
(selectedValue: AntdSelectValue | undefined) => {
|
||||||
// bringing selected options to the top of the list
|
// bringing selected options to the top of the list
|
||||||
if (selectedValue) {
|
if (selectedValue) {
|
||||||
const currentValue = selectedValue as string[] | string;
|
const topOptions: OptionsType = [];
|
||||||
const topOptions = selectOptions.filter(opt =>
|
const otherOptions: OptionsType = [];
|
||||||
Array.isArray(currentValue)
|
|
||||||
? currentValue.includes(opt.value)
|
selectOptions.forEach(opt => {
|
||||||
: currentValue === opt.value,
|
let found = false;
|
||||||
);
|
if (Array.isArray(selectedValue)) {
|
||||||
const otherOptions = selectOptions.filter(
|
if (isAsync) {
|
||||||
opt => !topOptions.find(tOpt => tOpt.value === opt.value),
|
found =
|
||||||
);
|
(selectedValue as AntdLabeledValue[]).find(
|
||||||
|
element => element.value === opt.value,
|
||||||
|
) !== undefined;
|
||||||
|
} else {
|
||||||
|
found = selectedValue.includes(opt.value);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
found = isAsync
|
||||||
|
? (selectedValue as AntdLabeledValue).value === opt.value
|
||||||
|
: selectedValue === opt.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (found) {
|
||||||
|
topOptions.push(opt);
|
||||||
|
} else {
|
||||||
|
otherOptions.push(opt);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// fallback for custom options in tags mode as they
|
// fallback for custom options in tags mode as they
|
||||||
// do not appear in the selectOptions state
|
// do not appear in the selectOptions state
|
||||||
if (!isSingleMode && Array.isArray(currentValue)) {
|
if (!isSingleMode && Array.isArray(selectedValue)) {
|
||||||
// eslint-disable-next-line no-restricted-syntax
|
selectedValue.forEach((val: string | number | AntdLabeledValue) => {
|
||||||
for (const val of currentValue) {
|
if (
|
||||||
if (!topOptions.find(tOpt => tOpt.value === val)) {
|
!topOptions.find(
|
||||||
topOptions.push({ label: val, value: val });
|
tOpt =>
|
||||||
|
tOpt.value ===
|
||||||
|
(isAsync ? (val as AntdLabeledValue)?.value : val),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (isAsync) {
|
||||||
|
const labelValue = val as AntdLabeledValue;
|
||||||
|
topOptions.push({
|
||||||
|
label: labelValue.label,
|
||||||
|
value: labelValue.value,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const value = val as string | number;
|
||||||
|
topOptions.push({ label: String(value), value });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const sortedOptions = [...topOptions, ...otherOptions];
|
const sortedOptions = [...topOptions, ...otherOptions];
|
||||||
@ -211,7 +270,7 @@ const Select = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isSingleMode, selectOptions],
|
[isAsync, isSingleMode, selectOptions],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOnSelect = (
|
const handleOnSelect = (
|
||||||
@ -220,7 +279,11 @@ const Select = ({
|
|||||||
if (isSingleMode) {
|
if (isSingleMode) {
|
||||||
setSelectValue(selectedValue);
|
setSelectValue(selectedValue);
|
||||||
} else {
|
} else {
|
||||||
const currentSelected = Array.isArray(selectValue) ? selectValue : [];
|
const currentSelected = selectValue
|
||||||
|
? Array.isArray(selectValue)
|
||||||
|
? selectValue
|
||||||
|
: [selectValue]
|
||||||
|
: [];
|
||||||
if (
|
if (
|
||||||
typeof selectedValue === 'number' ||
|
typeof selectedValue === 'number' ||
|
||||||
typeof selectedValue === 'string'
|
typeof selectedValue === 'string'
|
||||||
@ -271,7 +334,9 @@ const Select = ({
|
|||||||
const handlePaginatedFetch = useMemo(
|
const handlePaginatedFetch = useMemo(
|
||||||
() => (value: string, page: number, pageSize: number) => {
|
() => (value: string, page: number, pageSize: number) => {
|
||||||
const key = `${value};${page};${pageSize}`;
|
const key = `${value};${page};${pageSize}`;
|
||||||
if (fetchedQueries.current.has(key)) {
|
const cachedCount = fetchedQueries.current.get(key);
|
||||||
|
if (cachedCount) {
|
||||||
|
setTotalCount(cachedCount);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@ -279,7 +344,7 @@ const Select = ({
|
|||||||
fetchOptions(value, page, pageSize)
|
fetchOptions(value, page, pageSize)
|
||||||
.then(({ data, totalCount }: OptionsTypePage) => {
|
.then(({ data, totalCount }: OptionsTypePage) => {
|
||||||
handleData(data);
|
handleData(data);
|
||||||
fetchedQueries.current.add(key);
|
fetchedQueries.current.set(key, totalCount);
|
||||||
setTotalCount(totalCount);
|
setTotalCount(totalCount);
|
||||||
})
|
})
|
||||||
.catch(onError)
|
.catch(onError)
|
||||||
@ -351,6 +416,11 @@ const Select = ({
|
|||||||
|
|
||||||
const handleOnDropdownVisibleChange = (isDropdownVisible: boolean) => {
|
const handleOnDropdownVisibleChange = (isDropdownVisible: boolean) => {
|
||||||
setIsDropdownVisible(isDropdownVisible);
|
setIsDropdownVisible(isDropdownVisible);
|
||||||
|
|
||||||
|
if (isAsync && !loadingEnabled) {
|
||||||
|
setLoadingEnabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
// multiple or tags mode keep the dropdown visible while selecting options
|
// multiple or tags mode keep the dropdown visible while selecting options
|
||||||
// this waits for the dropdown to be closed before sorting the top options
|
// this waits for the dropdown to be closed before sorting the top options
|
||||||
if (!isSingleMode && !isDropdownVisible) {
|
if (!isSingleMode && !isDropdownVisible) {
|
||||||
@ -359,13 +429,20 @@ const Select = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const foundOption = hasOption(searchedValue, selectOptions);
|
const allowFetch = !fetchOnlyOnSearch || searchedValue;
|
||||||
if (isAsync && !foundOption) {
|
if (isAsync && loadingEnabled && allowFetch) {
|
||||||
const page = 0;
|
const page = 0;
|
||||||
handlePaginatedFetch(searchedValue, page, pageSize);
|
handlePaginatedFetch(searchedValue, page, pageSize);
|
||||||
setPage(page);
|
setPage(page);
|
||||||
}
|
}
|
||||||
}, [isAsync, searchedValue, selectOptions, pageSize, handlePaginatedFetch]);
|
}, [
|
||||||
|
isAsync,
|
||||||
|
searchedValue,
|
||||||
|
pageSize,
|
||||||
|
handlePaginatedFetch,
|
||||||
|
loadingEnabled,
|
||||||
|
fetchOnlyOnSearch,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSingleMode) {
|
if (isSingleMode) {
|
||||||
@ -382,6 +459,16 @@ const Select = ({
|
|||||||
return error ? <Error error={error} /> : originNode;
|
return error ? <Error error={error} /> : originNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SuffixIcon = () => {
|
||||||
|
if (isLoading) {
|
||||||
|
return <StyledSpin size="small" />;
|
||||||
|
}
|
||||||
|
if (shouldShowSearch && isDropdownVisible) {
|
||||||
|
return <SearchOutlined />;
|
||||||
|
}
|
||||||
|
return <DownOutlined />;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<StyledContainer>
|
||||||
{header}
|
{header}
|
||||||
@ -391,7 +478,7 @@ const Select = ({
|
|||||||
dropdownRender={dropdownRender}
|
dropdownRender={dropdownRender}
|
||||||
filterOption={handleFilterOption}
|
filterOption={handleFilterOption}
|
||||||
getPopupContainer={triggerNode => triggerNode.parentNode}
|
getPopupContainer={triggerNode => triggerNode.parentNode}
|
||||||
loading={isLoading}
|
labelInValue={isAsync}
|
||||||
maxTagCount={MAX_TAG_COUNT}
|
maxTagCount={MAX_TAG_COUNT}
|
||||||
mode={mappedMode}
|
mode={mappedMode}
|
||||||
onDeselect={handleOnDeselect}
|
onDeselect={handleOnDeselect}
|
||||||
@ -406,6 +493,7 @@ const Select = ({
|
|||||||
showArrow
|
showArrow
|
||||||
tokenSeparators={TOKEN_SEPARATORS}
|
tokenSeparators={TOKEN_SEPARATORS}
|
||||||
value={selectValue}
|
value={selectValue}
|
||||||
|
suffixIcon={<SuffixIcon />}
|
||||||
menuItemSelectedIcon={
|
menuItemSelectedIcon={
|
||||||
invertSelection ? (
|
invertSelection ? (
|
||||||
<StyledStopOutlined iconSize="m" />
|
<StyledStopOutlined iconSize="m" />
|
||||||
|
@ -36,18 +36,11 @@ const cachedSupersetGet = cacheWrapper(
|
|||||||
);
|
);
|
||||||
|
|
||||||
interface DatasetSelectProps {
|
interface DatasetSelectProps {
|
||||||
datasetDetails: Record<string, any> | undefined;
|
onChange: (value: { label: string; value: number }) => void;
|
||||||
datasetId: number;
|
value?: { label: string; value: number };
|
||||||
onChange: (value: number) => void;
|
|
||||||
value?: { value: number | undefined };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DatasetSelect = ({
|
const DatasetSelect = ({ onChange, value }: DatasetSelectProps) => {
|
||||||
datasetDetails,
|
|
||||||
datasetId,
|
|
||||||
onChange,
|
|
||||||
value,
|
|
||||||
}: DatasetSelectProps) => {
|
|
||||||
const getErrorMessage = useCallback(
|
const getErrorMessage = useCallback(
|
||||||
({ error, message }: ClientErrorObject) => {
|
({ error, message }: ClientErrorObject) => {
|
||||||
let errorText = message || error || t('An error has occurred');
|
let errorText = message || error || t('An error has occurred');
|
||||||
@ -84,15 +77,6 @@ const DatasetSelect = ({
|
|||||||
.sort((a: { label: string }, b: { label: string }) =>
|
.sort((a: { label: string }, b: { label: string }) =>
|
||||||
a.label.localeCompare(b.label),
|
a.label.localeCompare(b.label),
|
||||||
);
|
);
|
||||||
if (!search) {
|
|
||||||
const found = data.find(element => element.value === datasetId);
|
|
||||||
if (!found && datasetDetails?.table_name) {
|
|
||||||
data.push({
|
|
||||||
label: datasetDetails.table_name,
|
|
||||||
value: datasetId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
totalCount: response.json.count,
|
totalCount: response.json.count,
|
||||||
@ -107,7 +91,7 @@ const DatasetSelect = ({
|
|||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
ariaLabel={t('Dataset')}
|
ariaLabel={t('Dataset')}
|
||||||
value={value?.value}
|
value={value}
|
||||||
options={loadDatasetOptions}
|
options={loadDatasetOptions}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
/>
|
/>
|
||||||
|
@ -16,7 +16,7 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React, { FC, useEffect, useState } from 'react';
|
import React, { FC } from 'react';
|
||||||
import {
|
import {
|
||||||
Behavior,
|
Behavior,
|
||||||
SetDataMaskHook,
|
SetDataMaskHook,
|
||||||
@ -48,17 +48,9 @@ const DefaultValue: FC<DefaultValueProps> = ({
|
|||||||
formData,
|
formData,
|
||||||
enableNoResults,
|
enableNoResults,
|
||||||
}) => {
|
}) => {
|
||||||
const [loading, setLoading] = useState(hasDataset);
|
|
||||||
const formFilter = (form.getFieldValue('filters') || {})[filterId];
|
const formFilter = (form.getFieldValue('filters') || {})[filterId];
|
||||||
const queriesData = formFilter?.defaultValueQueriesData;
|
const queriesData = formFilter?.defaultValueQueriesData;
|
||||||
|
const loading = hasDataset && queriesData === null;
|
||||||
useEffect(() => {
|
|
||||||
if (!hasDataset || queriesData !== null) {
|
|
||||||
setLoading(false);
|
|
||||||
} else {
|
|
||||||
setLoading(true);
|
|
||||||
}
|
|
||||||
}, [hasDataset, queriesData]);
|
|
||||||
const value = formFilter.defaultDataMask?.filterState.value;
|
const value = formFilter.defaultDataMask?.filterState.value;
|
||||||
const isMissingRequiredValue =
|
const isMissingRequiredValue =
|
||||||
hasDefaultValue && (value === null || value === undefined);
|
hasDefaultValue && (value === null || value === undefined);
|
||||||
|
@ -355,8 +355,14 @@ const FiltersConfigForm = (
|
|||||||
const hasDataset = !!nativeFilterItems[formFilter?.filterType]?.value
|
const hasDataset = !!nativeFilterItems[formFilter?.filterType]?.value
|
||||||
?.datasourceCount;
|
?.datasourceCount;
|
||||||
|
|
||||||
|
const datasetId =
|
||||||
|
formFilter?.dataset?.value ??
|
||||||
|
filterToEdit?.targets[0]?.datasetId ??
|
||||||
|
mostUsedDataset(loadedDatasets, charts);
|
||||||
|
|
||||||
const { controlItems = {}, mainControlItems = {} } = formFilter
|
const { controlItems = {}, mainControlItems = {} } = formFilter
|
||||||
? getControlItemsMap({
|
? getControlItemsMap({
|
||||||
|
datasetId,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
forceUpdate,
|
forceUpdate,
|
||||||
form,
|
form,
|
||||||
@ -372,10 +378,9 @@ const FiltersConfigForm = (
|
|||||||
const nativeFilterItem = nativeFilterItems[formFilter?.filterType] ?? {};
|
const nativeFilterItem = nativeFilterItems[formFilter?.filterType] ?? {};
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
const enableNoResults = !!nativeFilterItem.value?.enableNoResults;
|
const enableNoResults = !!nativeFilterItem.value?.enableNoResults;
|
||||||
const datasetId = formFilter?.dataset?.value;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (datasetId && hasColumn) {
|
if (datasetId) {
|
||||||
cachedSupersetGet({
|
cachedSupersetGet({
|
||||||
endpoint: `/api/v1/dataset/${datasetId}`,
|
endpoint: `/api/v1/dataset/${datasetId}`,
|
||||||
})
|
})
|
||||||
@ -391,7 +396,7 @@ const FiltersConfigForm = (
|
|||||||
addDangerToast(response.message);
|
addDangerToast(response.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [datasetId, hasColumn]);
|
}, [datasetId]);
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
changeTab(tab: 'configuration' | 'scoping') {
|
changeTab(tab: 'configuration' | 'scoping') {
|
||||||
@ -491,10 +496,6 @@ const FiltersConfigForm = (
|
|||||||
[filterId, forceUpdate, form, formFilter, hasDataset],
|
[filterId, forceUpdate, form, formFilter, hasDataset],
|
||||||
);
|
);
|
||||||
|
|
||||||
const initialDatasetId =
|
|
||||||
filterToEdit?.targets[0]?.datasetId ??
|
|
||||||
mostUsedDataset(loadedDatasets, charts);
|
|
||||||
|
|
||||||
const newFormData = getFormData({
|
const newFormData = getFormData({
|
||||||
datasetId,
|
datasetId,
|
||||||
groupby: hasColumn ? formFilter?.column : undefined,
|
groupby: hasColumn ? formFilter?.column : undefined,
|
||||||
@ -508,6 +509,8 @@ const FiltersConfigForm = (
|
|||||||
setHasDefaultValue,
|
setHasDefaultValue,
|
||||||
] = useDefaultValue(formFilter, filterToEdit);
|
] = useDefaultValue(formFilter, filterToEdit);
|
||||||
|
|
||||||
|
const showDataset = !datasetId || datasetDetails;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasDataset && hasFilledDataset && hasDefaultValue && isDataDirty) {
|
if (hasDataset && hasFilledDataset && hasDefaultValue && isDataDirty) {
|
||||||
refreshHandler();
|
refreshHandler();
|
||||||
@ -519,6 +522,7 @@ const FiltersConfigForm = (
|
|||||||
formFilter,
|
formFilter,
|
||||||
isDataDirty,
|
isDataDirty,
|
||||||
refreshHandler,
|
refreshHandler,
|
||||||
|
showDataset,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const updateFormValues = useCallback(
|
const updateFormValues = useCallback(
|
||||||
@ -713,24 +717,29 @@ const FiltersConfigForm = (
|
|||||||
</StyledContainer>
|
</StyledContainer>
|
||||||
{hasDataset && (
|
{hasDataset && (
|
||||||
<StyledRowContainer>
|
<StyledRowContainer>
|
||||||
<StyledFormItem
|
{showDataset ? (
|
||||||
name={['filters', filterId, 'dataset']}
|
<StyledFormItem
|
||||||
initialValue={{ value: initialDatasetId }}
|
name={['filters', filterId, 'dataset']}
|
||||||
label={<StyledLabel>{t('Dataset')}</StyledLabel>}
|
label={<StyledLabel>{t('Dataset')}</StyledLabel>}
|
||||||
rules={[
|
initialValue={
|
||||||
{ required: !removed, message: t('Dataset is required') },
|
datasetDetails
|
||||||
]}
|
? {
|
||||||
{...getFiltersConfigModalTestId('datasource-input')}
|
label: datasetDetails.table_name,
|
||||||
>
|
value: datasetDetails.id,
|
||||||
{!datasetId || !hasColumn || datasetDetails ? (
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
rules={[
|
||||||
|
{ required: !removed, message: t('Dataset is required') },
|
||||||
|
]}
|
||||||
|
{...getFiltersConfigModalTestId('datasource-input')}
|
||||||
|
>
|
||||||
<DatasetSelect
|
<DatasetSelect
|
||||||
datasetDetails={datasetDetails}
|
onChange={(value: { label: string; value: number }) => {
|
||||||
datasetId={initialDatasetId}
|
|
||||||
onChange={(value: number) => {
|
|
||||||
// We need to reset the column when the dataset has changed
|
// We need to reset the column when the dataset has changed
|
||||||
if (value !== datasetId) {
|
if (value.value !== datasetId) {
|
||||||
setNativeFilterFieldValues(form, filterId, {
|
setNativeFilterFieldValues(form, filterId, {
|
||||||
dataset: { value },
|
dataset: value,
|
||||||
defaultDataMask: null,
|
defaultDataMask: null,
|
||||||
column: null,
|
column: null,
|
||||||
});
|
});
|
||||||
@ -738,10 +747,12 @@ const FiltersConfigForm = (
|
|||||||
forceUpdate();
|
forceUpdate();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
) : (
|
</StyledFormItem>
|
||||||
|
) : (
|
||||||
|
<StyledFormItem label={<StyledLabel>{t('Dataset')}</StyledLabel>}>
|
||||||
<Loading position="inline-centered" />
|
<Loading position="inline-centered" />
|
||||||
)}
|
</StyledFormItem>
|
||||||
</StyledFormItem>
|
)}
|
||||||
{hasDataset &&
|
{hasDataset &&
|
||||||
Object.keys(mainControlItems).map(
|
Object.keys(mainControlItems).map(
|
||||||
key => mainControlItems[key].element,
|
key => mainControlItems[key].element,
|
||||||
@ -784,11 +795,8 @@ const FiltersConfigForm = (
|
|||||||
required={hasDefaultValue}
|
required={hasDefaultValue}
|
||||||
rules={[
|
rules={[
|
||||||
{
|
{
|
||||||
validator: (rule, value) => {
|
validator: () => {
|
||||||
const hasValue =
|
if (formFilter?.defaultDataMask?.filterState?.value) {
|
||||||
value?.filterState?.value !== null &&
|
|
||||||
value?.filterState?.value !== undefined;
|
|
||||||
if (hasValue) {
|
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
return Promise.reject(
|
return Promise.reject(
|
||||||
|
@ -63,6 +63,7 @@ const filterMock: Filter = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const createProps: () => ControlItemsProps = () => ({
|
const createProps: () => ControlItemsProps = () => ({
|
||||||
|
datasetId: 1,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
forceUpdate: jest.fn(),
|
forceUpdate: jest.fn(),
|
||||||
form: formMock,
|
form: formMock,
|
||||||
|
@ -41,6 +41,7 @@ import { Filter } from '../../types';
|
|||||||
import { ColumnSelect } from './ColumnSelect';
|
import { ColumnSelect } from './ColumnSelect';
|
||||||
|
|
||||||
export interface ControlItemsProps {
|
export interface ControlItemsProps {
|
||||||
|
datasetId: number;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
forceUpdate: Function;
|
forceUpdate: Function;
|
||||||
form: FormInstance<NativeFiltersForm>;
|
form: FormInstance<NativeFiltersForm>;
|
||||||
@ -56,6 +57,7 @@ const CleanFormItem = styled(FormItem)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export default function getControlItemsMap({
|
export default function getControlItemsMap({
|
||||||
|
datasetId,
|
||||||
disabled,
|
disabled,
|
||||||
forceUpdate,
|
forceUpdate,
|
||||||
form,
|
form,
|
||||||
@ -87,7 +89,6 @@ export default function getControlItemsMap({
|
|||||||
filterToEdit?.controlValues?.[mainControlItem.name] ??
|
filterToEdit?.controlValues?.[mainControlItem.name] ??
|
||||||
mainControlItem?.config?.default;
|
mainControlItem?.config?.default;
|
||||||
const initColumn = filterToEdit?.targets[0]?.column?.name;
|
const initColumn = filterToEdit?.targets[0]?.column?.name;
|
||||||
const datasetId = formFilter?.dataset?.value;
|
|
||||||
|
|
||||||
const element = (
|
const element = (
|
||||||
<>
|
<>
|
||||||
|
@ -28,7 +28,7 @@ import {
|
|||||||
TimeGrainFilterPlugin,
|
TimeGrainFilterPlugin,
|
||||||
} from 'src/filters/components';
|
} from 'src/filters/components';
|
||||||
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
import { render, screen, waitFor } from 'spec/helpers/testing-library';
|
||||||
import mockDatasource, { datasourceId } from 'spec/fixtures/mockDatasource';
|
import mockDatasource, { id, datasourceId } from 'spec/fixtures/mockDatasource';
|
||||||
import chartQueries from 'spec/fixtures/mockChartQueries';
|
import chartQueries from 'spec/fixtures/mockChartQueries';
|
||||||
import {
|
import {
|
||||||
FiltersConfigModal,
|
FiltersConfigModal,
|
||||||
@ -71,9 +71,9 @@ const noTemporalColumnsState = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchMock.get('glob:*/api/v1/dataset/1', {
|
const datasetResult = (id: number) => ({
|
||||||
description_columns: {},
|
description_columns: {},
|
||||||
id: 1,
|
id,
|
||||||
label_columns: {
|
label_columns: {
|
||||||
columns: 'Columns',
|
columns: 'Columns',
|
||||||
table_name: 'Table Name',
|
table_name: 'Table Name',
|
||||||
@ -87,11 +87,14 @@ fetchMock.get('glob:*/api/v1/dataset/1', {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
table_name: 'birth_names',
|
table_name: 'birth_names',
|
||||||
id: 1,
|
id,
|
||||||
},
|
},
|
||||||
show_columns: ['id', 'table_name'],
|
show_columns: ['id', 'table_name'],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fetchMock.get('glob:*/api/v1/dataset/1', datasetResult(1));
|
||||||
|
fetchMock.get(`glob:*/api/v1/dataset/${id}`, datasetResult(id));
|
||||||
|
|
||||||
fetchMock.post('glob:*/api/v1/chart/data', {
|
fetchMock.post('glob:*/api/v1/chart/data', {
|
||||||
result: [
|
result: [
|
||||||
{
|
{
|
||||||
@ -320,6 +323,11 @@ test('validates the pre-filter value', async () => {
|
|||||||
|
|
||||||
test("doesn't render time range pre-filter if there are no temporal columns in datasource", async () => {
|
test("doesn't render time range pre-filter if there are no temporal columns in datasource", async () => {
|
||||||
defaultRender(noTemporalColumnsState());
|
defaultRender(noTemporalColumnsState());
|
||||||
|
userEvent.click(screen.getByText(DATASET_REGEX));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.queryByLabelText('Loading')).not.toBeInTheDocument();
|
||||||
|
userEvent.click(screen.getByText('birth_names'));
|
||||||
|
});
|
||||||
userEvent.click(screen.getByText(ADVANCED_REGEX));
|
userEvent.click(screen.getByText(ADVANCED_REGEX));
|
||||||
userEvent.click(getCheckbox(PRE_FILTER_REGEX));
|
userEvent.click(getCheckbox(PRE_FILTER_REGEX));
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
|
Loading…
Reference in New Issue
Block a user