mirror of
https://github.com/apache/superset.git
synced 2026-06-01 21:59:26 +00:00
feat(explore): Don't discard controls with custom sql when changing datasource (#20934)
This commit is contained in:
committed by
GitHub
parent
ec20c0104e
commit
cddc361adc
@@ -17,6 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { t } from '@superset-ui/core';
|
||||
import { DndItemType } from 'src/explore/components/DndItemType';
|
||||
import AdhocFilterPopoverTrigger from 'src/explore/components/controls/FilterControl/AdhocFilterPopoverTrigger';
|
||||
import AdhocFilter from 'src/explore/components/controls/FilterControl/AdhocFilter';
|
||||
@@ -63,6 +64,11 @@ export default function DndAdhocFilterOption({
|
||||
type={DndItemType.FilterOption}
|
||||
withCaret
|
||||
isExtra={adhocFilter.isExtra}
|
||||
datasourceWarningMessage={
|
||||
adhocFilter.datasourceWarning
|
||||
? t('This filter might be incompatible with current dataset')
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</AdhocFilterPopoverTrigger>
|
||||
);
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
isFeatureEnabled,
|
||||
tn,
|
||||
QueryFormColumn,
|
||||
t,
|
||||
isAdhocColumn,
|
||||
} from '@superset-ui/core';
|
||||
import {
|
||||
ColumnMeta,
|
||||
@@ -35,7 +37,6 @@ import OptionWrapper from 'src/explore/components/controls/DndColumnSelectContro
|
||||
import { OptionSelector } from 'src/explore/components/controls/DndColumnSelectControl/utils';
|
||||
import { DatasourcePanelDndItem } from 'src/explore/components/DatasourcePanel/types';
|
||||
import { DndItemType } from 'src/explore/components/DndItemType';
|
||||
import { useComponentDidUpdate } from 'src/hooks/useComponentDidUpdate';
|
||||
import ColumnSelectPopoverTrigger from './ColumnSelectPopoverTrigger';
|
||||
import { DndControlProps } from './types';
|
||||
import SelectControl from '../SelectControl';
|
||||
@@ -68,34 +69,6 @@ function DndColumnSelect(props: DndColumnSelectProps) {
|
||||
return new OptionSelector(optionsMap, multi, value);
|
||||
}, [multi, options, value]);
|
||||
|
||||
// synchronize values in case of dataset changes
|
||||
const handleOptionsChange = useCallback(() => {
|
||||
const optionSelectorValues = optionSelector.getValues();
|
||||
if (typeof value !== typeof optionSelectorValues) {
|
||||
onChange(optionSelectorValues);
|
||||
}
|
||||
if (
|
||||
typeof value === 'string' &&
|
||||
typeof optionSelectorValues === 'string' &&
|
||||
value !== optionSelectorValues
|
||||
) {
|
||||
onChange(optionSelectorValues);
|
||||
}
|
||||
if (
|
||||
Array.isArray(optionSelectorValues) &&
|
||||
Array.isArray(value) &&
|
||||
(optionSelectorValues.length !== value.length ||
|
||||
optionSelectorValues.every((val, index) => val === value[index]))
|
||||
) {
|
||||
onChange(optionSelectorValues);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify(value), JSON.stringify(optionSelector.getValues())]);
|
||||
|
||||
// useComponentDidUpdate to avoid running this for the first render, to avoid
|
||||
// calling onChange when the initial value is not valid for the dataset
|
||||
useComponentDidUpdate(handleOptionsChange);
|
||||
|
||||
const onDrop = useCallback(
|
||||
(item: DatasourcePanelDndItem) => {
|
||||
const column = item.value as ColumnMeta;
|
||||
@@ -142,8 +115,12 @@ function DndColumnSelect(props: DndColumnSelectProps) {
|
||||
|
||||
const valuesRenderer = useCallback(
|
||||
() =>
|
||||
optionSelector.values.map((column, idx) =>
|
||||
clickEnabled ? (
|
||||
optionSelector.values.map((column, idx) => {
|
||||
const datasourceWarningMessage =
|
||||
isAdhocColumn(column) && column.datasourceWarning
|
||||
? t('This column might be incompatible with current dataset')
|
||||
: undefined;
|
||||
return clickEnabled ? (
|
||||
<ColumnSelectPopoverTrigger
|
||||
key={idx}
|
||||
columns={options}
|
||||
@@ -166,6 +143,7 @@ function DndColumnSelect(props: DndColumnSelectProps) {
|
||||
type={`${DndItemType.ColumnOption}_${name}_${label}`}
|
||||
canDelete={canDelete}
|
||||
column={column}
|
||||
datasourceWarningMessage={datasourceWarningMessage}
|
||||
withCaret
|
||||
/>
|
||||
</ColumnSelectPopoverTrigger>
|
||||
@@ -178,9 +156,10 @@ function DndColumnSelect(props: DndColumnSelectProps) {
|
||||
type={`${DndItemType.ColumnOption}_${name}_${label}`}
|
||||
canDelete={canDelete}
|
||||
column={column}
|
||||
datasourceWarningMessage={datasourceWarningMessage}
|
||||
/>
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
[
|
||||
canDelete,
|
||||
clickEnabled,
|
||||
|
||||
@@ -133,6 +133,7 @@ test('remove selected custom metric when metric gets removed from dataset', () =
|
||||
);
|
||||
expect(screen.getByText('metric_a')).toBeVisible();
|
||||
expect(screen.queryByText('Metric B')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('metric_b')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('SUM(column_a)')).toBeVisible();
|
||||
expect(screen.getByText('SUM(Column B)')).toBeVisible();
|
||||
});
|
||||
@@ -171,15 +172,6 @@ test('remove selected custom metric when metric gets removed from dataset for si
|
||||
],
|
||||
};
|
||||
|
||||
// rerender twice - first to update columns, second to update value
|
||||
rerender(
|
||||
<DndMetricSelect
|
||||
{...newPropsWithRemovedMetric}
|
||||
value={metricValue}
|
||||
onChange={onChange}
|
||||
multi={false}
|
||||
/>,
|
||||
);
|
||||
rerender(
|
||||
<DndMetricSelect
|
||||
{...newPropsWithRemovedMetric}
|
||||
@@ -220,15 +212,6 @@ test('remove selected adhoc metric when column gets removed from dataset', async
|
||||
],
|
||||
};
|
||||
|
||||
// rerender twice - first to update columns, second to update value
|
||||
rerender(
|
||||
<DndMetricSelect
|
||||
{...newPropsWithRemovedColumn}
|
||||
value={metricValues}
|
||||
onChange={onChange}
|
||||
multi
|
||||
/>,
|
||||
);
|
||||
rerender(
|
||||
<DndMetricSelect
|
||||
{...newPropsWithRemovedColumn}
|
||||
|
||||
@@ -22,14 +22,15 @@ import {
|
||||
ensureIsArray,
|
||||
FeatureFlag,
|
||||
GenericDataType,
|
||||
isAdhocMetricSimple,
|
||||
isFeatureEnabled,
|
||||
isSavedMetric,
|
||||
Metric,
|
||||
QueryFormMetric,
|
||||
t,
|
||||
tn,
|
||||
} from '@superset-ui/core';
|
||||
import { ColumnMeta, withDndFallback } from '@superset-ui/chart-controls';
|
||||
import { isEqual } from 'lodash';
|
||||
import { usePrevious } from 'src/hooks/usePrevious';
|
||||
import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric';
|
||||
import AdhocMetricPopoverTrigger from 'src/explore/components/controls/MetricControl/AdhocMetricPopoverTrigger';
|
||||
import MetricDefinitionValue from 'src/explore/components/controls/MetricControl/MetricDefinitionValue';
|
||||
@@ -46,24 +47,49 @@ import MetricsControl from '../MetricControl/MetricsControl';
|
||||
const EMPTY_OBJECT = {};
|
||||
const DND_ACCEPTED_TYPES = [DndItemType.Column, DndItemType.Metric];
|
||||
|
||||
const isDictionaryForAdhocMetric = (value: any) =>
|
||||
value && !(value instanceof AdhocMetric) && value.expressionType;
|
||||
const isDictionaryForAdhocMetric = (value: QueryFormMetric) =>
|
||||
value &&
|
||||
!(value instanceof AdhocMetric) &&
|
||||
typeof value !== 'string' &&
|
||||
value.expressionType;
|
||||
|
||||
const coerceAdhocMetrics = (value: any) => {
|
||||
if (!value) {
|
||||
const coerceMetrics = (
|
||||
addedMetrics: QueryFormMetric | QueryFormMetric[] | undefined | null,
|
||||
savedMetrics: Metric[],
|
||||
columns: ColumnMeta[],
|
||||
) => {
|
||||
if (!addedMetrics) {
|
||||
return [];
|
||||
}
|
||||
if (!Array.isArray(value)) {
|
||||
if (isDictionaryForAdhocMetric(value)) {
|
||||
return [new AdhocMetric(value)];
|
||||
const metricsCompatibleWithDataset = ensureIsArray(addedMetrics).filter(
|
||||
metric => {
|
||||
if (isSavedMetric(metric)) {
|
||||
return savedMetrics.some(
|
||||
savedMetric => savedMetric.metric_name === metric,
|
||||
);
|
||||
}
|
||||
if (isAdhocMetricSimple(metric)) {
|
||||
return columns.some(
|
||||
column => column.column_name === metric.column.column_name,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
);
|
||||
|
||||
return metricsCompatibleWithDataset.map(metric => {
|
||||
if (!isDictionaryForAdhocMetric(metric)) {
|
||||
return metric;
|
||||
}
|
||||
return [value];
|
||||
}
|
||||
return value.map(val => {
|
||||
if (isDictionaryForAdhocMetric(val)) {
|
||||
return new AdhocMetric(val);
|
||||
if (isAdhocMetricSimple(metric)) {
|
||||
const column = columns.find(
|
||||
col => col.column_name === metric.column.column_name,
|
||||
);
|
||||
if (column) {
|
||||
return new AdhocMetric({ ...metric, column });
|
||||
}
|
||||
}
|
||||
return val;
|
||||
return new AdhocMetric(metric);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -81,53 +107,8 @@ const getOptionsForSavedMetrics = (
|
||||
|
||||
type ValueType = Metric | AdhocMetric | QueryFormMetric;
|
||||
|
||||
// TODO: use typeguards to distinguish saved metrics from adhoc metrics
|
||||
const getMetricsMatchingCurrentDataset = (
|
||||
values: ValueType[],
|
||||
columns: ColumnMeta[],
|
||||
savedMetrics: (savedMetricType | Metric)[],
|
||||
prevColumns: ColumnMeta[],
|
||||
prevSavedMetrics: (savedMetricType | Metric)[],
|
||||
): ValueType[] => {
|
||||
const areSavedMetricsEqual =
|
||||
!prevSavedMetrics || isEqual(prevSavedMetrics, savedMetrics);
|
||||
const areColsEqual = !prevColumns || isEqual(prevColumns, columns);
|
||||
|
||||
if (areColsEqual && areSavedMetricsEqual) {
|
||||
return values;
|
||||
}
|
||||
return values.reduce((acc: ValueType[], metric) => {
|
||||
if (typeof metric === 'string' || (metric as Metric).metric_name) {
|
||||
if (
|
||||
areSavedMetricsEqual ||
|
||||
savedMetrics?.some(
|
||||
savedMetric =>
|
||||
savedMetric.metric_name === metric ||
|
||||
savedMetric.metric_name === (metric as Metric).metric_name,
|
||||
)
|
||||
) {
|
||||
acc.push(metric);
|
||||
}
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (!areColsEqual) {
|
||||
const newCol = columns?.find(
|
||||
column =>
|
||||
(metric as AdhocMetric).column?.column_name === column.column_name,
|
||||
);
|
||||
if (newCol) {
|
||||
acc.push({ ...(metric as AdhocMetric), column: newCol });
|
||||
}
|
||||
} else {
|
||||
acc.push(metric);
|
||||
}
|
||||
return acc;
|
||||
}, []);
|
||||
};
|
||||
|
||||
const DndMetricSelect = (props: any) => {
|
||||
const { onChange, multi, columns, savedMetrics } = props;
|
||||
const { onChange, multi } = props;
|
||||
|
||||
const handleChange = useCallback(
|
||||
opts => {
|
||||
@@ -153,39 +134,20 @@ const DndMetricSelect = (props: any) => {
|
||||
);
|
||||
|
||||
const [value, setValue] = useState<ValueType[]>(
|
||||
coerceAdhocMetrics(props.value),
|
||||
coerceMetrics(props.value, props.savedMetrics, props.columns),
|
||||
);
|
||||
const [droppedItem, setDroppedItem] = useState<
|
||||
DatasourcePanelDndItem | typeof EMPTY_OBJECT
|
||||
>({});
|
||||
const [newMetricPopoverVisible, setNewMetricPopoverVisible] = useState(false);
|
||||
const prevColumns = usePrevious(columns);
|
||||
const prevSavedMetrics = usePrevious(savedMetrics);
|
||||
|
||||
useEffect(() => {
|
||||
setValue(coerceAdhocMetrics(props.value));
|
||||
}, [JSON.stringify(props.value)]);
|
||||
|
||||
useEffect(() => {
|
||||
// Remove selected custom metrics that do not exist in the dataset anymore
|
||||
// Remove selected adhoc metrics that use columns which do not exist in the dataset anymore
|
||||
// Sync adhoc metrics with dataset columns when they are modified by the user
|
||||
if (!props.value) {
|
||||
return;
|
||||
}
|
||||
const propsValues = ensureIsArray(props.value);
|
||||
const matchingMetrics = getMetricsMatchingCurrentDataset(
|
||||
propsValues,
|
||||
columns,
|
||||
savedMetrics,
|
||||
prevColumns,
|
||||
prevSavedMetrics,
|
||||
);
|
||||
|
||||
if (!isEqual(propsValues, matchingMetrics)) {
|
||||
handleChange(matchingMetrics);
|
||||
}
|
||||
}, [columns, savedMetrics, handleChange]);
|
||||
setValue(coerceMetrics(props.value, props.savedMetrics, props.columns));
|
||||
}, [
|
||||
JSON.stringify(props.value),
|
||||
JSON.stringify(props.savedMetrics),
|
||||
JSON.stringify(props.columns),
|
||||
]);
|
||||
|
||||
const canDrop = useCallback(
|
||||
(item: DatasourcePanelDndItem) => {
|
||||
@@ -291,6 +253,11 @@ const DndMetricSelect = (props: any) => {
|
||||
onDropLabel={handleDropLabel}
|
||||
type={`${DndItemType.AdhocMetricOption}_${props.name}_${props.label}`}
|
||||
multi={multi}
|
||||
datasourceWarningMessage={
|
||||
option instanceof AdhocMetric && option.datasourceWarning
|
||||
? t('This metric might be incompatible with current dataset')
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
),
|
||||
[
|
||||
|
||||
@@ -38,6 +38,7 @@ export default function Option({
|
||||
clickClose,
|
||||
withCaret,
|
||||
isExtra,
|
||||
datasourceWarningMessage,
|
||||
canDelete = true,
|
||||
}: OptionProps) {
|
||||
const theme = useTheme();
|
||||
@@ -60,15 +61,18 @@ export default function Option({
|
||||
</CloseContainer>
|
||||
)}
|
||||
<Label data-test="control-label">{children}</Label>
|
||||
{isExtra && (
|
||||
{(!!datasourceWarningMessage || isExtra) && (
|
||||
<StyledInfoTooltipWithTrigger
|
||||
icon="exclamation-triangle"
|
||||
placement="top"
|
||||
bsStyle="warning"
|
||||
tooltip={t(`
|
||||
tooltip={
|
||||
datasourceWarningMessage ||
|
||||
t(`
|
||||
This filter was inherited from the dashboard's context.
|
||||
It won't be saved when saving the chart.
|
||||
`)}
|
||||
`)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{withCaret && (
|
||||
|
||||
@@ -57,6 +57,7 @@ export default function OptionWrapper(
|
||||
clickClose,
|
||||
withCaret,
|
||||
isExtra,
|
||||
datasourceWarningMessage,
|
||||
canDelete = true,
|
||||
...rest
|
||||
} = props;
|
||||
@@ -176,6 +177,7 @@ export default function OptionWrapper(
|
||||
clickClose={clickClose}
|
||||
withCaret={withCaret}
|
||||
isExtra={isExtra}
|
||||
datasourceWarningMessage={datasourceWarningMessage}
|
||||
canDelete={canDelete}
|
||||
>
|
||||
<Label />
|
||||
|
||||
@@ -30,6 +30,7 @@ export interface OptionProps {
|
||||
clickClose: (index: number) => void;
|
||||
withCaret?: boolean;
|
||||
isExtra?: boolean;
|
||||
datasourceWarningMessage?: string;
|
||||
canDelete?: boolean;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user