mirror of
https://github.com/apache/superset.git
synced 2024-09-06 13:57:40 -04:00
chore: Improves the Select component UI/UX - iteration 4 (#15480)
This commit is contained in:
parent
2aa889944d
commit
ad773ffe79
@ -62,10 +62,13 @@ export function findValue<OptionType extends OptionTypeBase>(
|
||||
|
||||
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)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
@ -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(<SupersetResourceSelect {...mockedProps} />);
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the Select... placeholder', () => {
|
||||
render(<SupersetResourceSelect {...mockedProps} />);
|
||||
expect(screen.getByText('Select...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the Loading... message', () => {
|
||||
render(<SupersetResourceSelect {...mockedProps} />);
|
||||
const select = screen.getByText('Select...');
|
||||
userEvent.click(select);
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the No options message', async () => {
|
||||
render(<SupersetResourceSelect {...mockedProps} />);
|
||||
const select = screen.getByText('Select...');
|
||||
userEvent.click(select);
|
||||
expect(await screen.findByText('No options')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should render the typed text', async () => {
|
||||
render(<SupersetResourceSelect {...mockedProps} />);
|
||||
const select = screen.getByText('Select...');
|
||||
userEvent.click(select);
|
||||
userEvent.type(select, 'typed text');
|
||||
expect(await screen.findByText('typed text')).toBeInTheDocument();
|
||||
});
|
@ -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<V> = { value: V; label: string };
|
||||
|
||||
export interface SupersetResourceSelectProps<T = unknown, V = string> {
|
||||
value?: Value<V> | null;
|
||||
initialId?: number | string;
|
||||
onChange?: (value?: Value<V>) => void;
|
||||
isMulti?: boolean;
|
||||
searchColumn?: string;
|
||||
resource?: string; // e.g. "dataset", "dashboard/related/owners"
|
||||
transformItem?: (item: T) => Value<V>;
|
||||
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<string, any>();
|
||||
|
||||
export const cachedSupersetGet = cacheWrapper(
|
||||
SupersetClient.get,
|
||||
localCache,
|
||||
({ endpoint }) => endpoint || '',
|
||||
);
|
||||
|
||||
export default function SupersetResourceSelect<T, V>({
|
||||
value,
|
||||
initialId,
|
||||
onChange,
|
||||
isMulti,
|
||||
resource,
|
||||
searchColumn,
|
||||
transformItem,
|
||||
onError,
|
||||
defaultOptions = true,
|
||||
}: SupersetResourceSelectProps<T, V>) {
|
||||
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<V>, b: Value<V>) => a.label.localeCompare(b.label)),
|
||||
async badResponse => {
|
||||
onError(await getClientErrorObject(badResponse));
|
||||
return [];
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AsyncSelect
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
isMulti={isMulti}
|
||||
loadOptions={loadOptions}
|
||||
defaultOptions={defaultOptions} // true - load options on render
|
||||
cacheOptions
|
||||
filterOption={null} // options are filtered at the api
|
||||
/>
|
||||
);
|
||||
}
|
@ -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<string, any>();
|
||||
@ -128,6 +128,7 @@ export function ColumnSelect({
|
||||
<Select
|
||||
mode={mode}
|
||||
value={mode === 'multiple' ? value || [] : value}
|
||||
ariaLabel={t('Column select')}
|
||||
onChange={onChange}
|
||||
options={options}
|
||||
placeholder={t('Select a column')}
|
||||
|
@ -0,0 +1,125 @@
|
||||
/**
|
||||
* 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, { useCallback, useMemo } from 'react';
|
||||
import rison from 'rison';
|
||||
import { t, SupersetClient } from '@superset-ui/core';
|
||||
import { Select } from 'src/components';
|
||||
import { cacheWrapper } from 'src/utils/cacheWrapper';
|
||||
import {
|
||||
ClientErrorObject,
|
||||
getClientErrorObject,
|
||||
} from 'src/utils/getClientErrorObject';
|
||||
import { datasetToSelectOption } from './utils';
|
||||
|
||||
const PAGE_SIZE = 50;
|
||||
|
||||
const localCache = new Map<string, any>();
|
||||
|
||||
const cachedSupersetGet = cacheWrapper(
|
||||
SupersetClient.get,
|
||||
localCache,
|
||||
({ endpoint }) => endpoint || '',
|
||||
);
|
||||
|
||||
interface DatasetSelectProps {
|
||||
datasetDetails: Record<string, any> | undefined;
|
||||
datasetId: number;
|
||||
onChange: (value: number) => void;
|
||||
value?: { value: number | undefined };
|
||||
}
|
||||
|
||||
const DatasetSelect = ({
|
||||
datasetDetails,
|
||||
datasetId,
|
||||
onChange,
|
||||
value,
|
||||
}: DatasetSelectProps) => {
|
||||
const getErrorMessage = 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');
|
||||
}
|
||||
return errorText;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
// TODO Change offset and limit to page and pageSize
|
||||
const loadDatasetOptions = async (
|
||||
search: string,
|
||||
offset: number,
|
||||
limit: number, // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
) => {
|
||||
const searchColumn = 'table_name';
|
||||
const query = rison.encode({
|
||||
filters: [{ col: searchColumn, opr: 'ct', value: search }],
|
||||
page: Math.floor(offset / PAGE_SIZE),
|
||||
page_size: PAGE_SIZE,
|
||||
order_column: searchColumn,
|
||||
order_direction: 'asc',
|
||||
});
|
||||
return cachedSupersetGet({
|
||||
endpoint: `/api/v1/dataset/?q=${query}`,
|
||||
})
|
||||
.then(response => {
|
||||
const data: {
|
||||
label: string;
|
||||
value: string | number;
|
||||
}[] = response.json.result
|
||||
.map(datasetToSelectOption)
|
||||
.sort((a: { label: string }, b: { label: string }) =>
|
||||
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 {
|
||||
data,
|
||||
totalCount: response.json.count,
|
||||
};
|
||||
})
|
||||
.catch(async error => {
|
||||
const errorMessage = getErrorMessage(await getClientErrorObject(error));
|
||||
throw new Error(errorMessage);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Select
|
||||
ariaLabel={t('Dataset')}
|
||||
value={value?.value}
|
||||
pageSize={PAGE_SIZE}
|
||||
options={loadDatasetOptions}
|
||||
onChange={onChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const MemoizedSelect = (props: DatasetSelectProps) =>
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useMemo(() => <DatasetSelect {...props} />, []);
|
||||
|
||||
export default MemoizedSelect;
|
@ -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<string, any>();
|
||||
|
||||
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<RootState, DatasourcesState>(
|
||||
({ datasources }) => datasources,
|
||||
);
|
||||
|
||||
const charts = useSelector<RootState, ChartsState>(({ 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 };
|
||||
}) => (
|
||||
<Select
|
||||
ariaLabel={t('Parent filter')}
|
||||
placeholder={t('None')}
|
||||
options={parentFilterOptions}
|
||||
allowClear
|
||||
value={value?.value}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
||||
if (removed) {
|
||||
return <RemovedFilter onClick={() => restoreFilter(filterId)} />;
|
||||
}
|
||||
@ -657,6 +673,7 @@ const FiltersConfigForm = (
|
||||
{...getFiltersConfigModalTestId('filter-type')}
|
||||
>
|
||||
<Select
|
||||
ariaLabel={t('Filter type')}
|
||||
options={nativeFilterVizTypes.map(filterType => {
|
||||
// @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')}
|
||||
>
|
||||
<SupersetResourceSelect
|
||||
initialId={initialDatasetId}
|
||||
resource="dataset"
|
||||
searchColumn="table_name"
|
||||
transformItem={datasetToSelectOption}
|
||||
isMulti={false}
|
||||
onError={onDatasetSelectError}
|
||||
defaultOptions={Object.values(loadedDatasets).map(
|
||||
datasetToSelectOption,
|
||||
)}
|
||||
onChange={e => {
|
||||
// We need reset column when dataset changed
|
||||
if (datasetId && e?.value !== datasetId) {
|
||||
setNativeFilterFieldValues(form, filterId, {
|
||||
defaultDataMask: null,
|
||||
column: null,
|
||||
});
|
||||
}
|
||||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
{!datasetId || !hasColumn || datasetDetails ? (
|
||||
<DatasetSelect
|
||||
datasetDetails={datasetDetails}
|
||||
datasetId={initialDatasetId}
|
||||
onChange={(value: number) => {
|
||||
// We need to reset the column when the dataset has changed
|
||||
if (value !== datasetId) {
|
||||
setNativeFilterFieldValues(form, filterId, {
|
||||
dataset: { value },
|
||||
defaultDataMask: null,
|
||||
column: null,
|
||||
});
|
||||
}
|
||||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Loading position="inline-centered" />
|
||||
)}
|
||||
</StyledFormItem>
|
||||
{hasDataset &&
|
||||
Object.keys(mainControlItems).map(
|
||||
@ -851,6 +866,7 @@ const FiltersConfigForm = (
|
||||
name={['filters', filterId, 'parentFilter']}
|
||||
label={<StyledLabel>{t('Parent filter')}</StyledLabel>}
|
||||
initialValue={parentFilter}
|
||||
normalize={value => (value ? { value } : undefined)}
|
||||
data-test="parent-filter-input"
|
||||
required
|
||||
rules={[
|
||||
@ -860,11 +876,7 @@ const FiltersConfigForm = (
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
placeholder={t('None')}
|
||||
options={parentFilterOptions}
|
||||
isClearable
|
||||
/>
|
||||
<ParentSelect />
|
||||
</StyledRowSubFormItem>
|
||||
</CollapsibleControl>
|
||||
)}
|
||||
@ -986,27 +998,17 @@ const FiltersConfigForm = (
|
||||
'controlValues',
|
||||
'sortAscending',
|
||||
]}
|
||||
initialValue={filterToEdit?.controlValues?.sortAscending}
|
||||
initialValue={sort}
|
||||
label={<StyledLabel>{t('Sort type')}</StyledLabel>}
|
||||
>
|
||||
<Select
|
||||
form={form}
|
||||
filterId={filterId}
|
||||
name="sortAscending"
|
||||
options={[
|
||||
{
|
||||
value: true,
|
||||
label: t('Sort ascending'),
|
||||
},
|
||||
{
|
||||
value: false,
|
||||
label: t('Sort descending'),
|
||||
},
|
||||
]}
|
||||
onChange={({ value }: { value: boolean }) =>
|
||||
onSortChanged(value)
|
||||
}
|
||||
/>
|
||||
<Radio.Group
|
||||
onChange={value => {
|
||||
onSortChanged(value.target.value);
|
||||
}}
|
||||
>
|
||||
<Radio value>{t('Sort ascending')}</Radio>
|
||||
<Radio value={false}>{t('Sort descending')}</Radio>
|
||||
</Radio.Group>
|
||||
</StyledRowFormItem>
|
||||
{hasMetrics && (
|
||||
<StyledRowSubFormItem
|
||||
@ -1025,15 +1027,15 @@ const FiltersConfigForm = (
|
||||
}
|
||||
data-test="field-input"
|
||||
>
|
||||
<SelectControl
|
||||
form={form}
|
||||
filterId={filterId}
|
||||
<Select
|
||||
allowClear
|
||||
ariaLabel={t('Sort metric')}
|
||||
name="sortMetric"
|
||||
options={metrics.map((metric: Metric) => ({
|
||||
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,
|
||||
|
@ -101,7 +101,6 @@ export default function getControlItemsMap({
|
||||
/>
|
||||
<StyledFormItem
|
||||
// don't show the column select unless we have a dataset
|
||||
// style={{ display: datasetId == null ? undefined : 'none' }}
|
||||
name={['filters', filterId, 'column']}
|
||||
initialValue={initColumn}
|
||||
label={
|
||||
|
@ -21,6 +21,7 @@ import { FormInstance } from 'antd/lib/form';
|
||||
import React from 'react';
|
||||
import { CustomControlItem, DatasourceMeta } from '@superset-ui/chart-controls';
|
||||
import { Column, ensureIsArray, GenericDataType } from '@superset-ui/core';
|
||||
import { DatasourcesState, ChartsState } from 'src/dashboard/types';
|
||||
|
||||
const FILTERS_FIELD_NAME = 'filters';
|
||||
|
||||
@ -99,3 +100,28 @@ export const doesColumnMatchFilterType = (filterType: string, column: Column) =>
|
||||
!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<string, number>();
|
||||
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;
|
||||
};
|
||||
|
@ -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<FiltersConfigModalProps>,
|
||||
initialState = initialStoreState,
|
||||
) {
|
||||
return render(<FiltersConfigModal {...props} {...overrides} />, {
|
||||
function defaultRender(initialState = defaultState()) {
|
||||
return render(<FiltersConfigModal {...props} />, {
|
||||
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
|
||||
|
@ -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) {
|
||||
</StatusMessage>
|
||||
);
|
||||
}
|
||||
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 (
|
||||
<Styles height={height} width={width}>
|
||||
<FilterPluginStyle height={height} width={width}>
|
||||
<StyledFormItem
|
||||
validateStatus={filterState.validateStatus}
|
||||
{...formItemData}
|
||||
>
|
||||
<StyledSelect
|
||||
<Select
|
||||
allowClear
|
||||
value={value}
|
||||
placeholder={placeholderText}
|
||||
@ -106,22 +113,9 @@ export default function PluginFilterGroupBy(props: PluginFilterGroupByProps) {
|
||||
onBlur={unsetFocusedFilter}
|
||||
onFocus={setFocusedFilter}
|
||||
ref={inputRef}
|
||||
>
|
||||
{columns.map(
|
||||
(row: { column_name: string; verbose_name: string | null }) => {
|
||||
const {
|
||||
column_name: columnName,
|
||||
verbose_name: verboseName,
|
||||
} = row;
|
||||
return (
|
||||
<Option key={columnName} value={columnName}>
|
||||
{verboseName ?? columnName}
|
||||
</Option>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</StyledSelect>
|
||||
options={options}
|
||||
/>
|
||||
</StyledFormItem>
|
||||
</Styles>
|
||||
</FilterPluginStyle>
|
||||
);
|
||||
}
|
||||
|
@ -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 (
|
||||
<Styles height={height} width={width}>
|
||||
<FilterPluginStyle height={height} width={width}>
|
||||
{Number.isNaN(Number(min)) || Number.isNaN(Number(max)) ? (
|
||||
<h4>{t('Chosen non-numeric column')}</h4>
|
||||
) : (
|
||||
@ -196,6 +196,6 @@ export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
|
||||
</Wrapper>
|
||||
</StyledFormItem>
|
||||
)}
|
||||
</Styles>
|
||||
</FilterPluginStyle>
|
||||
);
|
||||
}
|
||||
|
@ -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<string>(formData.groupby);
|
||||
const [col] = groupby;
|
||||
const [initialColtypeMap] = useState(coltypeMap);
|
||||
const [selectedValues, setSelectedValues] = useState<SelectValue>(
|
||||
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 (
|
||||
<Styles height={height} width={width}>
|
||||
<FilterPluginStyle height={height} width={width}>
|
||||
<StyledFormItem
|
||||
validateStatus={filterState.validateStatus}
|
||||
{...formItemData}
|
||||
>
|
||||
<StyledSelect
|
||||
<Select
|
||||
allowClear
|
||||
allowNewOptions
|
||||
// @ts-ignore
|
||||
value={filterState.value || []}
|
||||
disabled={isDisabled}
|
||||
showSearch={showSearch}
|
||||
mode={multiSelect ? 'multiple' : undefined}
|
||||
mode={multiSelect ? 'multiple' : 'single'}
|
||||
placeholder={placeholderText}
|
||||
onSearch={searchWrapper}
|
||||
onSelect={clearSuggestionSearch}
|
||||
onBlur={handleBlur}
|
||||
onDropdownVisibleChange={setIsDropdownVisible}
|
||||
dropdownRender={(
|
||||
originNode: ReactElement & { ref?: RefObject<HTMLElement> },
|
||||
) => {
|
||||
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={<Icon iconSize="m" />}
|
||||
>
|
||||
{sortedData.map(row => {
|
||||
const [value] = groupby.map(col => row[col]);
|
||||
return (
|
||||
// @ts-ignore
|
||||
<Option key={`${value}`} value={value}>
|
||||
{labelFormatter(value, datatype)}
|
||||
</Option>
|
||||
);
|
||||
})}
|
||||
{currentSuggestionSearch &&
|
||||
!ensureIsArray(filterState.value).some(
|
||||
suggestion => suggestion === currentSuggestionSearch,
|
||||
) && (
|
||||
<Option value={currentSuggestionSearch}>
|
||||
{`${t('Create "%s"', currentSuggestionSearch)}`}
|
||||
</Option>
|
||||
)}
|
||||
</StyledSelect>
|
||||
invertSelection={inverseSelection}
|
||||
options={options}
|
||||
/>
|
||||
</StyledFormItem>
|
||||
</Styles>
|
||||
</FilterPluginStyle>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
`;
|
||||
|
||||
|
@ -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(
|
||||
</StatusMessage>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Styles height={height} width={width}>
|
||||
<FilterPluginStyle height={height} width={width}>
|
||||
<StyledFormItem
|
||||
validateStatus={filterState.validateStatus}
|
||||
{...formItemData}
|
||||
>
|
||||
<StyledSelect
|
||||
<Select
|
||||
allowClear
|
||||
value={value}
|
||||
placeholder={placeholderText}
|
||||
@ -106,22 +115,9 @@ export default function PluginFilterTimeColumn(
|
||||
onMouseEnter={setFocusedFilter}
|
||||
onMouseLeave={unsetFocusedFilter}
|
||||
ref={inputRef}
|
||||
>
|
||||
{timeColumns.map(
|
||||
(row: { column_name: string; verbose_name: string | null }) => {
|
||||
const {
|
||||
column_name: columnName,
|
||||
verbose_name: verboseName,
|
||||
} = row;
|
||||
return (
|
||||
<Option key={columnName} value={columnName}>
|
||||
{verboseName ?? columnName}
|
||||
</Option>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</StyledSelect>
|
||||
options={options}
|
||||
/>
|
||||
</StyledFormItem>
|
||||
</Styles>
|
||||
</FilterPluginStyle>
|
||||
);
|
||||
}
|
||||
|
@ -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(
|
||||
</StatusMessage>
|
||||
);
|
||||
}
|
||||
|
||||
const options = (data || []).map(
|
||||
(row: { name: string; duration: string }) => {
|
||||
const { name, duration } = row;
|
||||
return {
|
||||
label: name,
|
||||
value: duration,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
return (
|
||||
<Styles height={height} width={width}>
|
||||
<FilterPluginStyle height={height} width={width}>
|
||||
<StyledFormItem
|
||||
validateStatus={filterState.validateStatus}
|
||||
{...formItemData}
|
||||
>
|
||||
<StyledSelect
|
||||
<Select
|
||||
allowClear
|
||||
value={value}
|
||||
placeholder={placeholderText}
|
||||
@ -116,17 +125,9 @@ export default function PluginFilterTimegrain(
|
||||
onMouseEnter={setFocusedFilter}
|
||||
onMouseLeave={unsetFocusedFilter}
|
||||
ref={inputRef}
|
||||
>
|
||||
{(data || []).map((row: { name: string; duration: string }) => {
|
||||
const { name, duration } = row;
|
||||
return (
|
||||
<Option key={duration} value={duration}>
|
||||
{name}
|
||||
</Option>
|
||||
);
|
||||
})}
|
||||
</StyledSelect>
|
||||
options={options}
|
||||
/>
|
||||
</StyledFormItem>
|
||||
</Styles>
|
||||
</FilterPluginStyle>
|
||||
);
|
||||
}
|
||||
|
@ -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<PluginFilterStylesProps>`
|
||||
export const FilterPluginStyle = styled.div<PluginFilterStylesProps>`
|
||||
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;
|
||||
|
Loading…
Reference in New Issue
Block a user