fix(explore): Prevent shared controls from checking feature flags outside React render (#21315)

This commit is contained in:
Cody Leff 2022-09-14 14:41:47 -04:00 committed by GitHub
parent 59ca7861c0
commit 2285ebe72e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 285 additions and 435 deletions

View File

@ -31,11 +31,7 @@ export * from './components/ColumnTypeLabel/ColumnTypeLabel';
export * from './components/MetricOption'; export * from './components/MetricOption';
// React control components // React control components
export { export { default as sharedControls, withDndFallback } from './shared-controls';
sharedControls,
dndEntity,
dndColumnsControl,
} from './shared-controls';
export { default as sharedControlComponents } from './shared-controls/components'; export { default as sharedControlComponents } from './shared-controls/components';
export { legacySortBy } from './shared-controls/legacySortBy'; export { legacySortBy } from './shared-controls/legacySortBy';
export * from './shared-controls/emitFilterControl'; export * from './shared-controls/emitFilterControl';

View File

@ -17,6 +17,7 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import React, { useMemo } from 'react';
import { import {
FeatureFlag, FeatureFlag,
isFeatureEnabled, isFeatureEnabled,
@ -25,43 +26,83 @@ import {
t, t,
validateNonEmpty, validateNonEmpty,
} from '@superset-ui/core'; } from '@superset-ui/core';
import { ExtraControlProps, SharedControlConfig, Dataset } from '../types'; import {
ExtraControlProps,
SharedControlConfig,
Dataset,
Metric,
} from '../types';
import { DATASET_TIME_COLUMN_OPTION, TIME_FILTER_LABELS } from '../constants'; import { DATASET_TIME_COLUMN_OPTION, TIME_FILTER_LABELS } from '../constants';
import { QUERY_TIME_COLUMN_OPTION, defineSavedMetrics } from '..'; import {
QUERY_TIME_COLUMN_OPTION,
defineSavedMetrics,
ColumnOption,
ColumnMeta,
FilterOption,
} from '..';
import { xAxisControlConfig } from './constants'; import { xAxisControlConfig } from './constants';
export const dndGroupByControl: SharedControlConfig<'DndColumnSelect'> = { type Control = {
savedMetrics?: Metric[] | null;
default?: unknown;
};
/*
* Note: Previous to the commit that introduced this comment, the shared controls module
* would check feature flags at module execution time and expose a different control
* configuration (component + props) depending on the status of drag-and-drop feature
* flags. This commit combines those configs, merging the required props for both the
* drag-and-drop and non-drag-and-drop components, and renders a wrapper component that
* checks feature flags at component render time to avoid race conditions between when
* feature flags are set and when they're checked.
*/
export const dndGroupByControl: SharedControlConfig<
'DndColumnSelect' | 'SelectControl',
ColumnMeta
> = {
type: 'DndColumnSelect', type: 'DndColumnSelect',
label: t('Dimensions'), label: t('Dimensions'),
multi: true,
freeForm: true,
clearable: true,
default: [], default: [],
includeTime: false,
description: t( description: t(
'One or many columns to group by. High cardinality groupings should include a series limit ' + 'One or many columns to group by. High cardinality groupings should include a sort by metric ' +
'to limit the number of fetched and rendered series.', 'and series limit to limit the number of fetched and rendered series.',
), ),
mapStateToProps(state, { includeTime }) { optionRenderer: (c: ColumnMeta) => <ColumnOption showType column={c} />,
valueRenderer: (c: ColumnMeta) => <ColumnOption column={c} />,
valueKey: 'column_name',
allowAll: true,
filterOption: ({ data: opt }: FilterOption<ColumnMeta>, text: string) =>
(opt.column_name &&
opt.column_name.toLowerCase().includes(text.toLowerCase())) ||
(opt.verbose_name &&
opt.verbose_name.toLowerCase().includes(text.toLowerCase())) ||
false,
promptTextCreator: (label: unknown) => label,
mapStateToProps(state, controlState) {
const newState: ExtraControlProps = {}; const newState: ExtraControlProps = {};
const { datasource } = state; const { datasource } = state;
if (datasource?.columns[0]?.hasOwnProperty('groupby')) { if (datasource?.columns[0]?.hasOwnProperty('groupby')) {
const options = (datasource as Dataset).columns.filter(c => c.groupby); const options = (datasource as Dataset).columns.filter(c => c.groupby);
if (includeTime) { if (controlState?.includeTime) {
options.unshift(DATASET_TIME_COLUMN_OPTION); options.unshift(DATASET_TIME_COLUMN_OPTION);
} }
newState.options = Object.fromEntries( newState.options = options;
options.map(option => [option.column_name, option]),
);
newState.savedMetrics = (datasource as Dataset).metrics || []; newState.savedMetrics = (datasource as Dataset).metrics || [];
} else { } else {
const options = datasource?.columns; const options = (datasource?.columns as QueryColumn[]) || [];
if (includeTime) { if (controlState?.includeTime) {
(options as QueryColumn[])?.unshift(QUERY_TIME_COLUMN_OPTION); options.unshift(QUERY_TIME_COLUMN_OPTION);
} }
newState.options = Object.fromEntries( newState.options = options;
(options as QueryColumn[])?.map(option => [option.name, option]),
);
newState.options = datasource?.columns;
} }
return newState; return newState;
}, },
commaChoosesOption: false,
}; };
export const dndColumnsControl: typeof dndGroupByControl = { export const dndColumnsControl: typeof dndGroupByControl = {
@ -70,7 +111,7 @@ export const dndColumnsControl: typeof dndGroupByControl = {
description: t('One or many columns to pivot as columns'), description: t('One or many columns to pivot as columns'),
}; };
export const dndSeries: typeof dndGroupByControl = { export const dndSeriesControl: typeof dndGroupByControl = {
...dndGroupByControl, ...dndGroupByControl,
label: t('Dimension'), label: t('Dimension'),
multi: false, multi: false,
@ -82,7 +123,7 @@ export const dndSeries: typeof dndGroupByControl = {
), ),
}; };
export const dndEntity: typeof dndGroupByControl = { export const dndEntityControl: typeof dndGroupByControl = {
...dndGroupByControl, ...dndGroupByControl,
label: t('Entity'), label: t('Entity'),
default: null, default: null,
@ -91,7 +132,9 @@ export const dndEntity: typeof dndGroupByControl = {
description: t('This defines the element to be plotted on the chart'), description: t('This defines the element to be plotted on the chart'),
}; };
export const dnd_adhoc_filters: SharedControlConfig<'DndFilterSelect'> = { export const dndAdhocFilterControl: SharedControlConfig<
'DndFilterSelect' | 'AdhocFilterControl'
> = {
type: 'DndFilterSelect', type: 'DndFilterSelect',
label: t('Filters'), label: t('Filters'),
default: [], default: [],
@ -109,7 +152,9 @@ export const dnd_adhoc_filters: SharedControlConfig<'DndFilterSelect'> = {
provideFormDataToProps: true, provideFormDataToProps: true,
}; };
export const dnd_adhoc_metrics: SharedControlConfig<'DndMetricSelect'> = { export const dndAdhocMetricsControl: SharedControlConfig<
'DndMetricSelect' | 'MetricsControl'
> = {
type: 'DndMetricSelect', type: 'DndMetricSelect',
multi: true, multi: true,
label: t('Metrics'), label: t('Metrics'),
@ -123,20 +168,23 @@ export const dnd_adhoc_metrics: SharedControlConfig<'DndMetricSelect'> = {
description: t('One or many metrics to display'), description: t('One or many metrics to display'),
}; };
export const dnd_adhoc_metric: SharedControlConfig<'DndMetricSelect'> = { export const dndAdhocMetricControl: typeof dndAdhocMetricsControl = {
...dnd_adhoc_metrics, ...dndAdhocMetricsControl,
multi: false, multi: false,
label: t('Metric'), label: t('Metric'),
description: t('Metric'), description: t('Metric'),
}; };
export const dnd_adhoc_metric_2: SharedControlConfig<'DndMetricSelect'> = { export const dndAdhocMetricControl2: typeof dndAdhocMetricControl = {
...dnd_adhoc_metric, ...dndAdhocMetricControl,
label: t('Right Axis Metric'), label: t('Right Axis Metric'),
clearable: true,
description: t('Choose a metric for right axis'), description: t('Choose a metric for right axis'),
}; };
export const dnd_sort_by: SharedControlConfig<'DndMetricSelect'> = { export const dndSortByControl: SharedControlConfig<
'DndMetricSelect' | 'MetricsControl'
> = {
type: 'DndMetricSelect', type: 'DndMetricSelect',
label: t('Sort by'), label: t('Sort by'),
default: null, default: null,
@ -152,33 +200,37 @@ export const dnd_sort_by: SharedControlConfig<'DndMetricSelect'> = {
}), }),
}; };
export const dnd_size: SharedControlConfig<'DndMetricSelect'> = { export const dndSizeControl: typeof dndAdhocMetricControl = {
...dnd_adhoc_metric, ...dndAdhocMetricControl,
label: t('Bubble Size'), label: t('Bubble Size'),
description: t('Metric used to calculate bubble size'), description: t('Metric used to calculate bubble size'),
default: null,
}; };
export const dnd_x: SharedControlConfig<'DndMetricSelect'> = { export const dndXControl: typeof dndAdhocMetricControl = {
...dnd_adhoc_metric, ...dndAdhocMetricControl,
label: t('X Axis'), label: t('X Axis'),
description: t('Metric assigned to the [X] axis'), description: t('Metric assigned to the [X] axis'),
default: null,
}; };
export const dnd_y: SharedControlConfig<'DndMetricSelect'> = { export const dndYControl: typeof dndAdhocMetricControl = {
...dnd_adhoc_metric, ...dndAdhocMetricControl,
label: t('Y Axis'), label: t('Y Axis'),
description: t('Metric assigned to the [Y] axis'), description: t('Metric assigned to the [Y] axis'),
default: null,
}; };
export const dnd_secondary_metric: SharedControlConfig<'DndMetricSelect'> = { export const dndSecondaryMetricControl: typeof dndAdhocMetricControl = {
...dnd_adhoc_metric, ...dndAdhocMetricControl,
label: t('Color Metric'), label: t('Color Metric'),
default: null,
validators: [], validators: [],
description: t('A metric to use for color'), description: t('A metric to use for color'),
}; };
export const dnd_granularity_sqla: typeof dndGroupByControl = { export const dndGranularitySqlaControl: typeof dndSeriesControl = {
...dndSeries, ...dndSeriesControl,
label: TIME_FILTER_LABELS.granularity_sqla, label: TIME_FILTER_LABELS.granularity_sqla,
description: t( description: t(
'The time column for the visualization. Note that you ' + 'The time column for the visualization. Note that you ' +
@ -187,21 +239,20 @@ export const dnd_granularity_sqla: typeof dndGroupByControl = {
'filter below is applied against this column or ' + 'filter below is applied against this column or ' +
'expression', 'expression',
), ),
default: (c: Control) => c.default,
clearable: false,
canDelete: false, canDelete: false,
ghostButtonText: t( ghostButtonText: t('Drop temporal column here'),
isFeatureEnabled(FeatureFlag.ENABLE_DND_WITH_CLICK_UX) clickEnabledGhostButtonText: t('Drop a temporal column here or click'),
? 'Drop a temporal column here or click' optionRenderer: (c: ColumnMeta) => <ColumnOption showType column={c} />,
: 'Drop temporal column here', valueRenderer: (c: ColumnMeta) => <ColumnOption column={c} />,
), valueKey: 'column_name',
mapStateToProps: ({ datasource }) => { mapStateToProps: ({ datasource }) => {
if (datasource?.columns[0]?.hasOwnProperty('column_name')) { if (datasource?.columns[0]?.hasOwnProperty('column_name')) {
const temporalColumns = const temporalColumns =
(datasource as Dataset)?.columns?.filter(c => c.is_dttm) ?? []; (datasource as Dataset)?.columns?.filter(c => c.is_dttm) ?? [];
const options = Object.fromEntries(
temporalColumns.map(option => [option.column_name, option]),
);
return { return {
options, options: temporalColumns,
default: default:
(datasource as Dataset)?.main_dttm_col || (datasource as Dataset)?.main_dttm_col ||
temporalColumns[0]?.column_name || temporalColumns[0]?.column_name ||
@ -209,22 +260,36 @@ export const dnd_granularity_sqla: typeof dndGroupByControl = {
isTemporal: true, isTemporal: true,
}; };
} }
const sortedQueryColumns = (datasource as QueryResponse)?.columns?.sort( const sortedQueryColumns = (datasource as QueryResponse)?.columns?.sort(
query => (query?.is_dttm ? -1 : 1), query => (query?.is_dttm ? -1 : 1),
); );
const options = Object.fromEntries(
sortedQueryColumns.map(option => [option.name, option]),
);
return { return {
options, options: sortedQueryColumns,
default: sortedQueryColumns[0]?.name || null, default: sortedQueryColumns[0]?.name || null,
isTemporal: true, isTemporal: true,
}; };
}, },
}; };
export const dnd_x_axis: SharedControlConfig<'DndColumnSelect'> = { export const dndXAxisControl: typeof dndGroupByControl = {
...dndGroupByControl, ...dndGroupByControl,
...xAxisControlConfig, ...xAxisControlConfig,
}; };
export function withDndFallback(
DndComponent: React.ComponentType<any>,
FallbackComponent: React.ComponentType<any>,
) {
return function DndControl(props: any) {
const enableExploreDnd = useMemo(
() => isFeatureEnabled(FeatureFlag.ENABLE_EXPLORE_DRAG_AND_DROP),
[],
);
return enableExploreDnd ? (
<DndComponent {...props} />
) : (
<FallbackComponent {...props} />
);
};
}

View File

@ -33,7 +33,6 @@
* here's a list of the keys that are common to all controls, and as a result define the * here's a list of the keys that are common to all controls, and as a result define the
* control interface. * control interface.
*/ */
import React from 'react';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import { import {
FeatureFlag, FeatureFlag,
@ -43,10 +42,7 @@ import {
isFeatureEnabled, isFeatureEnabled,
SequentialScheme, SequentialScheme,
legacyValidateInteger, legacyValidateInteger,
validateNonEmpty,
ComparisionType, ComparisionType,
QueryResponse,
QueryColumn,
isAdhocColumn, isAdhocColumn,
isPhysicalColumn, isPhysicalColumn,
} from '@superset-ui/core'; } from '@superset-ui/core';
@ -59,38 +55,29 @@ import {
D3_TIME_FORMAT_DOCS, D3_TIME_FORMAT_DOCS,
DEFAULT_TIME_FORMAT, DEFAULT_TIME_FORMAT,
DEFAULT_NUMBER_FORMAT, DEFAULT_NUMBER_FORMAT,
defineSavedMetrics,
} from '../utils'; } from '../utils';
import { TIME_FILTER_LABELS, DATASET_TIME_COLUMN_OPTION } from '../constants'; import { TIME_FILTER_LABELS } from '../constants';
import { import { SharedControlConfig, Dataset } from '../types';
Metric,
SharedControlConfig,
ColumnMeta,
ExtraControlProps,
SelectControlConfig,
Dataset,
} from '../types';
import { ColumnOption } from '../components/ColumnOption';
import { import {
dnd_adhoc_filters, dndAdhocFilterControl,
dnd_adhoc_metric, dndAdhocMetricControl,
dnd_adhoc_metrics, dndAdhocMetricsControl,
dnd_granularity_sqla, dndGranularitySqlaControl,
dnd_sort_by, dndSortByControl,
dnd_secondary_metric, dndSecondaryMetricControl,
dnd_size, dndSizeControl,
dnd_x, dndXControl,
dnd_y, dndYControl,
dndColumnsControl, dndColumnsControl,
dndEntity, dndEntityControl,
dndGroupByControl, dndGroupByControl,
dndSeries, dndSeriesControl,
dnd_adhoc_metric_2, dndAdhocMetricControl2,
dnd_x_axis, dndXAxisControl,
} from './dndControls'; } from './dndControls';
import { QUERY_TIME_COLUMN_OPTION } from '..';
import { xAxisControlConfig } from './constants'; export { withDndFallback } from './dndControls';
const categoricalSchemeRegistry = getCategoricalSchemeRegistry(); const categoricalSchemeRegistry = getCategoricalSchemeRegistry();
const sequentialSchemeRegistry = getSequentialSchemeRegistry(); const sequentialSchemeRegistry = getSequentialSchemeRegistry();
@ -105,77 +92,11 @@ const { user } = JSON.parse(
appContainer?.getAttribute('data-bootstrap') || '{}', appContainer?.getAttribute('data-bootstrap') || '{}',
); );
type Control = {
savedMetrics?: Metric[] | null;
default?: unknown;
};
type SelectDefaultOption = { type SelectDefaultOption = {
label: string; label: string;
value: string; value: string;
}; };
const groupByControl: SharedControlConfig<'SelectControl', ColumnMeta> = {
type: 'SelectControl',
label: t('Dimensions'),
multi: true,
freeForm: true,
clearable: true,
default: [],
includeTime: false,
description: t(
'One or many columns to group by. High cardinality groupings should include a sort by metric ' +
'and series limit to limit the number of fetched and rendered series.',
),
optionRenderer: c => <ColumnOption showType column={c} />,
valueRenderer: c => <ColumnOption column={c} />,
valueKey: 'column_name',
allowAll: true,
filterOption: ({ data: opt }, text: string) =>
(opt.column_name &&
opt.column_name.toLowerCase().includes(text.toLowerCase())) ||
(opt.verbose_name &&
opt.verbose_name.toLowerCase().includes(text.toLowerCase())) ||
false,
promptTextCreator: (label: unknown) => label,
mapStateToProps(state, { includeTime }) {
const newState: ExtraControlProps = {};
const { datasource } = state;
if (datasource?.columns[0]?.hasOwnProperty('groupby')) {
const options = (datasource as Dataset).columns.filter(c => c.groupby);
if (includeTime) options.unshift(DATASET_TIME_COLUMN_OPTION);
newState.options = options;
} else {
const options = (datasource as QueryResponse).columns;
if (includeTime) options.unshift(QUERY_TIME_COLUMN_OPTION);
newState.options = options;
}
return newState;
},
commaChoosesOption: false,
};
const metrics: SharedControlConfig<'MetricsControl'> = {
type: 'MetricsControl',
multi: true,
label: t('Metrics'),
validators: [validateNonEmpty],
mapStateToProps: ({ datasource }) => ({
columns: datasource?.columns || [],
savedMetrics: defineSavedMetrics(datasource),
datasource,
datasourceType: datasource?.type,
}),
description: t('One or many metrics to display'),
};
const metric: SharedControlConfig<'MetricsControl'> = {
...metrics,
multi: false,
label: t('Metric'),
description: t('Metric'),
};
const datasourceControl: SharedControlConfig<'DatasourceControl'> = { const datasourceControl: SharedControlConfig<'DatasourceControl'> = {
type: 'DatasourceControl', type: 'DatasourceControl',
label: t('Datasource'), label: t('Datasource'),
@ -203,13 +124,6 @@ const color_picker: SharedControlConfig<'ColorPickerControl'> = {
renderTrigger: true, renderTrigger: true,
}; };
const metric_2: SharedControlConfig<'MetricsControl'> = {
...metric,
label: t('Right Axis Metric'),
clearable: true,
description: t('Choose a metric for right axis'),
};
const linear_color_scheme: SharedControlConfig<'ColorSchemeControl'> = { const linear_color_scheme: SharedControlConfig<'ColorSchemeControl'> = {
type: 'ColorSchemeControl', type: 'ColorSchemeControl',
label: t('Linear Color Scheme'), label: t('Linear Color Scheme'),
@ -229,20 +143,6 @@ const linear_color_scheme: SharedControlConfig<'ColorSchemeControl'> = {
}), }),
}; };
const secondary_metric: SharedControlConfig<'MetricsControl'> = {
...metric,
label: t('Color Metric'),
default: null,
validators: [],
description: t('A metric to use for color'),
};
const columnsControl: typeof groupByControl = {
...groupByControl,
label: t('Columns'),
description: t('One or many columns to pivot as columns'),
};
const granularity: SharedControlConfig<'SelectControl'> = { const granularity: SharedControlConfig<'SelectControl'> = {
type: 'SelectControl', type: 'SelectControl',
freeForm: true, freeForm: true,
@ -273,44 +173,6 @@ const granularity: SharedControlConfig<'SelectControl'> = {
), ),
}; };
const granularity_sqla: SharedControlConfig<'SelectControl', ColumnMeta> = {
type: 'SelectControl',
label: TIME_FILTER_LABELS.granularity_sqla,
description: t(
'The time column for the visualization. Note that you ' +
'can define arbitrary expression that return a DATETIME ' +
'column in the table. Also note that the ' +
'filter below is applied against this column or ' +
'expression',
),
default: (c: Control) => c.default,
clearable: false,
optionRenderer: c => <ColumnOption showType column={c} />,
valueRenderer: c => <ColumnOption column={c} />,
valueKey: 'column_name',
mapStateToProps: state => {
const props: Partial<SelectControlConfig<ColumnMeta | QueryColumn>> = {};
const { datasource } = state;
if (datasource?.hasOwnProperty('main_dttm_col')) {
const dataset = datasource as Dataset;
props.options = dataset.columns.filter((c: ColumnMeta) => c.is_dttm);
props.default = null;
if (dataset.main_dttm_col) {
props.default = dataset.main_dttm_col;
} else if (props?.options) {
props.default = (props.options[0] as ColumnMeta)?.column_name;
}
} else {
const sortedQueryColumns = (datasource as QueryResponse)?.columns?.sort(
query => (query?.is_dttm ? -1 : 1),
);
props.options = sortedQueryColumns;
if (props?.options) props.default = props.options[0]?.name;
}
return props;
},
};
const time_grain_sqla: SharedControlConfig<'SelectControl'> = { const time_grain_sqla: SharedControlConfig<'SelectControl'> = {
type: 'SelectControl', type: 'SelectControl',
label: TIME_FILTER_LABELS.time_grain_sqla, label: TIME_FILTER_LABELS.time_grain_sqla,
@ -411,64 +273,6 @@ const series_limit: SharedControlConfig<'SelectControl'> = {
), ),
}; };
const sort_by: SharedControlConfig<'MetricsControl'> = {
type: 'MetricsControl',
label: t('Sort by'),
default: null,
description: t(
'Metric used to define how the top series are sorted if a series or row limit is present. ' +
'If undefined reverts to the first metric (where appropriate).',
),
mapStateToProps: ({ datasource }) => ({
columns: datasource?.columns || [],
savedMetrics: defineSavedMetrics(datasource),
datasource,
datasourceType: datasource?.type,
}),
};
const series: typeof groupByControl = {
...groupByControl,
label: t('Dimensions'),
multi: false,
default: null,
description: t(
'Defines the grouping of entities. ' +
'Each series is shown as a specific color on the chart and ' +
'has a legend toggle',
),
};
const entity: typeof groupByControl = {
...groupByControl,
label: t('Entity'),
default: null,
multi: false,
validators: [validateNonEmpty],
description: t('This defines the element to be plotted on the chart'),
};
const x: SharedControlConfig<'MetricsControl'> = {
...metric,
label: t('X Axis'),
description: t('Metric assigned to the [X] axis'),
default: null,
};
const y: SharedControlConfig<'MetricsControl'> = {
...metric,
label: t('Y Axis'),
default: null,
description: t('Metric assigned to the [Y] axis'),
};
const size: SharedControlConfig<'MetricsControl'> = {
...metric,
label: t('Bubble Size'),
description: t('Metric used to calculate bubble size'),
default: null,
};
const y_axis_format: SharedControlConfig<'SelectControl', SelectDefaultOption> = const y_axis_format: SharedControlConfig<'SelectControl', SelectDefaultOption> =
{ {
type: 'SelectControl', type: 'SelectControl',
@ -507,23 +311,6 @@ const x_axis_time_format: SharedControlConfig<
option.label.includes(search) || option.value.includes(search), option.label.includes(search) || option.value.includes(search),
}; };
const adhoc_filters: SharedControlConfig<'AdhocFilterControl'> = {
type: 'AdhocFilterControl',
label: t('Filters'),
default: [],
description: '',
mapStateToProps: ({ datasource, form_data }) => ({
columns: datasource?.columns[0]?.hasOwnProperty('filterable')
? (datasource as Dataset)?.columns.filter(c => c.filterable)
: datasource?.columns || [],
savedMetrics: defineSavedMetrics(datasource),
// current active adhoc metrics
selectedMetrics:
form_data.metrics || (form_data.metric ? [form_data.metric] : []),
datasource,
}),
};
const color_scheme: SharedControlConfig<'ColorSchemeControl'> = { const color_scheme: SharedControlConfig<'ColorSchemeControl'> = {
type: 'ColorSchemeControl', type: 'ColorSchemeControl',
label: t('Color Scheme'), label: t('Color Scheme'),
@ -551,51 +338,40 @@ const show_empty_columns: SharedControlConfig<'CheckboxControl'> = {
description: t('Show empty columns'), description: t('Show empty columns'),
}; };
const x_axis: SharedControlConfig<'SelectControl', ColumnMeta> = { export default {
...groupByControl, metrics: dndAdhocMetricsControl,
...xAxisControlConfig, metric: dndAdhocMetricControl,
};
const enableExploreDnd = isFeatureEnabled(
FeatureFlag.ENABLE_EXPLORE_DRAG_AND_DROP,
);
const sharedControls = {
metrics: enableExploreDnd ? dnd_adhoc_metrics : metrics,
metric: enableExploreDnd ? dnd_adhoc_metric : metric,
datasource: datasourceControl, datasource: datasourceControl,
viz_type, viz_type,
color_picker, color_picker,
metric_2: enableExploreDnd ? dnd_adhoc_metric_2 : metric_2, metric_2: dndAdhocMetricControl2,
linear_color_scheme, linear_color_scheme,
secondary_metric: enableExploreDnd ? dnd_secondary_metric : secondary_metric, secondary_metric: dndSecondaryMetricControl,
groupby: enableExploreDnd ? dndGroupByControl : groupByControl, groupby: dndGroupByControl,
columns: enableExploreDnd ? dndColumnsControl : columnsControl, columns: dndColumnsControl,
granularity, granularity,
granularity_sqla: enableExploreDnd ? dnd_granularity_sqla : granularity_sqla, granularity_sqla: dndGranularitySqlaControl,
time_grain_sqla, time_grain_sqla,
time_range, time_range,
row_limit, row_limit,
limit, limit,
timeseries_limit_metric: enableExploreDnd ? dnd_sort_by : sort_by, timeseries_limit_metric: dndSortByControl,
orderby: enableExploreDnd ? dnd_sort_by : sort_by, orderby: dndSortByControl,
order_desc, order_desc,
series: enableExploreDnd ? dndSeries : series, series: dndSeriesControl,
entity: enableExploreDnd ? dndEntity : entity, entity: dndEntityControl,
x: enableExploreDnd ? dnd_x : x, x: dndXControl,
y: enableExploreDnd ? dnd_y : y, y: dndYControl,
size: enableExploreDnd ? dnd_size : size, size: dndSizeControl,
y_axis_format, y_axis_format,
x_axis_time_format, x_axis_time_format,
adhoc_filters: enableExploreDnd ? dnd_adhoc_filters : adhoc_filters, adhoc_filters: dndAdhocFilterControl,
color_scheme, color_scheme,
series_columns: enableExploreDnd ? dndColumnsControl : columnsControl, series_columns: dndColumnsControl,
series_limit, series_limit,
series_limit_metric: enableExploreDnd ? dnd_sort_by : sort_by, series_limit_metric: dndSortByControl,
legacy_order_by: enableExploreDnd ? dnd_sort_by : sort_by, legacy_order_by: dndSortByControl,
truncate_metric, truncate_metric,
x_axis: enableExploreDnd ? dnd_x_axis : x_axis, x_axis: dndXAxisControl,
show_empty_columns, show_empty_columns,
}; };
export { sharedControls, dndEntity, dndColumnsControl };

View File

@ -29,7 +29,7 @@ import type {
QueryFormMetric, QueryFormMetric,
QueryResponse, QueryResponse,
} from '@superset-ui/core'; } from '@superset-ui/core';
import { sharedControls } from './shared-controls'; import sharedControls from './shared-controls';
import sharedControlComponents from './shared-controls/components'; import sharedControlComponents from './shared-controls/components';
export type { Metric } from '@superset-ui/core'; export type { Metric } from '@superset-ui/core';
@ -238,7 +238,7 @@ export interface BaseControlConfig<
) => boolean; ) => boolean;
mapStateToProps?: ( mapStateToProps?: (
state: ControlPanelState, state: ControlPanelState,
controlState: ControlState, controlState?: ControlState,
// TODO: add strict `chartState` typing (see superset-frontend/src/explore/types) // TODO: add strict `chartState` typing (see superset-frontend/src/explore/types)
chartState?: AnyDict, chartState?: AnyDict,
) => ExtraControlProps; ) => ExtraControlProps;

View File

@ -17,7 +17,7 @@
* under the License. * under the License.
*/ */
import React, { ReactElement } from 'react'; import React, { ReactElement } from 'react';
import { sharedControls } from '../shared-controls'; import sharedControls from '../shared-controls';
import sharedControlComponents from '../shared-controls/components'; import sharedControlComponents from '../shared-controls/components';
import { import {
ControlType, ControlType,

View File

@ -30,7 +30,7 @@ import {
formatSelectOptions, formatSelectOptions,
formatSelectOptionsForRange, formatSelectOptionsForRange,
sections, sections,
dndEntity, sharedControls,
getStandardizedControls, getStandardizedControls,
} from '@superset-ui/chart-controls'; } from '@superset-ui/chart-controls';
@ -52,7 +52,7 @@ const allColumns = {
}; };
const dndAllColumns = { const dndAllColumns = {
...dndEntity, ...sharedControls.entity,
description: t('Columns to display'), description: t('Columns to display'),
}; };

View File

@ -16,44 +16,28 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { import { t, validateNonEmpty } from '@superset-ui/core';
FeatureFlag,
isFeatureEnabled,
t,
validateNonEmpty,
} from '@superset-ui/core';
import { import {
columnChoices, columnChoices,
ControlPanelConfig, ControlPanelConfig,
ControlPanelState, ControlPanelState,
formatSelectOptions, formatSelectOptions,
sections, sections,
dndColumnsControl,
getStandardizedControls, getStandardizedControls,
sharedControls,
} from '@superset-ui/chart-controls'; } from '@superset-ui/chart-controls';
const allColumns = { const columnsConfig = {
type: 'SelectControl', ...sharedControls.columns,
label: t('Columns'), label: t('Columns'),
default: null,
description: t('Select the numeric columns to draw the histogram'), description: t('Select the numeric columns to draw the histogram'),
mapStateToProps: (state: ControlPanelState) => ({ mapStateToProps: (state: ControlPanelState) => ({
...(sharedControls.columns.mapStateToProps?.(state) || {}),
choices: columnChoices(state.datasource), choices: columnChoices(state.datasource),
}), }),
multi: true,
validators: [validateNonEmpty], validators: [validateNonEmpty],
}; };
const dndAllColumns = {
...dndColumnsControl,
description: t('Select the numeric columns to draw the histogram'),
validators: [validateNonEmpty],
};
const columnsConfig = isFeatureEnabled(FeatureFlag.ENABLE_EXPLORE_DRAG_AND_DROP)
? dndAllColumns
: allColumns;
const config: ControlPanelConfig = { const config: ControlPanelConfig = {
controlPanelSections: [ controlPanelSections: [
sections.legacyRegularTime, sections.legacyRegularTime,

View File

@ -23,7 +23,7 @@ import {
ControlPanelState, ControlPanelState,
formatSelectOptions, formatSelectOptions,
sections, sections,
dndEntity, sharedControls,
getStandardizedControls, getStandardizedControls,
} from '@superset-ui/chart-controls'; } from '@superset-ui/chart-controls';
@ -36,7 +36,7 @@ const allColumns = {
}; };
const columnsConfig = isFeatureEnabled(FeatureFlag.ENABLE_EXPLORE_DRAG_AND_DROP) const columnsConfig = isFeatureEnabled(FeatureFlag.ENABLE_EXPLORE_DRAG_AND_DROP)
? dndEntity ? sharedControls.entity
: allColumns; : allColumns;
const colorChoices = [ const colorChoices = [

View File

@ -18,12 +18,12 @@
*/ */
import { t } from '@superset-ui/core'; import { t } from '@superset-ui/core';
import { dndEntity } from '@superset-ui/chart-controls'; import { sharedControls } from '@superset-ui/chart-controls';
export const dndLineColumn = { export const dndLineColumn = {
name: 'line_column', name: 'line_column',
config: { config: {
...dndEntity, ...sharedControls.entity,
label: t('Lines column'), label: t('Lines column'),
description: t('The database columns that contains lines information'), description: t('The database columns that contains lines information'),
}, },
@ -32,7 +32,7 @@ export const dndLineColumn = {
export const dndGeojsonColumn = { export const dndGeojsonColumn = {
name: 'geojson', name: 'geojson',
config: { config: {
...dndEntity, ...sharedControls.entity,
label: t('GeoJson Column'), label: t('GeoJson Column'),
description: t('Select the geojson column'), description: t('Select the geojson column'),
}, },

View File

@ -64,7 +64,7 @@ const config: ControlPanelConfig = {
controlState, controlState,
) || {}; ) || {};
timeserieslimitProps.value = state.controls?.limit?.value timeserieslimitProps.value = state.controls?.limit?.value
? controlState.value ? controlState?.value
: []; : [];
return timeserieslimitProps; return timeserieslimitProps;
}, },

View File

@ -345,7 +345,7 @@ const config: ControlPanelConfig = {
controlState, controlState,
) || {}; ) || {};
timeserieslimitProps.value = state.controls?.limit?.value timeserieslimitProps.value = state.controls?.limit?.value
? controlState.value ? controlState?.value
: []; : [];
return timeserieslimitProps; return timeserieslimitProps;
}, },

View File

@ -49,7 +49,7 @@ const allColumns: typeof sharedControls.groupby = {
options: datasource?.columns || [], options: datasource?.columns || [],
queryMode: getQueryMode(controls), queryMode: getQueryMode(controls),
externalValidationErrors: externalValidationErrors:
isRawMode({ controls }) && ensureIsArray(controlState.value).length === 0 isRawMode({ controls }) && ensureIsArray(controlState?.value).length === 0
? [t('must have a value')] ? [t('must have a value')]
: [], : [],
}), }),
@ -74,7 +74,7 @@ const dndAllColumns: typeof sharedControls.groupby = {
} }
newState.queryMode = getQueryMode(controls); newState.queryMode = getQueryMode(controls);
newState.externalValidationErrors = newState.externalValidationErrors =
isRawMode({ controls }) && ensureIsArray(controlState.value).length === 0 isRawMode({ controls }) && ensureIsArray(controlState?.value).length === 0
? [t('must have a value')] ? [t('must have a value')]
: []; : [];
return newState; return newState;

View File

@ -46,7 +46,7 @@ const percentMetrics: typeof sharedControls.metrics = {
externalValidationErrors: validateAggControlValues(controls, [ externalValidationErrors: validateAggControlValues(controls, [
controls.groupby?.value, controls.groupby?.value,
controls.metrics?.value, controls.metrics?.value,
controlState.value, controlState?.value,
]), ]),
}), }),
rerender: ['groupby', 'metrics'], rerender: ['groupby', 'metrics'],

View File

@ -40,7 +40,6 @@ import {
sections, sections,
sharedControls, sharedControls,
ControlPanelState, ControlPanelState,
ExtraControlProps,
ControlState, ControlState,
emitFilterControl, emitFilterControl,
Dataset, Dataset,
@ -96,15 +95,14 @@ const queryMode: ControlConfig<'RadioButtonControl'> = {
rerender: ['all_columns', 'groupby', 'metrics', 'percent_metrics'], rerender: ['all_columns', 'groupby', 'metrics', 'percent_metrics'],
}; };
const all_columns: typeof sharedControls.groupby = { const allColumnsControl: typeof sharedControls.groupby = {
type: 'SelectControl', ...sharedControls.groupby,
label: t('Columns'), label: t('Columns'),
description: t('Columns to display'), description: t('Columns to display'),
multi: true, multi: true,
freeForm: true, freeForm: true,
allowAll: true, allowAll: true,
commaChoosesOption: false, commaChoosesOption: false,
default: [],
optionRenderer: c => <ColumnOption showType column={c} />, optionRenderer: c => <ColumnOption showType column={c} />,
valueRenderer: c => <ColumnOption column={c} />, valueRenderer: c => <ColumnOption column={c} />,
valueKey: 'column_name', valueKey: 'column_name',
@ -112,7 +110,7 @@ const all_columns: typeof sharedControls.groupby = {
options: datasource?.columns || [], options: datasource?.columns || [],
queryMode: getQueryMode(controls), queryMode: getQueryMode(controls),
externalValidationErrors: externalValidationErrors:
isRawMode({ controls }) && ensureIsArray(controlState.value).length === 0 isRawMode({ controls }) && ensureIsArray(controlState?.value).length === 0
? [t('must have a value')] ? [t('must have a value')]
: [], : [],
}), }),
@ -120,37 +118,12 @@ const all_columns: typeof sharedControls.groupby = {
resetOnHide: false, resetOnHide: false,
}; };
const dnd_all_columns: typeof sharedControls.groupby = { const percentMetricsControl: typeof sharedControls.metrics = {
type: 'DndColumnSelect', ...sharedControls.metrics,
label: t('Columns'),
description: t('Columns to display'),
default: [],
mapStateToProps({ datasource, controls }, controlState) {
const newState: ExtraControlProps = {};
if (datasource?.columns[0]?.hasOwnProperty('column_name')) {
const options = (datasource as Dataset).columns;
newState.options = Object.fromEntries(
options.map((option: ColumnMeta) => [option.column_name, option]),
);
} else newState.options = datasource?.columns;
newState.queryMode = getQueryMode(controls);
newState.externalValidationErrors =
isRawMode({ controls }) && ensureIsArray(controlState.value).length === 0
? [t('must have a value')]
: [];
return newState;
},
visibility: isRawMode,
resetOnHide: false,
};
const percent_metrics: typeof sharedControls.metrics = {
type: 'MetricsControl',
label: t('Percentage metrics'), label: t('Percentage metrics'),
description: t( description: t(
'Metrics for which percentage of total are to be displayed. Calculated from only data within the row limit.', 'Metrics for which percentage of total are to be displayed. Calculated from only data within the row limit.',
), ),
multi: true,
visibility: isAggMode, visibility: isAggMode,
resetOnHide: false, resetOnHide: false,
mapStateToProps: ({ datasource, controls }, controlState) => ({ mapStateToProps: ({ datasource, controls }, controlState) => ({
@ -162,7 +135,7 @@ const percent_metrics: typeof sharedControls.metrics = {
externalValidationErrors: validateAggControlValues(controls, [ externalValidationErrors: validateAggControlValues(controls, [
controls.groupby?.value, controls.groupby?.value,
controls.metrics?.value, controls.metrics?.value,
controlState.value, controlState?.value,
]), ]),
}), }),
rerender: ['groupby', 'metrics'], rerender: ['groupby', 'metrics'],
@ -170,11 +143,6 @@ const percent_metrics: typeof sharedControls.metrics = {
validators: [], validators: [],
}; };
const dnd_percent_metrics = {
...percent_metrics,
type: 'DndMetricSelect',
};
const config: ControlPanelConfig = { const config: ControlPanelConfig = {
controlPanelSections: [ controlPanelSections: [
sections.legacyTimeseriesTime, sections.legacyTimeseriesTime,
@ -251,19 +219,13 @@ const config: ControlPanelConfig = {
}, },
{ {
name: 'all_columns', name: 'all_columns',
config: isFeatureEnabled(FeatureFlag.ENABLE_EXPLORE_DRAG_AND_DROP) config: allColumnsControl,
? dnd_all_columns
: all_columns,
}, },
], ],
[ [
{ {
name: 'percent_metrics', name: 'percent_metrics',
config: { config: percentMetricsControl,
...(isFeatureEnabled(FeatureFlag.ENABLE_EXPLORE_DRAG_AND_DROP)
? dnd_percent_metrics
: percent_metrics),
},
}, },
], ],
['adhoc_filters'], ['adhoc_filters'],

View File

@ -16,6 +16,7 @@
* specific language governing permissions and limitations * specific language governing permissions and limitations
* under the License. * under the License.
*/ */
import { FeatureFlag } from '@superset-ui/core';
import React from 'react'; import React from 'react';
import { render, screen } from 'spec/helpers/testing-library'; import { render, screen } from 'spec/helpers/testing-library';
import { import {
@ -27,12 +28,18 @@ const defaultProps: DndColumnSelectProps = {
type: 'DndColumnSelect', type: 'DndColumnSelect',
name: 'Filter', name: 'Filter',
onChange: jest.fn(), onChange: jest.fn(),
options: { options: [{ column_name: 'Column A' }],
string: { column_name: 'Column A' },
},
actions: { setControlValue: jest.fn() }, actions: { setControlValue: jest.fn() },
}; };
beforeAll(() => {
window.featureFlags = { [FeatureFlag.ENABLE_EXPLORE_DRAG_AND_DROP]: true };
});
afterAll(() => {
window.featureFlags = {};
});
test('renders with default props', async () => { test('renders with default props', async () => {
render(<DndColumnSelect {...defaultProps} />, { render(<DndColumnSelect {...defaultProps} />, {
useDnd: true, useDnd: true,
@ -42,7 +49,7 @@ test('renders with default props', async () => {
}); });
test('renders with value', async () => { test('renders with value', async () => {
render(<DndColumnSelect {...defaultProps} value="string" />, { render(<DndColumnSelect {...defaultProps} value="Column A" />, {
useDnd: true, useDnd: true,
useRedux: true, useRedux: true,
}); });

View File

@ -24,7 +24,11 @@ import {
tn, tn,
QueryFormColumn, QueryFormColumn,
} from '@superset-ui/core'; } from '@superset-ui/core';
import { ColumnMeta, isColumnMeta } from '@superset-ui/chart-controls'; import {
ColumnMeta,
isColumnMeta,
withDndFallback,
} from '@superset-ui/chart-controls';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
import DndSelectLabel from 'src/explore/components/controls/DndColumnSelectControl/DndSelectLabel'; import DndSelectLabel from 'src/explore/components/controls/DndColumnSelectControl/DndSelectLabel';
import OptionWrapper from 'src/explore/components/controls/DndColumnSelectControl/OptionWrapper'; import OptionWrapper from 'src/explore/components/controls/DndColumnSelectControl/OptionWrapper';
@ -34,13 +38,14 @@ import { DndItemType } from 'src/explore/components/DndItemType';
import { useComponentDidUpdate } from 'src/hooks/useComponentDidUpdate'; import { useComponentDidUpdate } from 'src/hooks/useComponentDidUpdate';
import ColumnSelectPopoverTrigger from './ColumnSelectPopoverTrigger'; import ColumnSelectPopoverTrigger from './ColumnSelectPopoverTrigger';
import { DndControlProps } from './types'; import { DndControlProps } from './types';
import SelectControl from '../SelectControl';
export type DndColumnSelectProps = DndControlProps<QueryFormColumn> & { export type DndColumnSelectProps = DndControlProps<QueryFormColumn> & {
options: Record<string, ColumnMeta>; options: ColumnMeta[];
isTemporal?: boolean; isTemporal?: boolean;
}; };
export function DndColumnSelect(props: DndColumnSelectProps) { function DndColumnSelect(props: DndColumnSelectProps) {
const { const {
value, value,
options, options,
@ -48,16 +53,20 @@ export function DndColumnSelect(props: DndColumnSelectProps) {
onChange, onChange,
canDelete = true, canDelete = true,
ghostButtonText, ghostButtonText,
clickEnabledGhostButtonText,
name, name,
label, label,
isTemporal, isTemporal,
} = props; } = props;
const [newColumnPopoverVisible, setNewColumnPopoverVisible] = useState(false); const [newColumnPopoverVisible, setNewColumnPopoverVisible] = useState(false);
const optionSelector = useMemo( const optionSelector = useMemo(() => {
() => new OptionSelector(options, multi, value), const optionsMap = Object.fromEntries(
[multi, options, value], options.map(option => [option.column_name, option]),
); );
return new OptionSelector(optionsMap, multi, value);
}, [multi, options, value]);
// synchronize values in case of dataset changes // synchronize values in case of dataset changes
const handleOptionsChange = useCallback(() => { const handleOptionsChange = useCallback(() => {
@ -126,15 +135,18 @@ export function DndColumnSelect(props: DndColumnSelectProps) {
[onChange, optionSelector], [onChange, optionSelector],
); );
const popoverOptions = useMemo(() => Object.values(options), [options]); const clickEnabled = useMemo(
() => isFeatureEnabled(FeatureFlag.ENABLE_DND_WITH_CLICK_UX),
[],
);
const valuesRenderer = useCallback( const valuesRenderer = useCallback(
() => () =>
optionSelector.values.map((column, idx) => optionSelector.values.map((column, idx) =>
isFeatureEnabled(FeatureFlag.ENABLE_DND_WITH_CLICK_UX) ? ( clickEnabled ? (
<ColumnSelectPopoverTrigger <ColumnSelectPopoverTrigger
key={idx} key={idx}
columns={popoverOptions} columns={options}
onColumnEdit={newColumn => { onColumnEdit={newColumn => {
if (isColumnMeta(newColumn)) { if (isColumnMeta(newColumn)) {
optionSelector.replace(idx, newColumn.column_name); optionSelector.replace(idx, newColumn.column_name);
@ -171,13 +183,15 @@ export function DndColumnSelect(props: DndColumnSelectProps) {
), ),
[ [
canDelete, canDelete,
clickEnabled,
isTemporal,
label, label,
name, name,
onChange, onChange,
onClickClose, onClickClose,
onShiftOptions, onShiftOptions,
optionSelector, optionSelector,
popoverOptions, options,
], ],
); );
@ -205,15 +219,24 @@ export function DndColumnSelect(props: DndColumnSelectProps) {
togglePopover(true); togglePopover(true);
}, [togglePopover]); }, [togglePopover]);
const defaultGhostButtonText = isFeatureEnabled( const labelGhostButtonText = useMemo(() => {
FeatureFlag.ENABLE_DND_WITH_CLICK_UX, if (clickEnabled) {
) return (
? tn( clickEnabledGhostButtonText ??
'Drop a column here or click', ghostButtonText ??
'Drop columns here or click', tn(
multi ? 2 : 1, 'Drop a column here or click',
) 'Drop columns here or click',
: tn('Drop column here', 'Drop columns here', multi ? 2 : 1); multi ? 2 : 1,
)
);
}
return (
ghostButtonText ??
tn('Drop column here', 'Drop columns here', multi ? 2 : 1)
);
}, [clickEnabled, clickEnabledGhostButtonText, ghostButtonText, multi]);
return ( return (
<div> <div>
@ -223,16 +246,12 @@ export function DndColumnSelect(props: DndColumnSelectProps) {
valuesRenderer={valuesRenderer} valuesRenderer={valuesRenderer}
accept={DndItemType.Column} accept={DndItemType.Column}
displayGhostButton={multi || optionSelector.values.length === 0} displayGhostButton={multi || optionSelector.values.length === 0}
ghostButtonText={ghostButtonText || defaultGhostButtonText} ghostButtonText={labelGhostButtonText}
onClickGhostButton={ onClickGhostButton={clickEnabled ? openPopover : undefined}
isFeatureEnabled(FeatureFlag.ENABLE_DND_WITH_CLICK_UX)
? openPopover
: undefined
}
{...props} {...props}
/> />
<ColumnSelectPopoverTrigger <ColumnSelectPopoverTrigger
columns={popoverOptions} columns={options}
onColumnEdit={addNewColumnWithPopover} onColumnEdit={addNewColumnWithPopover}
isControlledComponent isControlledComponent
togglePopover={togglePopover} togglePopover={togglePopover}
@ -245,3 +264,10 @@ export function DndColumnSelect(props: DndColumnSelectProps) {
</div> </div>
); );
} }
const DndColumnSelectWithFallback = withDndFallback(
DndColumnSelect,
SelectControl,
);
export { DndColumnSelectWithFallback as DndColumnSelect };

View File

@ -17,7 +17,7 @@
* under the License. * under the License.
*/ */
import React from 'react'; import React from 'react';
import { GenericDataType } from '@superset-ui/core'; import { FeatureFlag, GenericDataType } from '@superset-ui/core';
import { render, screen } from 'spec/helpers/testing-library'; import { render, screen } from 'spec/helpers/testing-library';
import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric'; import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric';
import AdhocFilter, { import AdhocFilter, {
@ -48,6 +48,14 @@ const baseFormData = {
datasource: 'table__1', datasource: 'table__1',
}; };
beforeAll(() => {
window.featureFlags = { [FeatureFlag.ENABLE_EXPLORE_DRAG_AND_DROP]: true };
});
afterAll(() => {
window.featureFlags = {};
});
test('renders with default props', async () => { test('renders with default props', async () => {
render(<DndFilterSelect {...defaultProps} />, { useDnd: true }); render(<DndFilterSelect {...defaultProps} />, { useDnd: true });
expect( expect(

View File

@ -27,7 +27,7 @@ import {
SupersetClient, SupersetClient,
t, t,
} from '@superset-ui/core'; } from '@superset-ui/core';
import { ColumnMeta } from '@superset-ui/chart-controls'; import { ColumnMeta, withDndFallback } from '@superset-ui/chart-controls';
import { import {
OPERATOR_ENUM_TO_OPERATOR_TYPE, OPERATOR_ENUM_TO_OPERATOR_TYPE,
Operators, Operators,
@ -49,6 +49,7 @@ import {
} from 'src/explore/components/DatasourcePanel/types'; } from 'src/explore/components/DatasourcePanel/types';
import { DndItemType } from 'src/explore/components/DndItemType'; import { DndItemType } from 'src/explore/components/DndItemType';
import { ControlComponentProps } from 'src/explore/components/Control'; import { ControlComponentProps } from 'src/explore/components/Control';
import AdhocFilterControl from '../FilterControl/AdhocFilterControl';
const EMPTY_OBJECT = {}; const EMPTY_OBJECT = {};
const DND_ACCEPTED_TYPES = [ const DND_ACCEPTED_TYPES = [
@ -69,7 +70,7 @@ export interface DndFilterSelectProps
datasource: Datasource; datasource: Datasource;
} }
export const DndFilterSelect = (props: DndFilterSelectProps) => { const DndFilterSelect = (props: DndFilterSelectProps) => {
const { datasource, onChange = () => {}, name: controlName } = props; const { datasource, onChange = () => {}, name: controlName } = props;
const propsValues = Array.from(props.value ?? []); const propsValues = Array.from(props.value ?? []);
@ -407,3 +408,10 @@ export const DndFilterSelect = (props: DndFilterSelectProps) => {
</> </>
); );
}; };
const DndFilterSelectWithFallback = withDndFallback(
DndFilterSelect,
AdhocFilterControl,
);
export { DndFilterSelectWithFallback as DndFilterSelect };

View File

@ -18,6 +18,7 @@
*/ */
import React from 'react'; import React from 'react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { FeatureFlag } from '@superset-ui/core';
import { import {
render, render,
screen, screen,
@ -67,6 +68,14 @@ const adhocMetricB = {
optionName: 'def', optionName: 'def',
}; };
beforeAll(() => {
window.featureFlags = { [FeatureFlag.ENABLE_EXPLORE_DRAG_AND_DROP]: true };
});
afterAll(() => {
window.featureFlags = {};
});
test('renders with default props', () => { test('renders with default props', () => {
render(<DndMetricSelect {...defaultProps} />, { useDnd: true }); render(<DndMetricSelect {...defaultProps} />, { useDnd: true });
expect(screen.getByText('Drop column or metric here')).toBeInTheDocument(); expect(screen.getByText('Drop column or metric here')).toBeInTheDocument();

View File

@ -27,7 +27,7 @@ import {
QueryFormMetric, QueryFormMetric,
tn, tn,
} from '@superset-ui/core'; } from '@superset-ui/core';
import { ColumnMeta } from '@superset-ui/chart-controls'; import { ColumnMeta, withDndFallback } from '@superset-ui/chart-controls';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { usePrevious } from 'src/hooks/usePrevious'; import { usePrevious } from 'src/hooks/usePrevious';
import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric'; import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric';
@ -41,6 +41,7 @@ import { DndItemType } from 'src/explore/components/DndItemType';
import DndSelectLabel from 'src/explore/components/controls/DndColumnSelectControl/DndSelectLabel'; import DndSelectLabel from 'src/explore/components/controls/DndColumnSelectControl/DndSelectLabel';
import { savedMetricType } from 'src/explore/components/controls/MetricControl/types'; import { savedMetricType } from 'src/explore/components/controls/MetricControl/types';
import { AGGREGATES } from 'src/explore/constants'; import { AGGREGATES } from 'src/explore/constants';
import MetricsControl from '../MetricControl/MetricsControl';
const EMPTY_OBJECT = {}; const EMPTY_OBJECT = {};
const DND_ACCEPTED_TYPES = [DndItemType.Column, DndItemType.Metric]; const DND_ACCEPTED_TYPES = [DndItemType.Column, DndItemType.Metric];
@ -125,7 +126,7 @@ const getMetricsMatchingCurrentDataset = (
}, []); }, []);
}; };
export const DndMetricSelect = (props: any) => { const DndMetricSelect = (props: any) => {
const { onChange, multi, columns, savedMetrics } = props; const { onChange, multi, columns, savedMetrics } = props;
const handleChange = useCallback( const handleChange = useCallback(
@ -408,3 +409,10 @@ export const DndMetricSelect = (props: any) => {
</div> </div>
); );
}; };
const DndMetricSelectWithFallback = withDndFallback(
DndMetricSelect,
MetricsControl,
);
export { DndMetricSelectWithFallback as DndMetricSelect };

View File

@ -46,6 +46,7 @@ export type DndControlProps<ValueType extends JsonValue> =
multi?: boolean; multi?: boolean;
canDelete?: boolean; canDelete?: boolean;
ghostButtonText?: string; ghostButtonText?: string;
clickEnabledGhostButtonText?: string;
onChange: (value: ValueType | ValueType[] | null | undefined) => void; onChange: (value: ValueType | ValueType[] | null | undefined) => void;
}; };