From 3596459f200e2c3110c7b9d8decb81c414e6ca43 Mon Sep 17 00:00:00 2001
From: ashah65 <137852504+ashah65@users.noreply.github.com>
Date: Tue, 9 Jun 2026 11:52:35 -0400
Subject: [PATCH 1/3] feat(native-filters): make filter dependency support
extensible via plugin registry
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Replace hardcoded ALLOW_DEPENDENCIES list with a registry-based check so
any filter plugin registering with Behavior.NativeFilter automatically
supports the cascade dependency feature — no core changes needed for
new plugins.
Also adds isColumnSelect support in getControlItemsMap to allow plugins
to declare dataset column picker controls.
---
.../FiltersConfigForm/FiltersConfigForm.tsx | 269 ++----------------
.../FiltersConfigForm/getControlItemsMap.tsx | 172 ++++++++---
.../FiltersConfigModal/FiltersConfigModal.tsx | 22 +-
.../FiltersConfigModal/hooks/index.ts | 5 +-
.../hooks/useFilterOperations.ts | 22 +-
5 files changed, 186 insertions(+), 304 deletions(-)
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
index c59fec2483b5..244fd566ce7a 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
@@ -45,16 +45,12 @@ import {
useEffect,
useImperativeHandle,
useMemo,
- useRef,
useState,
RefObject,
memo,
} from 'react';
import rison from 'rison';
-import {
- PluginFilterSelectCustomizeProps,
- SelectFilterOperatorType,
-} from 'src/filters/components/Select/types';
+import { PluginFilterSelectCustomizeProps } from 'src/filters/components/Select/types';
import { useSelector } from 'react-redux';
import { getChartDataRequest } from 'src/components/Chart/chartAction';
import {
@@ -93,7 +89,7 @@ import {
} from 'src/dashboard/components/nativeFilters/utils';
import { DatasetSelectLabel } from 'src/features/datasets/DatasetSelectLabel';
import {
- ALLOW_DEPENDENCIES as TYPES_SUPPORT_DEPENDENCIES,
+ filterSupportsDependencies,
getFiltersConfigModalTestId,
} from '../FiltersConfigModal';
import { FilterRemoval, NativeFiltersForm } from '../types';
@@ -107,7 +103,6 @@ import RemovedFilter from './RemovedFilter';
import { useBackendFormUpdate, useDefaultValue } from './state';
import {
hasTemporalColumns,
- getTimeGrainOptions,
isValidFilterValue,
mostUsedDataset,
setNativeFilterFieldValues,
@@ -120,7 +115,6 @@ import {
INPUT_WIDTH,
} from './constants';
import DependencyList from './DependencyList';
-import { datasetLabel } from 'src/features/semanticLayers/label';
const FORM_ITEM_WIDTH = 260;
@@ -264,12 +258,6 @@ export interface FiltersConfigFormProps {
const FILTERS_WITH_ADHOC_FILTERS = ['filter_select', 'filter_range'];
-const getOptionDataTest = (
- prefix: string,
- value: string | number | undefined,
-) =>
- `${prefix}-${String(value ?? 'undefined').replace(/[^a-zA-Z0-9_-]/g, '-')}`;
-
// TODO: Rename the filter plugins and remove this mapping
const FILTER_TYPE_NAME_MAPPING = {
[t('Select filter')]: t('Value'),
@@ -326,12 +314,6 @@ const FiltersConfigForm = (
const filters = form.getFieldValue('filters');
const formValues = filters?.[filterId];
const formFilter = formValues || undoFormValues || defaultFormFilter;
- const formFilterWithTimeGrains = formFilter as typeof formFilter & {
- time_grains?: string[];
- };
- const filterToEditWithTimeGrains = filterToEdit as
- | (Filter & { time_grains?: string[] })
- | undefined;
const handleModifyFilter = useCallback(() => {
if (onModifyFilter) {
@@ -454,7 +436,7 @@ const FiltersConfigForm = (
formFilter?.filterType,
);
- const canDependOnOtherFilters = TYPES_SUPPORT_DEPENDENCIES.includes(
+ const canDependOnOtherFilters = filterSupportsDependencies(
formFilter?.filterType,
);
@@ -593,11 +575,6 @@ const FiltersConfigForm = (
!!filterToEdit?.adhoc_filters?.length ||
!!filterToEdit?.time_range;
- const hasTimeGrainPreFilter = !!(
- formFilterWithTimeGrains?.time_grains?.length ||
- filterToEditWithTimeGrains?.time_grains?.length
- );
-
const hasEnableSingleValue =
formFilter?.controlValues?.enableSingleValue !== undefined ||
filterToEdit?.controlValues?.enableSingleValue !== undefined;
@@ -647,49 +624,6 @@ const FiltersConfigForm = (
forceUpdate();
};
- const currentOperatorType: SelectFilterOperatorType =
- formFilter?.controlValues?.operatorType ??
- filterToEdit?.controlValues?.operatorType ??
- SelectFilterOperatorType.Exact;
-
- const selectedColumnIsString = useMemo(() => {
- const columnName = formFilter?.column;
- if (!columnName || !datasetDetails?.columns) return true;
- const colMeta = datasetDetails.columns.find(
- (c: { column_name: string }) => c.column_name === columnName,
- );
- if (!colMeta) return true;
- return colMeta.type_generic === GenericDataType.String;
- }, [formFilter?.column, datasetDetails?.columns]);
-
- const onOperatorTypeChanged = (value: SelectFilterOperatorType) => {
- const previous = form.getFieldValue('filters')?.[filterId].controlValues;
- setNativeFilterFieldValues(form, filterId, {
- controlValues: {
- ...previous,
- operatorType: value,
- },
- defaultDataMask: null,
- });
- formChanged();
- forceUpdate();
- };
-
- const prevColumnRef = useRef(formFilter?.column);
- const datasetLoaded = !!datasetDetails?.columns;
- useEffect(() => {
- const columnChanged = prevColumnRef.current !== formFilter?.column;
- if (
- (columnChanged || datasetLoaded) &&
- !selectedColumnIsString &&
- currentOperatorType !== SelectFilterOperatorType.Exact
- ) {
- onOperatorTypeChanged(SelectFilterOperatorType.Exact);
- }
- prevColumnRef.current = formFilter?.column;
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [formFilter?.column, selectedColumnIsString, datasetLoaded]);
-
const validatePreFilter = () =>
setTimeout(
() =>
@@ -751,7 +685,6 @@ const FiltersConfigForm = (
'columns.filterable',
'columns.is_dttm',
'columns.type',
- 'columns.type_generic',
'columns.verbose_name',
'database.id',
'database.database_name',
@@ -765,7 +698,6 @@ const FiltersConfigForm = (
'schema',
'sql',
'table_name',
- 'time_grain_sqla',
],
})}`,
})
@@ -956,16 +888,6 @@ const FiltersConfigForm = (
label: name || pluginKey,
};
})}
- optionRender={option => (
-
- {option.label || option.value}
-
- )}
onChange={value => {
setNativeFilterFieldValues(form, filterId, {
filterType: value,
@@ -1024,16 +946,6 @@ const FiltersConfigForm = (
disabled: isDisabled,
};
})}
- optionRender={option => (
-
- {option.label || option.value}
-
- )}
onChange={value => {
setNativeFilterFieldValues(form, filterId, {
filterType: value,
@@ -1060,7 +972,7 @@ const FiltersConfigForm = (
{datasetLabel()}}
+ label={{t('Dataset')}}
initialValue={
datasetDetails
? {
@@ -1080,10 +992,7 @@ const FiltersConfigForm = (
rules={[
{
required: !isRemoved,
- message:
- datasetLabel() === t('Datasource')
- ? t('Datasource is required')
- : t('Dataset is required'),
+ message: t('Dataset is required'),
},
]}
{...getFiltersConfigModalTestId('datasource-input')}
@@ -1109,7 +1018,7 @@ const FiltersConfigForm = (
) : (
{datasetLabel()}}
+ label={{t('Dataset')}}
>
@@ -1297,78 +1206,6 @@ const FiltersConfigForm = (
)}
- {itemTypeField === 'filter_timegrain' &&
- hasDataset &&
- datasetDetails?.time_grain_sqla &&
- datasetDetails.time_grain_sqla.length > 0 && (
-
- {
- if (!checked) {
- setNativeFilterFieldValues(
- form,
- filterId,
- { time_grains: undefined },
- );
- forceUpdate();
- }
- formChanged();
- }}
- >
-
-
-
-
- )}
{itemTypeField !== 'filter_range' ? (
- {hasMetrics && !isChartCustomization && (
+ {hasMetrics && (
{
- const previous =
- form.getFieldValue('filters')?.[
- filterId
- ].controlValues || {};
- setNativeFilterFieldValues(
- form,
- filterId,
- {
- controlValues: {
- ...previous,
- sortMetric: value,
+ if (value !== undefined) {
+ const previous =
+ form.getFieldValue(
+ 'filters',
+ )?.[filterId].controlValues ||
+ {};
+ setNativeFilterFieldValues(
+ form,
+ filterId,
+ {
+ controlValues: {
+ ...previous,
+ sortMetric: value,
+ },
},
- },
- );
- forceUpdate();
+ );
+ forceUpdate();
+ }
formChanged();
}}
/>
@@ -1661,67 +1501,6 @@ const FiltersConfigForm = (
hidden
initialValue={null}
/>
- {!isChartCustomization &&
- itemTypeField === 'filter_select' && (
-
- {t('Match type')}
-
-
- >
- }
- >
-
- )}
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx
index 7b8f95777230..94d2ff491746 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx
@@ -17,11 +17,14 @@
* under the License.
*/
import { CustomControlItem } from '@superset-ui/chart-controls';
-import { ReactNode } from 'react';
+import { ReactNode, useState, useEffect } from 'react';
+import rison from 'rison';
+import { cachedSupersetGet } from 'src/utils/cachedSupersetGet';
import {
Checkbox,
FormItem,
InfoTooltip,
+ Select,
Tooltip,
type FormInstance,
} from '@superset-ui/core/components';
@@ -64,6 +67,87 @@ const CleanFormItem = styled(FormItem)`
margin-bottom: 0;
`;
+/** Resolves the saved or default initial value for a control. */
+function resolveInitialValue(
+ controlItem: CustomControlItem,
+ filterToEdit?: ControlItemsProps['filterToEdit'],
+ customizationToEdit?: ControlItemsProps['customizationToEdit'],
+) {
+ return (
+ filterToEdit?.controlValues?.[controlItem.name] ??
+ customizationToEdit?.controlValues?.[controlItem.name] ??
+ controlItem?.config?.default ??
+ null
+ );
+}
+
+/** Renders a StyledLabel with an optional description tooltip. */
+function ControlLabel({
+ label,
+ description,
+}: {
+ label?: ReactNode;
+ description?: ReactNode;
+}) {
+ return (
+
+ {label}
+ {description && (
+ <>
+
+
+ >
+ )}
+
+ );
+}
+
+function DatasetColumnSelect({
+ datasetId,
+ value,
+ onChange,
+}: {
+ datasetId?: number;
+ value?: string | null;
+ onChange?: (value: string | null) => void;
+}) {
+ const [{ loadedForId, fetchedColumns }, setFetchState] = useState<{
+ loadedForId?: number;
+ fetchedColumns: string[];
+ }>({ fetchedColumns: [] });
+
+ const loading = !!(datasetId && loadedForId !== datasetId);
+ const options = loadedForId === datasetId ? fetchedColumns : [];
+
+ useEffect(() => {
+ if (!datasetId) return;
+ cachedSupersetGet({
+ endpoint: `/api/v1/dataset/${datasetId}?q=${rison.encode({
+ columns: ['columns.column_name'],
+ })}`,
+ }).then(({ json: { result } }) => {
+ setFetchState({
+ loadedForId: datasetId,
+ fetchedColumns: result.columns
+ .map((col: { column_name: string }) => col.column_name)
+ .filter(Boolean),
+ });
+ });
+ }, [datasetId]);
+
+ return (
+
@@ -171,10 +257,11 @@ export default function getControlItemsMap({
controlItem.name !== 'operatorType',
)
.forEach(controlItem => {
- const initialValue =
- filterToEdit?.controlValues?.[controlItem.name] ??
- customizationToEdit?.controlValues?.[controlItem.name] ??
- controlItem?.config?.default;
+ const initialValue = resolveInitialValue(
+ controlItem,
+ filterToEdit,
+ customizationToEdit,
+ );
const element = (
<>
- <>
- {typeof controlItem.config.label === 'function'
- ? (controlItem.config.label as Function)()
- : controlItem.config.label}
-
- {controlItem.config.description && (
-
- )}
- >
+
@@ -244,6 +318,36 @@ export default function getControlItemsMap({
);
mapControlItems[controlItem.name] = { element, checked: initialValue };
});
+
+ // Render plugin-declared column-picker controls using config hooks
+ controlItems
+ .filter((item: CustomControlItem) => item?.config?.isColumnSelect === true)
+ .forEach(controlItem => {
+ const initialValue = resolveInitialValue(
+ controlItem,
+ filterToEdit,
+ customizationToEdit,
+ );
+ const element = (
+
+ }
+ >
+
+
+ );
+ mapMainControlItems[controlItem.name] = {
+ element,
+ checked: initialValue,
+ };
+ });
return {
controlItems: mapControlItems,
mainControlItems: mapMainControlItems,
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.tsx
index 44ba434ab660..c54fd6fc2caf 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigModal.tsx
@@ -49,7 +49,7 @@ import {
useFilterOperations,
useCustomizationOperations,
useModalSaveLogic,
- ALLOW_DEPENDENCIES,
+ filterSupportsDependencies,
} from './hooks';
import {
getFilterIds,
@@ -57,12 +57,11 @@ import {
isFilterId,
isChartCustomizationId,
transformDividerId,
- isDivider,
} from './utils';
import { ConfigModalContent } from './ConfigModalContent';
import ConfigModalSidebar from './ConfigModalSidebar';
-export { ALLOW_DEPENDENCIES };
+export { filterSupportsDependencies };
const StyledModalBody = styled(BaseModalBody)`
.filters-list {
@@ -471,20 +470,9 @@ function FiltersConfigModal({
[modalSaveLogic, setSaveAlertVisible],
);
- const handleValuesChange = useCallback(
- (changedValues: Partial) => {
- debouncedHandleErroredItems();
- // DividerConfigForm doesn't call handleModifyItem on change the way
- // FiltersConfigForm does, so detect divider field changes here and mark
- // the divider as modified so canSave becomes true and the save payload
- // includes the updated divider values.
- Object.keys(changedValues?.filters ?? {}).forEach(id => {
- if (isDivider(id)) {
- handleModifyItem(id);
- }
- });
- },
- [debouncedHandleErroredItems, handleModifyItem],
+ const handleValuesChange = useMemo(
+ () => debouncedHandleErroredItems,
+ [debouncedHandleErroredItems],
);
const handleActiveFilterPanelChange = useCallback(
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/hooks/index.ts b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/hooks/index.ts
index 8f3879d59d91..e5845fc3e2ca 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/hooks/index.ts
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/hooks/index.ts
@@ -18,7 +18,10 @@
*/
export { useItemStateManager } from './useItemStateManager';
export type { ItemStateManager } from './useItemStateManager';
-export { useFilterOperations, ALLOW_DEPENDENCIES } from './useFilterOperations';
+export {
+ useFilterOperations,
+ filterSupportsDependencies,
+} from './useFilterOperations';
export type {
FilterOperations,
FilterOperationsParams,
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/hooks/useFilterOperations.ts b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/hooks/useFilterOperations.ts
index b57143e85332..eeb7db1bfce3 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/hooks/useFilterOperations.ts
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/hooks/useFilterOperations.ts
@@ -18,17 +18,25 @@
*/
import { useCallback } from 'react';
import { t } from '@apache-superset/core/translation';
-import { Filter, Divider, NativeFilterType } from '@superset-ui/core';
+import {
+ Behavior,
+ Filter,
+ Divider,
+ NativeFilterType,
+ getChartMetadataRegistry,
+} from '@superset-ui/core';
import type { FormInstance } from '@superset-ui/core/components';
import { NativeFiltersForm } from '../types';
import { generateFilterId, hasCircularDependency } from '../utils';
import type { ItemStateManager } from './useItemStateManager';
-export const ALLOW_DEPENDENCIES = [
- 'filter_range',
- 'filter_select',
- 'filter_time',
-];
+export function filterSupportsDependencies(
+ filterType: string | undefined,
+): boolean {
+ if (!filterType) return false;
+ const metadata = getChartMetadataRegistry().get(filterType);
+ return metadata?.behaviors?.includes(Behavior.NativeFilter) ?? false;
+}
interface AvailableFilterOption {
label: string;
@@ -148,7 +156,7 @@ export function useFilterOperations({
return (
component &&
'filterType' in component &&
- ALLOW_DEPENDENCIES.includes(component.filterType)
+ filterSupportsDependencies(component.filterType)
);
},
[filterConfigMap, form, filterState.removedItems],
From 212fbbc71d11f72b3381046dc5e61ef5480cccbd Mon Sep 17 00:00:00 2001
From: ashah65 <137852504+ashah65@users.noreply.github.com>
Date: Tue, 9 Jun 2026 14:28:18 -0400
Subject: [PATCH 2/3] fix(native-filters): address code review findings in
FiltersConfigModal
- Mark divider edits as modified so canSave is enabled for divider changes
- Persist sortMetric=undefined when Select is cleared via allowClear
- Restore datasetLabel() for semantic-layers feature flag consistency
- Restore time_grains allowlist UI for filter_timegrain config
- Restore operatorType (Match type) UI for filter_select config
- Fix ControlLabel to resolve function-typed label/description props
- Fix DatasetColumnSelect: add cancellation, error handling, allowClear
Co-Authored-By: Claude Sonnet 4.6
---
.../FiltersConfigForm/FiltersConfigForm.tsx | 240 ++++++++++++++++--
.../FiltersConfigForm/getControlItemsMap.tsx | 52 +++-
.../FiltersConfigModal/FiltersConfigModal.tsx | 13 +-
3 files changed, 268 insertions(+), 37 deletions(-)
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
index 244fd566ce7a..5cc087a44423 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/FiltersConfigForm.tsx
@@ -45,12 +45,16 @@ import {
useEffect,
useImperativeHandle,
useMemo,
+ useRef,
useState,
RefObject,
memo,
} from 'react';
import rison from 'rison';
-import { PluginFilterSelectCustomizeProps } from 'src/filters/components/Select/types';
+import {
+ PluginFilterSelectCustomizeProps,
+ SelectFilterOperatorType,
+} from 'src/filters/components/Select/types';
import { useSelector } from 'react-redux';
import { getChartDataRequest } from 'src/components/Chart/chartAction';
import {
@@ -102,6 +106,7 @@ import getControlItemsMap from './getControlItemsMap';
import RemovedFilter from './RemovedFilter';
import { useBackendFormUpdate, useDefaultValue } from './state';
import {
+ getTimeGrainOptions,
hasTemporalColumns,
isValidFilterValue,
mostUsedDataset,
@@ -115,6 +120,7 @@ import {
INPUT_WIDTH,
} from './constants';
import DependencyList from './DependencyList';
+import { datasetLabel } from 'src/features/semanticLayers/label';
const FORM_ITEM_WIDTH = 260;
@@ -314,6 +320,12 @@ const FiltersConfigForm = (
const filters = form.getFieldValue('filters');
const formValues = filters?.[filterId];
const formFilter = formValues || undoFormValues || defaultFormFilter;
+ const formFilterWithTimeGrains = formFilter as typeof formFilter & {
+ time_grains?: string[];
+ };
+ const filterToEditWithTimeGrains = filterToEdit as
+ | (Filter & { time_grains?: string[] })
+ | undefined;
const handleModifyFilter = useCallback(() => {
if (onModifyFilter) {
@@ -575,6 +587,11 @@ const FiltersConfigForm = (
!!filterToEdit?.adhoc_filters?.length ||
!!filterToEdit?.time_range;
+ const hasTimeGrainPreFilter = !!(
+ formFilterWithTimeGrains?.time_grains?.length ||
+ filterToEditWithTimeGrains?.time_grains?.length
+ );
+
const hasEnableSingleValue =
formFilter?.controlValues?.enableSingleValue !== undefined ||
filterToEdit?.controlValues?.enableSingleValue !== undefined;
@@ -624,6 +641,49 @@ const FiltersConfigForm = (
forceUpdate();
};
+ const currentOperatorType: SelectFilterOperatorType =
+ formFilter?.controlValues?.operatorType ??
+ filterToEdit?.controlValues?.operatorType ??
+ SelectFilterOperatorType.Exact;
+
+ const selectedColumnIsString = useMemo(() => {
+ const columnName = formFilter?.column;
+ if (!columnName || !datasetDetails?.columns) return true;
+ const colMeta = datasetDetails.columns.find(
+ (c: { column_name: string }) => c.column_name === columnName,
+ );
+ if (!colMeta) return true;
+ return colMeta.type_generic === GenericDataType.String;
+ }, [formFilter?.column, datasetDetails?.columns]);
+
+ const onOperatorTypeChanged = (value: SelectFilterOperatorType) => {
+ const previous = form.getFieldValue('filters')?.[filterId].controlValues;
+ setNativeFilterFieldValues(form, filterId, {
+ controlValues: {
+ ...previous,
+ operatorType: value,
+ },
+ defaultDataMask: null,
+ });
+ formChanged();
+ forceUpdate();
+ };
+
+ const prevColumnRef = useRef(formFilter?.column);
+ const datasetLoaded = !!datasetDetails?.columns;
+ useEffect(() => {
+ const columnChanged = prevColumnRef.current !== formFilter?.column;
+ if (
+ (columnChanged || datasetLoaded) &&
+ !selectedColumnIsString &&
+ currentOperatorType !== SelectFilterOperatorType.Exact
+ ) {
+ onOperatorTypeChanged(SelectFilterOperatorType.Exact);
+ }
+ prevColumnRef.current = formFilter?.column;
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [formFilter?.column, selectedColumnIsString, datasetLoaded]);
+
const validatePreFilter = () =>
setTimeout(
() =>
@@ -685,6 +745,7 @@ const FiltersConfigForm = (
'columns.filterable',
'columns.is_dttm',
'columns.type',
+ 'columns.type_generic',
'columns.verbose_name',
'database.id',
'database.database_name',
@@ -698,6 +759,7 @@ const FiltersConfigForm = (
'schema',
'sql',
'table_name',
+ 'time_grain_sqla',
],
})}`,
})
@@ -972,7 +1034,7 @@ const FiltersConfigForm = (
{t('Dataset')}}
+ label={{datasetLabel()}}
initialValue={
datasetDetails
? {
@@ -992,7 +1054,10 @@ const FiltersConfigForm = (
rules={[
{
required: !isRemoved,
- message: t('Dataset is required'),
+ message:
+ datasetLabel() === t('Datasource')
+ ? t('Datasource is required')
+ : t('Dataset is required'),
},
]}
{...getFiltersConfigModalTestId('datasource-input')}
@@ -1018,7 +1083,7 @@ const FiltersConfigForm = (
) : (
{t('Dataset')}}
+ label={{datasetLabel()}}
>
@@ -1206,6 +1271,78 @@ const FiltersConfigForm = (
)}
+ {itemTypeField === 'filter_timegrain' &&
+ hasDataset &&
+ datasetDetails?.time_grain_sqla &&
+ datasetDetails.time_grain_sqla.length > 0 && (
+
+ {
+ if (!checked) {
+ setNativeFilterFieldValues(
+ form,
+ filterId,
+ { time_grains: undefined },
+ );
+ forceUpdate();
+ }
+ formChanged();
+ }}
+ >
+
+
+
+
+ )}
{itemTypeField !== 'filter_range' ? (
- {hasMetrics && (
+ {hasMetrics && !isChartCustomization && (
{
- if (value !== undefined) {
- const previous =
- form.getFieldValue(
- 'filters',
- )?.[filterId].controlValues ||
- {};
- setNativeFilterFieldValues(
- form,
- filterId,
- {
- controlValues: {
- ...previous,
- sortMetric: value,
- },
+ const previous =
+ form.getFieldValue(
+ 'filters',
+ )?.[filterId].controlValues ||
+ {};
+ setNativeFilterFieldValues(
+ form,
+ filterId,
+ {
+ controlValues: {
+ ...previous,
+ sortMetric: value,
},
- );
- forceUpdate();
- }
+ },
+ );
+ forceUpdate();
formChanged();
}}
/>
@@ -1501,6 +1636,67 @@ const FiltersConfigForm = (
hidden
initialValue={null}
/>
+ {!isChartCustomization &&
+ itemTypeField === 'filter_select' && (
+
+ {t('Match type')}
+
+
+ >
+ }
+ >
+
+ )}
diff --git a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx
index 94d2ff491746..518d887ef7cf 100644
--- a/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx
+++ b/superset-frontend/src/dashboard/components/nativeFilters/FiltersConfigModal/FiltersConfigForm/getControlItemsMap.tsx
@@ -86,16 +86,29 @@ function ControlLabel({
label,
description,
}: {
- label?: ReactNode;
- description?: ReactNode;
+ label?: ReactNode | (() => ReactNode);
+ description?: ReactNode | (() => ReactNode);
}) {
+ const resolvedLabel =
+ typeof label === 'function' ? (label as () => ReactNode)() : label;
+ const resolvedDescription =
+ typeof description === 'function'
+ ? (description as () => ReactNode)()
+ : description;
+ const tooltipText =
+ resolvedDescription != null && typeof resolvedDescription === 'string'
+ ? resolvedDescription
+ : resolvedDescription != null
+ ? String(resolvedDescription)
+ : undefined;
+
return (
- {label}
- {description && (
+ {resolvedLabel}
+ {tooltipText && (
<>
-
+
>
)}
@@ -121,22 +134,35 @@ function DatasetColumnSelect({
useEffect(() => {
if (!datasetId) return;
+ let cancelled = false;
cachedSupersetGet({
endpoint: `/api/v1/dataset/${datasetId}?q=${rison.encode({
columns: ['columns.column_name'],
})}`,
- }).then(({ json: { result } }) => {
- setFetchState({
- loadedForId: datasetId,
- fetchedColumns: result.columns
- .map((col: { column_name: string }) => col.column_name)
- .filter(Boolean),
+ })
+ .then(({ json: { result } }) => {
+ if (!cancelled) {
+ setFetchState({
+ loadedForId: datasetId,
+ fetchedColumns: result.columns
+ .map((col: { column_name: string }) => col.column_name)
+ .filter(Boolean),
+ });
+ }
+ })
+ .catch(() => {
+ if (!cancelled) {
+ setFetchState({ loadedForId: datasetId, fetchedColumns: [] });
+ }
});
- });
+ return () => {
+ cancelled = true;
+ };
}, [datasetId]);
return (