mirror of https://github.com/apache/superset.git
feat(native-filters): enable filter indicator and make datasource optional (#13148)
* refactor: continue decouple native filters * refactor(native-filters): decouple native filter parms from config modal * lint: fix TS errors * lint: fix TS errors * refactor: update filter badge * refactor: rename ant filters * test: fix tests * test: fix tests * refactor: remove 18 from dataset
This commit is contained in:
parent
5ab613d89e
commit
5aa38ef929
|
@ -19,7 +19,16 @@
|
|||
export default {
|
||||
id: 1234,
|
||||
slug: 'dashboardSlug',
|
||||
metadata: {},
|
||||
metadata: {
|
||||
filter_configuration: [
|
||||
{
|
||||
id: 'DefaultsID',
|
||||
filterType: 'filter_select',
|
||||
targets: [{}],
|
||||
cascadeParentIds: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
userId: 'mock_user_id',
|
||||
dash_edit_perm: true,
|
||||
dash_save_perm: true,
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { FilterType } from 'src/dashboard/components/nativeFilters/types';
|
||||
import { NativeFiltersState } from 'src/dashboard/reducers/types';
|
||||
|
||||
export const nativeFilters: NativeFiltersState = {
|
||||
|
@ -24,7 +23,7 @@ export const nativeFilters: NativeFiltersState = {
|
|||
'NATIVE_FILTER-e7Q8zKixx': {
|
||||
id: 'NATIVE_FILTER-e7Q8zKixx',
|
||||
name: 'region',
|
||||
filterType: FilterType.filter_select,
|
||||
filterType: 'filter_select',
|
||||
targets: [
|
||||
{
|
||||
datasetId: 2,
|
||||
|
@ -49,7 +48,7 @@ export const nativeFilters: NativeFiltersState = {
|
|||
'NATIVE_FILTER-x9QPw0so1': {
|
||||
id: 'NATIVE_FILTER-x9QPw0so1',
|
||||
name: 'country_code',
|
||||
filterType: FilterType.filter_select,
|
||||
filterType: 'filter_select',
|
||||
targets: [
|
||||
{
|
||||
datasetId: 2,
|
||||
|
@ -75,6 +74,9 @@ export const nativeFilters: NativeFiltersState = {
|
|||
filtersState: {
|
||||
'NATIVE_FILTER-e7Q8zKixx': {
|
||||
id: 'NATIVE_FILTER-e7Q8zKixx',
|
||||
currentState: {
|
||||
value: ['East Asia & Pacific'],
|
||||
},
|
||||
extraFormData: {
|
||||
append_form_data: {
|
||||
filters: [
|
||||
|
@ -128,6 +130,9 @@ export const singleNativeFiltersState = {
|
|||
[NATIVE_FILTER_ID]: {
|
||||
id: NATIVE_FILTER_ID,
|
||||
extraFormData,
|
||||
currentState: {
|
||||
value: ['No, not an ethnic minority'],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -40,10 +40,25 @@ Object.defineProperty(window, 'matchMedia', {
|
|||
})),
|
||||
});
|
||||
|
||||
jest.mock('@superset-ui/core', () => ({
|
||||
// @ts-ignore
|
||||
...jest.requireActual('@superset-ui/core'),
|
||||
getChartMetadataRegistry: () => ({
|
||||
items: {
|
||||
filter_select: {
|
||||
value: {
|
||||
datasourceCount: 1,
|
||||
behaviors: ['NATIVE_FILTER'],
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('FiltersConfigModal', () => {
|
||||
const mockedProps = {
|
||||
isOpen: true,
|
||||
initialFilterId: 'DefaultFilterId',
|
||||
initialFilterId: 'DefaultsID',
|
||||
createNewOnOpen: true,
|
||||
onCancel: jest.fn(),
|
||||
save: jest.fn(),
|
||||
|
|
|
@ -18,10 +18,10 @@
|
|||
*/
|
||||
export const nativeFiltersInfo = {
|
||||
filters: {
|
||||
DefaultID1: {
|
||||
id: 'DefaultID1',
|
||||
DefaultsID: {
|
||||
id: 'DefaultsID',
|
||||
name: 'test',
|
||||
type: 'text',
|
||||
type: 'filter_select',
|
||||
targets: [
|
||||
{
|
||||
datasetId: 0,
|
||||
|
|
|
@ -18,10 +18,9 @@
|
|||
*/
|
||||
import { TIME_FILTER_MAP } from 'src/explore/constants';
|
||||
import { getChartIdsInFilterScope } from 'src/dashboard/util/activeDashboardFilters';
|
||||
import {
|
||||
NativeFiltersState,
|
||||
NativeFilterState,
|
||||
} from 'src/dashboard/reducers/types';
|
||||
import { NativeFiltersState } from 'src/dashboard/reducers/types';
|
||||
import { getTreeCheckedItems } from '../nativeFilters/FilterConfigModal/utils';
|
||||
import { Layout } from '../../types';
|
||||
|
||||
export enum IndicatorStatus {
|
||||
Unset = 'UNSET',
|
||||
|
@ -130,7 +129,7 @@ const getRejectedColumns = (chart: any): Set<string> =>
|
|||
);
|
||||
|
||||
export type Indicator = {
|
||||
column: string;
|
||||
column?: string;
|
||||
name: string;
|
||||
value: string[];
|
||||
status: IndicatorStatus;
|
||||
|
@ -172,50 +171,52 @@ export const selectIndicatorsForChart = (
|
|||
return indicators;
|
||||
};
|
||||
|
||||
const selectNativeIndicatorValue = (
|
||||
filterState: NativeFilterState,
|
||||
): string[] => {
|
||||
const filters = filterState?.extraFormData?.append_form_data?.filters;
|
||||
if (filters?.length) {
|
||||
const filter = filters[0];
|
||||
if ('val' in filter) {
|
||||
const val = filter.val as string | string[];
|
||||
if (Array.isArray(val)) {
|
||||
return val;
|
||||
}
|
||||
return [val];
|
||||
}
|
||||
}
|
||||
return [];
|
||||
};
|
||||
|
||||
export const selectNativeIndicatorsForChart = (
|
||||
nativeFilters: NativeFiltersState,
|
||||
chartId: number,
|
||||
charts: any,
|
||||
dashboardLayout: Layout,
|
||||
): Indicator[] => {
|
||||
const chart = charts[chartId];
|
||||
|
||||
const appliedColumns = getAppliedColumns(chart);
|
||||
const rejectedColumns = getRejectedColumns(chart);
|
||||
|
||||
const getStatus = (column: string, value: string[]): IndicatorStatus => {
|
||||
if (rejectedColumns.has(column)) return IndicatorStatus.Incompatible;
|
||||
if (appliedColumns.has(column) && value.length > 0) {
|
||||
const getStatus = (
|
||||
value: string[],
|
||||
isAffectedByScope: boolean,
|
||||
column?: string,
|
||||
): IndicatorStatus => {
|
||||
if (!column && isAffectedByScope) {
|
||||
// Filter without datasource
|
||||
return IndicatorStatus.Applied;
|
||||
}
|
||||
if (column && rejectedColumns.has(column))
|
||||
return IndicatorStatus.Incompatible;
|
||||
if (column && appliedColumns.has(column) && value.length > 0) {
|
||||
return IndicatorStatus.Applied;
|
||||
}
|
||||
return IndicatorStatus.Unset;
|
||||
};
|
||||
|
||||
const indicators = Object.values(nativeFilters.filters).map(nativeFilter => {
|
||||
const column = nativeFilter.targets[0].column.name;
|
||||
const isAffectedByScope = getTreeCheckedItems(
|
||||
nativeFilter.scope,
|
||||
dashboardLayout,
|
||||
).some(
|
||||
layoutItem => dashboardLayout[layoutItem]?.meta?.chartId === chartId,
|
||||
);
|
||||
const column = nativeFilter.targets[0]?.column?.name;
|
||||
const filterState = nativeFilters.filtersState[nativeFilter.id];
|
||||
const value = selectNativeIndicatorValue(filterState);
|
||||
let value = filterState?.currentState?.value ?? [];
|
||||
if (!Array.isArray(value)) {
|
||||
value = [value];
|
||||
}
|
||||
return {
|
||||
column,
|
||||
name: nativeFilter.name,
|
||||
path: [nativeFilter.id],
|
||||
status: getStatus(column, value),
|
||||
status: getStatus(value, isAffectedByScope, column),
|
||||
value,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -51,15 +51,19 @@ const FilterValue: React.FC<FilterProps> = ({
|
|||
const { id, targets, filterType } = filter;
|
||||
const cascadingFilters = useCascadingFilters(id);
|
||||
const filterState = useFilterState(id);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
const [state, setState] = useState([]);
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
const [formData, setFormData] = useState<Partial<QueryFormData>>({});
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [target] = targets;
|
||||
const { datasetId = 18, column } = target;
|
||||
const {
|
||||
datasetId,
|
||||
column = {},
|
||||
}: Partial<{ datasetId: number; column: { name?: string } }> = target;
|
||||
const { name: groupby } = column;
|
||||
const currentValue = filterState.currentState?.value;
|
||||
const hasDataSource = !!(datasetId && groupby);
|
||||
const [loading, setLoading] = useState<boolean>(hasDataSource);
|
||||
useEffect(() => {
|
||||
const newFormData = getFormData({
|
||||
datasetId,
|
||||
|
@ -71,6 +75,9 @@ const FilterValue: React.FC<FilterProps> = ({
|
|||
});
|
||||
if (!areObjectsEqual(formData || {}, newFormData)) {
|
||||
setFormData(newFormData);
|
||||
if (!hasDataSource) {
|
||||
return;
|
||||
}
|
||||
getChartDataRequest({
|
||||
formData: newFormData,
|
||||
force: false,
|
||||
|
@ -131,7 +138,8 @@ const FilterValue: React.FC<FilterProps> = ({
|
|||
height={20}
|
||||
width={220}
|
||||
formData={formData}
|
||||
queriesData={state}
|
||||
// For charts that don't have datasource we need workaround for empty placeholder
|
||||
queriesData={hasDataSource ? state : [{ data: [null] }]}
|
||||
chartType={filterType}
|
||||
// @ts-ignore (update superset-ui)
|
||||
hooks={{ setExtraFormData }}
|
||||
|
|
|
@ -21,6 +21,8 @@ import {
|
|||
SuperChart,
|
||||
t,
|
||||
getChartControlPanelRegistry,
|
||||
getChartMetadataRegistry,
|
||||
Behavior,
|
||||
} from '@superset-ui/core';
|
||||
import { FormInstance } from 'antd/lib/form';
|
||||
import React, { useCallback } from 'react';
|
||||
|
@ -39,15 +41,10 @@ import { CustomControlItem } from '@superset-ui/chart-controls';
|
|||
import { ColumnSelect } from './ColumnSelect';
|
||||
import { NativeFiltersForm } from './types';
|
||||
import FilterScope from './FilterScope';
|
||||
import {
|
||||
FilterTypeNames,
|
||||
getControlItems,
|
||||
setFilterFieldValues,
|
||||
useForceUpdate,
|
||||
} from './utils';
|
||||
import { getControlItems, setFilterFieldValues, useForceUpdate } from './utils';
|
||||
import { useBackendFormUpdate } from './state';
|
||||
import { getFormData } from '../utils';
|
||||
import { Filter, FilterType } from '../types';
|
||||
import { Filter } from '../types';
|
||||
|
||||
type DatasetSelectValue = {
|
||||
value: number;
|
||||
|
@ -121,9 +118,25 @@ export const FilterConfigForm: React.FC<FilterConfigFormProps> = ({
|
|||
const controlItems = getControlItems(
|
||||
controlPanelRegistry.get(formFilter?.filterType),
|
||||
);
|
||||
useBackendFormUpdate(form, filterId, filterToEdit);
|
||||
|
||||
const initDatasetId = filterToEdit?.targets[0].datasetId;
|
||||
const nativeFilterItems = getChartMetadataRegistry().items;
|
||||
const nativeFilterVizTypes = Object.entries(nativeFilterItems)
|
||||
// @ts-ignore
|
||||
.filter(([, { value }]) =>
|
||||
value.behaviors?.includes(Behavior.NATIVE_FILTER),
|
||||
)
|
||||
.map(([key]) => key);
|
||||
|
||||
// @ts-ignore
|
||||
const hasDatasource = !!nativeFilterItems[formFilter?.filterType]?.value
|
||||
?.datasourceCount;
|
||||
|
||||
const hasFilledDatasource =
|
||||
(formFilter?.dataset && formFilter?.column) || !hasDatasource;
|
||||
|
||||
useBackendFormUpdate(form, filterId, filterToEdit, hasDatasource);
|
||||
|
||||
const initDatasetId = filterToEdit?.targets[0]?.datasetId;
|
||||
const initColumn = filterToEdit?.targets[0]?.column?.name;
|
||||
const newFormData = getFormData({
|
||||
datasetId: formFilter?.dataset?.value,
|
||||
|
@ -179,69 +192,76 @@ export const FilterConfigForm: React.FC<FilterConfigFormProps> = ({
|
|||
<Input />
|
||||
</StyledFormItem>
|
||||
<StyledFormItem
|
||||
name={['filters', filterId, 'dataset']}
|
||||
initialValue={{ value: initDatasetId }}
|
||||
label={<StyledLabel>{t('Datasource')}</StyledLabel>}
|
||||
rules={[{ required: !removed, message: t('Datasource is required') }]}
|
||||
data-test="datasource-input"
|
||||
name={['filters', filterId, 'filterType']}
|
||||
rules={[{ required: !removed, message: t('Name is required') }]}
|
||||
initialValue={filterToEdit?.filterType || 'filter_select'}
|
||||
label={<StyledLabel>{t('Filter Type')}</StyledLabel>}
|
||||
>
|
||||
<SupersetResourceSelect
|
||||
initialId={initDatasetId}
|
||||
resource="dataset"
|
||||
searchColumn="table_name"
|
||||
transformItem={datasetToSelectOption}
|
||||
isMulti={false}
|
||||
onError={onDatasetSelectError}
|
||||
onChange={e => {
|
||||
// We need reset column when dataset changed
|
||||
const datasetId = formFilter?.dataset?.value;
|
||||
if (datasetId && e?.value !== datasetId) {
|
||||
setFilterFieldValues(form, filterId, {
|
||||
column: null,
|
||||
});
|
||||
}
|
||||
<Select
|
||||
options={nativeFilterVizTypes.map(filterType => ({
|
||||
value: filterType,
|
||||
// @ts-ignore
|
||||
label: nativeFilterItems[filterType]?.value.name,
|
||||
}))}
|
||||
onChange={({ value }: { value: string }) => {
|
||||
setFilterFieldValues(form, filterId, {
|
||||
filterType: value,
|
||||
defaultValue: null,
|
||||
});
|
||||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
</StyledFormItem>
|
||||
</StyledContainer>
|
||||
<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={<StyledLabel>{t('Field')}</StyledLabel>}
|
||||
rules={[{ required: !removed, message: t('Field is required') }]}
|
||||
data-test="field-input"
|
||||
>
|
||||
<ColumnSelect
|
||||
form={form}
|
||||
filterId={filterId}
|
||||
datasetId={formFilter?.dataset?.value}
|
||||
onChange={forceUpdate}
|
||||
/>
|
||||
</StyledFormItem>
|
||||
<StyledFormItem
|
||||
name={['filters', filterId, 'filterType']}
|
||||
rules={[{ required: !removed, message: t('Name is required') }]}
|
||||
initialValue={filterToEdit?.filterType || FilterType.filter_select}
|
||||
label={<StyledLabel>{t('Filter Type')}</StyledLabel>}
|
||||
>
|
||||
<Select
|
||||
options={Object.values(FilterType).map(filterType => ({
|
||||
value: filterType,
|
||||
label: FilterTypeNames[filterType],
|
||||
}))}
|
||||
onChange={({ value }: { value: FilterType }) => {
|
||||
setFilterFieldValues(form, filterId, {
|
||||
filterType: value,
|
||||
defaultValue: null,
|
||||
});
|
||||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
</StyledFormItem>
|
||||
{formFilter?.dataset && formFilter?.column && (
|
||||
{hasDatasource && (
|
||||
<>
|
||||
<StyledFormItem
|
||||
name={['filters', filterId, 'dataset']}
|
||||
initialValue={{ value: initDatasetId }}
|
||||
label={<StyledLabel>{t('Datasource')}</StyledLabel>}
|
||||
rules={[
|
||||
{ required: !removed, message: t('Datasource is required') },
|
||||
]}
|
||||
data-test="datasource-input"
|
||||
>
|
||||
<SupersetResourceSelect
|
||||
initialId={initDatasetId}
|
||||
resource="dataset"
|
||||
searchColumn="table_name"
|
||||
transformItem={datasetToSelectOption}
|
||||
isMulti={false}
|
||||
onError={onDatasetSelectError}
|
||||
onChange={e => {
|
||||
// We need reset column when dataset changed
|
||||
const datasetId = formFilter?.dataset?.value;
|
||||
if (datasetId && e?.value !== datasetId) {
|
||||
setFilterFieldValues(form, filterId, {
|
||||
column: null,
|
||||
});
|
||||
}
|
||||
forceUpdate();
|
||||
}}
|
||||
/>
|
||||
</StyledFormItem>
|
||||
<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={<StyledLabel>{t('Field')}</StyledLabel>}
|
||||
rules={[{ required: !removed, message: t('Field is required') }]}
|
||||
data-test="field-input"
|
||||
>
|
||||
<ColumnSelect
|
||||
form={form}
|
||||
filterId={filterId}
|
||||
datasetId={formFilter?.dataset?.value}
|
||||
onChange={forceUpdate}
|
||||
/>
|
||||
</StyledFormItem>
|
||||
</>
|
||||
)}
|
||||
{hasFilledDatasource && (
|
||||
<CleanFormItem
|
||||
name={['filters', filterId, 'defaultValueFormData']}
|
||||
hidden
|
||||
|
@ -259,25 +279,29 @@ export const FilterConfigForm: React.FC<FilterConfigFormProps> = ({
|
|||
data-test="default-input"
|
||||
label={<StyledLabel>{t('Default Value')}</StyledLabel>}
|
||||
>
|
||||
{formFilter?.dataset &&
|
||||
formFilter?.column &&
|
||||
formFilter?.defaultValueQueriesData && (
|
||||
<SuperChart
|
||||
height={25}
|
||||
width={250}
|
||||
formData={newFormData}
|
||||
queriesData={formFilter?.defaultValueQueriesData}
|
||||
chartType={formFilter?.filterType}
|
||||
hooks={{
|
||||
setExtraFormData: ({ currentState }) => {
|
||||
setFilterFieldValues(form, filterId, {
|
||||
defaultValue: currentState?.value,
|
||||
});
|
||||
forceUpdate();
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{((hasFilledDatasource && formFilter?.defaultValueQueriesData) ||
|
||||
!hasDatasource) && (
|
||||
<SuperChart
|
||||
height={25}
|
||||
width={250}
|
||||
formData={newFormData}
|
||||
// For charts that don't have datasource we need workaround for empty placeholder
|
||||
queriesData={
|
||||
hasDatasource
|
||||
? formFilter?.defaultValueQueriesData
|
||||
: [{ data: [null] }]
|
||||
}
|
||||
chartType={formFilter?.filterType}
|
||||
hooks={{
|
||||
setExtraFormData: ({ currentState }) => {
|
||||
setFilterFieldValues(form, filterId, {
|
||||
defaultValue: currentState?.value,
|
||||
});
|
||||
forceUpdate();
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</StyledFormItem>
|
||||
<StyledFormItem
|
||||
name={['filters', filterId, 'parentFilter']}
|
||||
|
|
|
@ -400,20 +400,22 @@ export function FilterConfigModal({
|
|||
const formInputs = values.filters[id];
|
||||
// if user didn't open a filter, return the original config
|
||||
if (!formInputs) return filterConfigMap[id];
|
||||
let target = {};
|
||||
if (formInputs.dataset && formInputs.column) {
|
||||
target = {
|
||||
datasetId: formInputs.dataset.value,
|
||||
column: {
|
||||
name: formInputs.column,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
id,
|
||||
controlValues: formInputs.controlValues,
|
||||
name: formInputs.name,
|
||||
filterType: formInputs.filterType,
|
||||
// for now there will only ever be one target
|
||||
targets: [
|
||||
{
|
||||
datasetId: formInputs.dataset.value,
|
||||
column: {
|
||||
name: formInputs.column,
|
||||
},
|
||||
},
|
||||
],
|
||||
targets: [target],
|
||||
defaultValue: formInputs.defaultValue || null,
|
||||
cascadeParentIds: formInputs.parentFilter
|
||||
? [formInputs.parentFilter.value]
|
||||
|
|
|
@ -1,67 +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 { styled } from '@superset-ui/core';
|
||||
import { Button } from 'src/common/components';
|
||||
import Icon from 'src/components/Icon';
|
||||
import { useFilterConfiguration } from '../state';
|
||||
|
||||
interface Args {
|
||||
filter: any;
|
||||
index: number;
|
||||
}
|
||||
|
||||
interface FiltersListProps {
|
||||
setEditFilter: (arg0: Args) => void;
|
||||
setDataset: (arg0: any) => void;
|
||||
}
|
||||
const FiltersStyle = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
const FiltersList = ({ setEditFilter, setDataset }: FiltersListProps) => {
|
||||
const filterConfigs = useFilterConfiguration();
|
||||
<>
|
||||
{filterConfigs.map((filter, i: number) => (
|
||||
<FiltersStyle>
|
||||
<Button
|
||||
type="link"
|
||||
key={filter.name}
|
||||
onClick={() => {
|
||||
setEditFilter({ filter, index: i });
|
||||
setDataset(filter.targets[0].datasetId);
|
||||
}}
|
||||
>
|
||||
{filter.name}
|
||||
</Button>
|
||||
<span
|
||||
role="button"
|
||||
title="Edit dashboard"
|
||||
tabIndex={0}
|
||||
className="action-button"
|
||||
>
|
||||
<Icon name="trash" />
|
||||
</span>
|
||||
</FiltersStyle>
|
||||
))}
|
||||
</>;
|
||||
};
|
||||
|
||||
export default FiltersList;
|
|
@ -79,11 +79,16 @@ export const useBackendFormUpdate = (
|
|||
form: FormInstance<NativeFiltersForm>,
|
||||
filterId: string,
|
||||
filterToEdit?: Filter,
|
||||
hasDatasource?: boolean,
|
||||
) => {
|
||||
const forceUpdate = useForceUpdate();
|
||||
const formFilter = (form.getFieldValue('filters') || {})[filterId];
|
||||
useEffect(() => {
|
||||
let resolvedDefaultValue: any = null;
|
||||
if (!hasDatasource) {
|
||||
forceUpdate();
|
||||
return;
|
||||
}
|
||||
// No need to check data set change because it cascading update column
|
||||
// So check that column exists is enough
|
||||
if (!formFilter?.column) {
|
||||
|
@ -107,7 +112,7 @@ export const useBackendFormUpdate = (
|
|||
if (
|
||||
filterToEdit?.filterType === formFilter?.filterType &&
|
||||
filterToEdit?.targets[0].datasetId === formFilter?.dataset?.value &&
|
||||
formFilter?.column === filterToEdit?.targets[0]?.column?.name
|
||||
formFilter?.column === filterToEdit?.targets[0].column?.name
|
||||
) {
|
||||
resolvedDefaultValue = filterToEdit?.defaultValue;
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
* under the License.
|
||||
*/
|
||||
import { QueryObjectFilterClause } from '@superset-ui/core';
|
||||
import { Column, FilterType, Scope } from '../types';
|
||||
import { Column, Scope } from '../types';
|
||||
|
||||
export enum Scoping {
|
||||
all,
|
||||
|
@ -30,7 +30,7 @@ export type AntCallback = (value1?: any, value2?: any) => void;
|
|||
export interface NativeFiltersFormItem {
|
||||
scope: Scope;
|
||||
name: string;
|
||||
filterType: FilterType;
|
||||
filterType: string;
|
||||
dataset: {
|
||||
value: number;
|
||||
label: string;
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { t } from '@superset-ui/core';
|
||||
|
||||
import { flatMapDeep } from 'lodash';
|
||||
import { Charts, Layout, LayoutItem } from 'src/dashboard/types';
|
||||
import {
|
||||
|
@ -29,7 +29,7 @@ import React from 'react';
|
|||
import { DASHBOARD_ROOT_ID } from 'src/dashboard/util/constants';
|
||||
import { CustomControlItem } from '@superset-ui/chart-controls';
|
||||
import { TreeItem } from './types';
|
||||
import { FilterType, Scope } from '../types';
|
||||
import { Scope } from '../types';
|
||||
|
||||
export const useForceUpdate = () => {
|
||||
const [, updateState] = React.useState({});
|
||||
|
@ -155,12 +155,6 @@ export const findFilterScope = (
|
|||
};
|
||||
};
|
||||
|
||||
export const FilterTypeNames = {
|
||||
[FilterType.filter_select]: t('Select'),
|
||||
[FilterType.filter_range]: t('Range'),
|
||||
[FilterType.filter_time]: t('Time'),
|
||||
};
|
||||
|
||||
export const setFilterFieldValues = (
|
||||
form: FormInstance,
|
||||
filterId: string,
|
||||
|
|
|
@ -27,12 +27,6 @@ export interface Scope {
|
|||
excluded: number[];
|
||||
}
|
||||
|
||||
export enum FilterType {
|
||||
filter_select = 'filter_select',
|
||||
filter_range = 'filter_range',
|
||||
filter_time = 'filter_time',
|
||||
}
|
||||
|
||||
/** The target of a filter is the datasource/column being filtered */
|
||||
export interface Target {
|
||||
datasetId: number;
|
||||
|
@ -51,10 +45,10 @@ export interface Filter {
|
|||
id: string; // randomly generated at filter creation
|
||||
name: string;
|
||||
scope: Scope;
|
||||
filterType: FilterType;
|
||||
filterType: string;
|
||||
// for now there will only ever be one target
|
||||
// when multiple targets are supported, change this to Target[]
|
||||
targets: [Target];
|
||||
targets: [Partial<Target>];
|
||||
controlValues: {
|
||||
[key: string]: any;
|
||||
};
|
||||
|
|
|
@ -41,26 +41,34 @@ export const getFormData = ({
|
|||
datasetId?: number;
|
||||
inputRef?: RefObject<HTMLInputElement>;
|
||||
cascadingFilters?: object;
|
||||
groupby: string;
|
||||
}): Partial<QueryFormData> => ({
|
||||
adhoc_filters: [],
|
||||
datasource: `${datasetId}__table`,
|
||||
extra_filters: [],
|
||||
extra_form_data: cascadingFilters,
|
||||
granularity_sqla: 'ds',
|
||||
groupby: [groupby],
|
||||
metrics: ['count'],
|
||||
row_limit: 10000,
|
||||
showSearch: true,
|
||||
currentValue,
|
||||
defaultValue,
|
||||
time_range: 'No filter',
|
||||
time_range_endpoints: ['inclusive', 'exclusive'],
|
||||
url_params: {},
|
||||
viz_type: 'filter_select',
|
||||
inputRef,
|
||||
...controlValues,
|
||||
});
|
||||
groupby?: string;
|
||||
}): Partial<QueryFormData> => {
|
||||
let otherProps: { datasource?: string; groupby?: string[] } = {};
|
||||
if (datasetId && groupby) {
|
||||
otherProps = {
|
||||
datasource: `${datasetId}__table`,
|
||||
groupby: [groupby],
|
||||
};
|
||||
}
|
||||
return {
|
||||
adhoc_filters: [],
|
||||
extra_filters: [],
|
||||
extra_form_data: cascadingFilters,
|
||||
granularity_sqla: 'ds',
|
||||
metrics: ['count'],
|
||||
row_limit: 10000,
|
||||
showSearch: true,
|
||||
currentValue,
|
||||
defaultValue,
|
||||
time_range: 'No filter',
|
||||
time_range_endpoints: ['inclusive', 'exclusive'],
|
||||
url_params: {},
|
||||
viz_type: 'filter_select',
|
||||
inputRef,
|
||||
...controlValues,
|
||||
...otherProps,
|
||||
};
|
||||
};
|
||||
|
||||
export function mergeExtraFormData(
|
||||
originalExtra: ExtraFormData,
|
||||
|
|
|
@ -52,7 +52,13 @@ const sortByStatus = (indicators: Indicator[]): Indicator[] => {
|
|||
};
|
||||
|
||||
const mapStateToProps = (
|
||||
{ datasources, dashboardFilters, nativeFilters, charts }: any,
|
||||
{
|
||||
datasources,
|
||||
dashboardFilters,
|
||||
nativeFilters,
|
||||
charts,
|
||||
dashboardLayout: { present },
|
||||
}: any,
|
||||
{ chartId }: FiltersBadgeProps,
|
||||
) => {
|
||||
const dashboardIndicators = selectIndicatorsForChart(
|
||||
|
@ -66,6 +72,7 @@ const mapStateToProps = (
|
|||
nativeFilters,
|
||||
chartId,
|
||||
charts,
|
||||
present,
|
||||
);
|
||||
|
||||
const indicators = uniqWith(
|
||||
|
|
|
@ -19,16 +19,16 @@
|
|||
import { styled } from '@superset-ui/core';
|
||||
import React from 'react';
|
||||
import { Slider } from 'src/common/components';
|
||||
import { AntdPluginFilterRangeProps } from './types';
|
||||
import { AntdPluginFilterStylesProps } from '../types';
|
||||
import { PluginFilterRangeProps } from './types';
|
||||
import { PluginFilterStylesProps } from '../types';
|
||||
import { getRangeExtraFormData } from '../../utils';
|
||||
|
||||
const Styles = styled.div<AntdPluginFilterStylesProps>`
|
||||
const Styles = styled.div<PluginFilterStylesProps>`
|
||||
height: ${({ height }) => height};
|
||||
width: ${({ width }) => width};
|
||||
`;
|
||||
|
||||
export default function AntdRangeFilter(props: AntdPluginFilterRangeProps) {
|
||||
export default function RangeFilterPlugin(props: PluginFilterRangeProps) {
|
||||
const { data, formData, height, width, setExtraFormData, inputRef } = props;
|
||||
const [row] = data;
|
||||
// @ts-ignore
|
|
@ -22,10 +22,10 @@ import controlPanel from './controlPanel';
|
|||
import transformProps from './transformProps';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
|
||||
export default class AntdRangeFilterPlugin extends ChartPlugin {
|
||||
export default class RangeFilterPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
const metadata = new ChartMetadata({
|
||||
name: t('Range filter plugin'),
|
||||
name: t('Range filter'),
|
||||
description: 'Range filter plugin using AntD',
|
||||
behaviors: [Behavior.CROSS_FILTER, Behavior.NATIVE_FILTER],
|
||||
thumbnail,
|
||||
|
@ -34,7 +34,7 @@ export default class AntdRangeFilterPlugin extends ChartPlugin {
|
|||
super({
|
||||
buildQuery,
|
||||
controlPanel,
|
||||
loadChart: () => import('./AntdRangeFilter'),
|
||||
loadChart: () => import('./RangeFilterPlugin'),
|
||||
metadata,
|
||||
transformProps,
|
||||
});
|
||||
|
|
|
@ -22,18 +22,18 @@ import {
|
|||
SetExtraFormDataHook,
|
||||
} from '@superset-ui/core';
|
||||
import { RefObject } from 'react';
|
||||
import { AntdPluginFilterStylesProps } from '../types';
|
||||
import { PluginFilterStylesProps } from '../types';
|
||||
|
||||
interface AntdPluginFilterSelectCustomizeProps {
|
||||
interface PluginFilterSelectCustomizeProps {
|
||||
max?: number;
|
||||
min?: number;
|
||||
}
|
||||
|
||||
export type PluginFilterRangeQueryFormData = QueryFormData &
|
||||
AntdPluginFilterStylesProps &
|
||||
AntdPluginFilterSelectCustomizeProps;
|
||||
PluginFilterStylesProps &
|
||||
PluginFilterSelectCustomizeProps;
|
||||
|
||||
export type AntdPluginFilterRangeProps = AntdPluginFilterStylesProps & {
|
||||
export type PluginFilterRangeProps = PluginFilterStylesProps & {
|
||||
data: DataRecord[];
|
||||
formData: PluginFilterRangeQueryFormData;
|
||||
setExtraFormData: SetExtraFormDataHook;
|
||||
|
|
|
@ -21,10 +21,10 @@ import { action } from '@storybook/addon-actions';
|
|||
import { boolean, withKnobs } from '@storybook/addon-knobs';
|
||||
import { SuperChart, getChartTransformPropsRegistry } from '@superset-ui/core';
|
||||
import { mockQueryDataForCountries } from 'spec/fixtures/mockNativeFilters';
|
||||
import AntdSelectFilterPlugin from './index';
|
||||
import SelectFilterPlugin from './index';
|
||||
import transformProps from './transformProps';
|
||||
|
||||
new AntdSelectFilterPlugin().configure({ key: 'filter_select' }).register();
|
||||
new SelectFilterPlugin().configure({ key: 'filter_select' }).register();
|
||||
|
||||
getChartTransformPropsRegistry().registerValue('filter_select', transformProps);
|
||||
|
|
@ -19,20 +19,18 @@
|
|||
import { styled } from '@superset-ui/core';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Select } from 'src/common/components';
|
||||
import { AntdPluginFilterSelectProps } from './types';
|
||||
import { AntdPluginFilterStylesProps } from '../types';
|
||||
import { PluginFilterSelectProps } from './types';
|
||||
import { PluginFilterStylesProps } from '../types';
|
||||
import { getSelectExtraFormData } from '../../utils';
|
||||
|
||||
const Styles = styled.div<AntdPluginFilterStylesProps>`
|
||||
const Styles = styled.div<PluginFilterStylesProps>`
|
||||
height: ${({ height }) => height};
|
||||
width: ${({ width }) => width};
|
||||
`;
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
export default function AntdPluginFilterSelect(
|
||||
props: AntdPluginFilterSelectProps,
|
||||
) {
|
||||
export default function PluginFilterSelect(props: PluginFilterSelectProps) {
|
||||
const { data, formData, height, width, setExtraFormData } = props;
|
||||
const {
|
||||
defaultValue,
|
|
@ -22,10 +22,10 @@ import controlPanel from './controlPanel';
|
|||
import transformProps from './transformProps';
|
||||
import thumbnail from './images/thumbnail.png';
|
||||
|
||||
export default class AntdFilterSelectPlugin extends ChartPlugin {
|
||||
export default class FilterSelectPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
const metadata = new ChartMetadata({
|
||||
name: t('Select filter plugin'),
|
||||
name: t('Select filter'),
|
||||
description: 'Select filter plugin using AntD',
|
||||
behaviors: [Behavior.CROSS_FILTER, Behavior.NATIVE_FILTER],
|
||||
thumbnail,
|
||||
|
@ -34,7 +34,7 @@ export default class AntdFilterSelectPlugin extends ChartPlugin {
|
|||
super({
|
||||
buildQuery,
|
||||
controlPanel,
|
||||
loadChart: () => import('./AntdSelectFilter'),
|
||||
loadChart: () => import('./SelectFilterPlugin'),
|
||||
metadata,
|
||||
transformProps,
|
||||
});
|
||||
|
|
|
@ -22,9 +22,9 @@ import {
|
|||
SetExtraFormDataHook,
|
||||
} from '@superset-ui/core';
|
||||
import { RefObject } from 'react';
|
||||
import { AntdPluginFilterStylesProps } from '../types';
|
||||
import { PluginFilterStylesProps } from '../types';
|
||||
|
||||
interface AntdPluginFilterSelectCustomizeProps {
|
||||
interface PluginFilterSelectCustomizeProps {
|
||||
defaultValue?: (string | number)[] | null;
|
||||
currentValue?: (string | number)[] | null;
|
||||
enableEmptyFilter: boolean;
|
||||
|
@ -34,17 +34,17 @@ interface AntdPluginFilterSelectCustomizeProps {
|
|||
inputRef?: RefObject<HTMLInputElement>;
|
||||
}
|
||||
|
||||
export type AntdPluginFilterSelectQueryFormData = QueryFormData &
|
||||
AntdPluginFilterStylesProps &
|
||||
AntdPluginFilterSelectCustomizeProps;
|
||||
export type PluginFilterSelectQueryFormData = QueryFormData &
|
||||
PluginFilterStylesProps &
|
||||
PluginFilterSelectCustomizeProps;
|
||||
|
||||
export type AntdPluginFilterSelectProps = AntdPluginFilterStylesProps & {
|
||||
export type PluginFilterSelectProps = PluginFilterStylesProps & {
|
||||
data: DataRecord[];
|
||||
setExtraFormData: SetExtraFormDataHook;
|
||||
formData: AntdPluginFilterSelectQueryFormData;
|
||||
formData: PluginFilterSelectQueryFormData;
|
||||
};
|
||||
|
||||
export const DEFAULT_FORM_DATA: AntdPluginFilterSelectCustomizeProps = {
|
||||
export const DEFAULT_FORM_DATA: PluginFilterSelectCustomizeProps = {
|
||||
defaultValue: null,
|
||||
currentValue: null,
|
||||
enableEmptyFilter: false,
|
||||
|
|
|
@ -19,18 +19,18 @@
|
|||
import { styled } from '@superset-ui/core';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import DateFilterControl from 'src/explore/components/controls/DateFilterControl/DateFilterControl';
|
||||
import { AntdPluginFilterStylesProps } from '../types';
|
||||
import { AntdPluginFilterTimeProps } from './types';
|
||||
import { PluginFilterStylesProps } from '../types';
|
||||
import { PluginFilterTimeProps } from './types';
|
||||
|
||||
const DEFAULT_VALUE = 'Last week';
|
||||
|
||||
const Styles = styled.div<AntdPluginFilterStylesProps>`
|
||||
const Styles = styled.div<PluginFilterStylesProps>`
|
||||
height: ${({ height }) => height}px;
|
||||
width: ${({ width }) => width}px;
|
||||
overflow-x: scroll;
|
||||
`;
|
||||
|
||||
export default function AntdTimeFilter(props: AntdPluginFilterTimeProps) {
|
||||
export default function TimeFilterPlugin(props: PluginFilterTimeProps) {
|
||||
const { formData, setExtraFormData, width } = props;
|
||||
const { defaultValue, currentValue } = formData;
|
||||
|
|
@ -24,15 +24,16 @@ import thumbnail from './images/thumbnail.png';
|
|||
export default class TimeFilterPlugin extends ChartPlugin {
|
||||
constructor() {
|
||||
const metadata = new ChartMetadata({
|
||||
name: t('Time range filter plugin'),
|
||||
name: t('Time filter'),
|
||||
description: 'Custom time filter plugin',
|
||||
behaviors: [Behavior.CROSS_FILTER, Behavior.NATIVE_FILTER],
|
||||
thumbnail,
|
||||
datasourceCount: 0,
|
||||
});
|
||||
|
||||
super({
|
||||
controlPanel,
|
||||
loadChart: () => import('./AntdTimeFilter'),
|
||||
loadChart: () => import('./TimeFilterPlugin'),
|
||||
metadata,
|
||||
transformProps,
|
||||
});
|
||||
|
|
|
@ -21,21 +21,21 @@ import {
|
|||
DataRecord,
|
||||
SetExtraFormDataHook,
|
||||
} from '@superset-ui/core';
|
||||
import { AntdPluginFilterStylesProps } from '../types';
|
||||
import { PluginFilterStylesProps } from '../types';
|
||||
|
||||
interface PluginFilterTimeCustomizeProps {
|
||||
defaultValue?: string | null;
|
||||
currentValue?: string | null;
|
||||
}
|
||||
|
||||
export type AntdPluginFilterSelectQueryFormData = QueryFormData &
|
||||
AntdPluginFilterStylesProps &
|
||||
export type PluginFilterSelectQueryFormData = QueryFormData &
|
||||
PluginFilterStylesProps &
|
||||
PluginFilterTimeCustomizeProps;
|
||||
|
||||
export type AntdPluginFilterTimeProps = AntdPluginFilterStylesProps & {
|
||||
export type PluginFilterTimeProps = PluginFilterStylesProps & {
|
||||
data: DataRecord[];
|
||||
setExtraFormData: SetExtraFormDataHook;
|
||||
formData: AntdPluginFilterSelectQueryFormData;
|
||||
formData: PluginFilterSelectQueryFormData;
|
||||
};
|
||||
|
||||
export const DEFAULT_FORM_DATA: PluginFilterTimeCustomizeProps = {
|
||||
|
|
|
@ -16,6 +16,6 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export { default as AntdSelectFilterPlugin } from './Select';
|
||||
export { default as AntdRangeFilterPlugin } from './Range';
|
||||
export { default as SelectFilterPlugin } from './Select';
|
||||
export { default as RangeFilterPlugin } from './Range';
|
||||
export { default as TimeFilterPlugin } from './Time';
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export interface AntdPluginFilterStylesProps {
|
||||
export interface PluginFilterStylesProps {
|
||||
height: number;
|
||||
width: number;
|
||||
}
|
||||
|
|
|
@ -60,8 +60,8 @@ import {
|
|||
EchartsTimeseriesChartPlugin,
|
||||
} from '@superset-ui/plugin-chart-echarts';
|
||||
import {
|
||||
AntdSelectFilterPlugin,
|
||||
AntdRangeFilterPlugin,
|
||||
SelectFilterPlugin,
|
||||
RangeFilterPlugin,
|
||||
TimeFilterPlugin,
|
||||
} from 'src/filters/components/';
|
||||
import FilterBoxChartPlugin from '../FilterBox/FilterBoxChartPlugin';
|
||||
|
@ -112,8 +112,8 @@ export default class MainPreset extends Preset {
|
|||
new EchartsTimeseriesChartPlugin().configure({
|
||||
key: 'echarts_timeseries',
|
||||
}),
|
||||
new AntdSelectFilterPlugin().configure({ key: 'filter_select' }),
|
||||
new AntdRangeFilterPlugin().configure({ key: 'filter_range' }),
|
||||
new SelectFilterPlugin().configure({ key: 'filter_select' }),
|
||||
new RangeFilterPlugin().configure({ key: 'filter_range' }),
|
||||
new TimeFilterPlugin().configure({ key: 'filter_time' }),
|
||||
],
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue