mirror of
https://github.com/apache/superset.git
synced 2024-09-06 22:07:34 -04:00
feat(explore): Don't discard controls with custom sql when changing datasource (#20934)
This commit is contained in:
parent
ec20c0104e
commit
cddc361adc
@ -29,6 +29,7 @@ export interface AdhocColumn {
|
|||||||
expressionType: 'SQL';
|
expressionType: 'SQL';
|
||||||
columnType?: 'BASE_AXIS' | 'SERIES';
|
columnType?: 'BASE_AXIS' | 'SERIES';
|
||||||
timeGrain?: string;
|
timeGrain?: string;
|
||||||
|
datasourceWarning?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -17,6 +17,6 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default function isDefined(x: unknown) {
|
export default function isDefined<T>(x: T): x is NonNullable<T> {
|
||||||
return x !== null && x !== undefined;
|
return x !== null && x !== undefined;
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,7 @@ import {
|
|||||||
css,
|
css,
|
||||||
SupersetTheme,
|
SupersetTheme,
|
||||||
useTheme,
|
useTheme,
|
||||||
|
isDefined,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import {
|
import {
|
||||||
ControlPanelSectionConfig,
|
ControlPanelSectionConfig,
|
||||||
@ -45,6 +46,9 @@ import {
|
|||||||
ExpandedControlItem,
|
ExpandedControlItem,
|
||||||
sections,
|
sections,
|
||||||
} from '@superset-ui/chart-controls';
|
} from '@superset-ui/chart-controls';
|
||||||
|
import { useSelector } from 'react-redux';
|
||||||
|
import { rgba } from 'emotion-rgba';
|
||||||
|
import { kebabCase } from 'lodash';
|
||||||
|
|
||||||
import Collapse from 'src/components/Collapse';
|
import Collapse from 'src/components/Collapse';
|
||||||
import Tabs from 'src/components/Tabs';
|
import Tabs from 'src/components/Tabs';
|
||||||
@ -57,9 +61,6 @@ import { ExploreActions } from 'src/explore/actions/exploreActions';
|
|||||||
import { ChartState, ExplorePageState } from 'src/explore/types';
|
import { ChartState, ExplorePageState } from 'src/explore/types';
|
||||||
import { Tooltip } from 'src/components/Tooltip';
|
import { Tooltip } from 'src/components/Tooltip';
|
||||||
import Icons from 'src/components/Icons';
|
import Icons from 'src/components/Icons';
|
||||||
|
|
||||||
import { rgba } from 'emotion-rgba';
|
|
||||||
import { kebabCase } from 'lodash';
|
|
||||||
import ControlRow from './ControlRow';
|
import ControlRow from './ControlRow';
|
||||||
import Control from './Control';
|
import Control from './Control';
|
||||||
import { ExploreAlert } from './ExploreAlert';
|
import { ExploreAlert } from './ExploreAlert';
|
||||||
@ -269,6 +270,36 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => {
|
|||||||
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const controlsTransferred = useSelector<
|
||||||
|
ExplorePageState,
|
||||||
|
string[] | undefined
|
||||||
|
>(state => state.explore.controlsTransferred);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.chart.chartStatus === 'success') {
|
||||||
|
controlsTransferred?.forEach(controlName => {
|
||||||
|
const alteredControls = ensureIsArray(
|
||||||
|
props.controls[controlName].value,
|
||||||
|
).map(value => {
|
||||||
|
if (
|
||||||
|
typeof value === 'object' &&
|
||||||
|
isDefined(value) &&
|
||||||
|
'datasourceWarning' in value
|
||||||
|
) {
|
||||||
|
return { ...value, datasourceWarning: false };
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
});
|
||||||
|
props.actions.setControlValue(controlName, alteredControls);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
controlsTransferred,
|
||||||
|
props.actions,
|
||||||
|
props.chart.chartStatus,
|
||||||
|
props.controls,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
prevDatasource &&
|
prevDatasource &&
|
||||||
@ -455,7 +486,7 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => {
|
|||||||
>
|
>
|
||||||
<Icons.InfoCircleOutlined
|
<Icons.InfoCircleOutlined
|
||||||
css={css`
|
css={css`
|
||||||
${iconStyles}
|
${iconStyles};
|
||||||
color: ${errorColor};
|
color: ${errorColor};
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
@ -591,7 +622,7 @@ export const ControlPanelsContainer = (props: ControlPanelsContainerProps) => {
|
|||||||
>
|
>
|
||||||
<Icons.ExclamationCircleOutlined
|
<Icons.ExclamationCircleOutlined
|
||||||
css={css`
|
css={css`
|
||||||
${iconStyles}
|
${iconStyles};
|
||||||
color: ${errorColor};
|
color: ${errorColor};
|
||||||
`}
|
`}
|
||||||
/>
|
/>
|
||||||
|
@ -458,6 +458,7 @@ function ExploreViewContainer(props) {
|
|||||||
!areObjectsEqual(
|
!areObjectsEqual(
|
||||||
props.controls[key].value,
|
props.controls[key].value,
|
||||||
lastQueriedControls[key].value,
|
lastQueriedControls[key].value,
|
||||||
|
{ ignoreFields: ['datasourceWarning'] },
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { t } from '@superset-ui/core';
|
||||||
import { DndItemType } from 'src/explore/components/DndItemType';
|
import { DndItemType } from 'src/explore/components/DndItemType';
|
||||||
import AdhocFilterPopoverTrigger from 'src/explore/components/controls/FilterControl/AdhocFilterPopoverTrigger';
|
import AdhocFilterPopoverTrigger from 'src/explore/components/controls/FilterControl/AdhocFilterPopoverTrigger';
|
||||||
import AdhocFilter from 'src/explore/components/controls/FilterControl/AdhocFilter';
|
import AdhocFilter from 'src/explore/components/controls/FilterControl/AdhocFilter';
|
||||||
@ -63,6 +64,11 @@ export default function DndAdhocFilterOption({
|
|||||||
type={DndItemType.FilterOption}
|
type={DndItemType.FilterOption}
|
||||||
withCaret
|
withCaret
|
||||||
isExtra={adhocFilter.isExtra}
|
isExtra={adhocFilter.isExtra}
|
||||||
|
datasourceWarningMessage={
|
||||||
|
adhocFilter.datasourceWarning
|
||||||
|
? t('This filter might be incompatible with current dataset')
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</AdhocFilterPopoverTrigger>
|
</AdhocFilterPopoverTrigger>
|
||||||
);
|
);
|
||||||
|
@ -23,6 +23,8 @@ import {
|
|||||||
isFeatureEnabled,
|
isFeatureEnabled,
|
||||||
tn,
|
tn,
|
||||||
QueryFormColumn,
|
QueryFormColumn,
|
||||||
|
t,
|
||||||
|
isAdhocColumn,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import {
|
import {
|
||||||
ColumnMeta,
|
ColumnMeta,
|
||||||
@ -35,7 +37,6 @@ import OptionWrapper from 'src/explore/components/controls/DndColumnSelectContro
|
|||||||
import { OptionSelector } from 'src/explore/components/controls/DndColumnSelectControl/utils';
|
import { OptionSelector } from 'src/explore/components/controls/DndColumnSelectControl/utils';
|
||||||
import { DatasourcePanelDndItem } from 'src/explore/components/DatasourcePanel/types';
|
import { DatasourcePanelDndItem } from 'src/explore/components/DatasourcePanel/types';
|
||||||
import { DndItemType } from 'src/explore/components/DndItemType';
|
import { DndItemType } from 'src/explore/components/DndItemType';
|
||||||
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';
|
import SelectControl from '../SelectControl';
|
||||||
@ -68,34 +69,6 @@ function DndColumnSelect(props: DndColumnSelectProps) {
|
|||||||
return new OptionSelector(optionsMap, multi, value);
|
return new OptionSelector(optionsMap, multi, value);
|
||||||
}, [multi, options, 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(
|
const onDrop = useCallback(
|
||||||
(item: DatasourcePanelDndItem) => {
|
(item: DatasourcePanelDndItem) => {
|
||||||
const column = item.value as ColumnMeta;
|
const column = item.value as ColumnMeta;
|
||||||
@ -142,8 +115,12 @@ function DndColumnSelect(props: DndColumnSelectProps) {
|
|||||||
|
|
||||||
const valuesRenderer = useCallback(
|
const valuesRenderer = useCallback(
|
||||||
() =>
|
() =>
|
||||||
optionSelector.values.map((column, idx) =>
|
optionSelector.values.map((column, idx) => {
|
||||||
clickEnabled ? (
|
const datasourceWarningMessage =
|
||||||
|
isAdhocColumn(column) && column.datasourceWarning
|
||||||
|
? t('This column might be incompatible with current dataset')
|
||||||
|
: undefined;
|
||||||
|
return clickEnabled ? (
|
||||||
<ColumnSelectPopoverTrigger
|
<ColumnSelectPopoverTrigger
|
||||||
key={idx}
|
key={idx}
|
||||||
columns={options}
|
columns={options}
|
||||||
@ -166,6 +143,7 @@ function DndColumnSelect(props: DndColumnSelectProps) {
|
|||||||
type={`${DndItemType.ColumnOption}_${name}_${label}`}
|
type={`${DndItemType.ColumnOption}_${name}_${label}`}
|
||||||
canDelete={canDelete}
|
canDelete={canDelete}
|
||||||
column={column}
|
column={column}
|
||||||
|
datasourceWarningMessage={datasourceWarningMessage}
|
||||||
withCaret
|
withCaret
|
||||||
/>
|
/>
|
||||||
</ColumnSelectPopoverTrigger>
|
</ColumnSelectPopoverTrigger>
|
||||||
@ -178,9 +156,10 @@ function DndColumnSelect(props: DndColumnSelectProps) {
|
|||||||
type={`${DndItemType.ColumnOption}_${name}_${label}`}
|
type={`${DndItemType.ColumnOption}_${name}_${label}`}
|
||||||
canDelete={canDelete}
|
canDelete={canDelete}
|
||||||
column={column}
|
column={column}
|
||||||
|
datasourceWarningMessage={datasourceWarningMessage}
|
||||||
/>
|
/>
|
||||||
),
|
);
|
||||||
),
|
}),
|
||||||
[
|
[
|
||||||
canDelete,
|
canDelete,
|
||||||
clickEnabled,
|
clickEnabled,
|
||||||
|
@ -133,6 +133,7 @@ test('remove selected custom metric when metric gets removed from dataset', () =
|
|||||||
);
|
);
|
||||||
expect(screen.getByText('metric_a')).toBeVisible();
|
expect(screen.getByText('metric_a')).toBeVisible();
|
||||||
expect(screen.queryByText('Metric B')).not.toBeInTheDocument();
|
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_a)')).toBeVisible();
|
||||||
expect(screen.getByText('SUM(Column B)')).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(
|
rerender(
|
||||||
<DndMetricSelect
|
<DndMetricSelect
|
||||||
{...newPropsWithRemovedMetric}
|
{...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(
|
rerender(
|
||||||
<DndMetricSelect
|
<DndMetricSelect
|
||||||
{...newPropsWithRemovedColumn}
|
{...newPropsWithRemovedColumn}
|
||||||
|
@ -22,14 +22,15 @@ import {
|
|||||||
ensureIsArray,
|
ensureIsArray,
|
||||||
FeatureFlag,
|
FeatureFlag,
|
||||||
GenericDataType,
|
GenericDataType,
|
||||||
|
isAdhocMetricSimple,
|
||||||
isFeatureEnabled,
|
isFeatureEnabled,
|
||||||
|
isSavedMetric,
|
||||||
Metric,
|
Metric,
|
||||||
QueryFormMetric,
|
QueryFormMetric,
|
||||||
|
t,
|
||||||
tn,
|
tn,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import { ColumnMeta, withDndFallback } from '@superset-ui/chart-controls';
|
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 AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric';
|
||||||
import AdhocMetricPopoverTrigger from 'src/explore/components/controls/MetricControl/AdhocMetricPopoverTrigger';
|
import AdhocMetricPopoverTrigger from 'src/explore/components/controls/MetricControl/AdhocMetricPopoverTrigger';
|
||||||
import MetricDefinitionValue from 'src/explore/components/controls/MetricControl/MetricDefinitionValue';
|
import MetricDefinitionValue from 'src/explore/components/controls/MetricControl/MetricDefinitionValue';
|
||||||
@ -46,24 +47,49 @@ 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];
|
||||||
|
|
||||||
const isDictionaryForAdhocMetric = (value: any) =>
|
const isDictionaryForAdhocMetric = (value: QueryFormMetric) =>
|
||||||
value && !(value instanceof AdhocMetric) && value.expressionType;
|
value &&
|
||||||
|
!(value instanceof AdhocMetric) &&
|
||||||
|
typeof value !== 'string' &&
|
||||||
|
value.expressionType;
|
||||||
|
|
||||||
const coerceAdhocMetrics = (value: any) => {
|
const coerceMetrics = (
|
||||||
if (!value) {
|
addedMetrics: QueryFormMetric | QueryFormMetric[] | undefined | null,
|
||||||
|
savedMetrics: Metric[],
|
||||||
|
columns: ColumnMeta[],
|
||||||
|
) => {
|
||||||
|
if (!addedMetrics) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
if (!Array.isArray(value)) {
|
const metricsCompatibleWithDataset = ensureIsArray(addedMetrics).filter(
|
||||||
if (isDictionaryForAdhocMetric(value)) {
|
metric => {
|
||||||
return [new AdhocMetric(value)];
|
if (isSavedMetric(metric)) {
|
||||||
|
return savedMetrics.some(
|
||||||
|
savedMetric => savedMetric.metric_name === metric,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return [value];
|
if (isAdhocMetricSimple(metric)) {
|
||||||
|
return columns.some(
|
||||||
|
column => column.column_name === metric.column.column_name,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return value.map(val => {
|
return true;
|
||||||
if (isDictionaryForAdhocMetric(val)) {
|
},
|
||||||
return new AdhocMetric(val);
|
);
|
||||||
|
|
||||||
|
return metricsCompatibleWithDataset.map(metric => {
|
||||||
|
if (!isDictionaryForAdhocMetric(metric)) {
|
||||||
|
return metric;
|
||||||
}
|
}
|
||||||
return val;
|
if (isAdhocMetricSimple(metric)) {
|
||||||
|
const column = columns.find(
|
||||||
|
col => col.column_name === metric.column.column_name,
|
||||||
|
);
|
||||||
|
if (column) {
|
||||||
|
return new AdhocMetric({ ...metric, column });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new AdhocMetric(metric);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -81,53 +107,8 @@ const getOptionsForSavedMetrics = (
|
|||||||
|
|
||||||
type ValueType = Metric | AdhocMetric | QueryFormMetric;
|
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 DndMetricSelect = (props: any) => {
|
||||||
const { onChange, multi, columns, savedMetrics } = props;
|
const { onChange, multi } = props;
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
opts => {
|
opts => {
|
||||||
@ -153,39 +134,20 @@ const DndMetricSelect = (props: any) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const [value, setValue] = useState<ValueType[]>(
|
const [value, setValue] = useState<ValueType[]>(
|
||||||
coerceAdhocMetrics(props.value),
|
coerceMetrics(props.value, props.savedMetrics, props.columns),
|
||||||
);
|
);
|
||||||
const [droppedItem, setDroppedItem] = useState<
|
const [droppedItem, setDroppedItem] = useState<
|
||||||
DatasourcePanelDndItem | typeof EMPTY_OBJECT
|
DatasourcePanelDndItem | typeof EMPTY_OBJECT
|
||||||
>({});
|
>({});
|
||||||
const [newMetricPopoverVisible, setNewMetricPopoverVisible] = useState(false);
|
const [newMetricPopoverVisible, setNewMetricPopoverVisible] = useState(false);
|
||||||
const prevColumns = usePrevious(columns);
|
|
||||||
const prevSavedMetrics = usePrevious(savedMetrics);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setValue(coerceAdhocMetrics(props.value));
|
setValue(coerceMetrics(props.value, props.savedMetrics, props.columns));
|
||||||
}, [JSON.stringify(props.value)]);
|
}, [
|
||||||
|
JSON.stringify(props.value),
|
||||||
useEffect(() => {
|
JSON.stringify(props.savedMetrics),
|
||||||
// Remove selected custom metrics that do not exist in the dataset anymore
|
JSON.stringify(props.columns),
|
||||||
// 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]);
|
|
||||||
|
|
||||||
const canDrop = useCallback(
|
const canDrop = useCallback(
|
||||||
(item: DatasourcePanelDndItem) => {
|
(item: DatasourcePanelDndItem) => {
|
||||||
@ -291,6 +253,11 @@ const DndMetricSelect = (props: any) => {
|
|||||||
onDropLabel={handleDropLabel}
|
onDropLabel={handleDropLabel}
|
||||||
type={`${DndItemType.AdhocMetricOption}_${props.name}_${props.label}`}
|
type={`${DndItemType.AdhocMetricOption}_${props.name}_${props.label}`}
|
||||||
multi={multi}
|
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,
|
clickClose,
|
||||||
withCaret,
|
withCaret,
|
||||||
isExtra,
|
isExtra,
|
||||||
|
datasourceWarningMessage,
|
||||||
canDelete = true,
|
canDelete = true,
|
||||||
}: OptionProps) {
|
}: OptionProps) {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@ -60,15 +61,18 @@ export default function Option({
|
|||||||
</CloseContainer>
|
</CloseContainer>
|
||||||
)}
|
)}
|
||||||
<Label data-test="control-label">{children}</Label>
|
<Label data-test="control-label">{children}</Label>
|
||||||
{isExtra && (
|
{(!!datasourceWarningMessage || isExtra) && (
|
||||||
<StyledInfoTooltipWithTrigger
|
<StyledInfoTooltipWithTrigger
|
||||||
icon="exclamation-triangle"
|
icon="exclamation-triangle"
|
||||||
placement="top"
|
placement="top"
|
||||||
bsStyle="warning"
|
bsStyle="warning"
|
||||||
tooltip={t(`
|
tooltip={
|
||||||
|
datasourceWarningMessage ||
|
||||||
|
t(`
|
||||||
This filter was inherited from the dashboard's context.
|
This filter was inherited from the dashboard's context.
|
||||||
It won't be saved when saving the chart.
|
It won't be saved when saving the chart.
|
||||||
`)}
|
`)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{withCaret && (
|
{withCaret && (
|
||||||
|
@ -57,6 +57,7 @@ export default function OptionWrapper(
|
|||||||
clickClose,
|
clickClose,
|
||||||
withCaret,
|
withCaret,
|
||||||
isExtra,
|
isExtra,
|
||||||
|
datasourceWarningMessage,
|
||||||
canDelete = true,
|
canDelete = true,
|
||||||
...rest
|
...rest
|
||||||
} = props;
|
} = props;
|
||||||
@ -176,6 +177,7 @@ export default function OptionWrapper(
|
|||||||
clickClose={clickClose}
|
clickClose={clickClose}
|
||||||
withCaret={withCaret}
|
withCaret={withCaret}
|
||||||
isExtra={isExtra}
|
isExtra={isExtra}
|
||||||
|
datasourceWarningMessage={datasourceWarningMessage}
|
||||||
canDelete={canDelete}
|
canDelete={canDelete}
|
||||||
>
|
>
|
||||||
<Label />
|
<Label />
|
||||||
|
@ -30,6 +30,7 @@ export interface OptionProps {
|
|||||||
clickClose: (index: number) => void;
|
clickClose: (index: number) => void;
|
||||||
withCaret?: boolean;
|
withCaret?: boolean;
|
||||||
isExtra?: boolean;
|
isExtra?: boolean;
|
||||||
|
datasourceWarningMessage?: string;
|
||||||
canDelete?: boolean;
|
canDelete?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,6 +36,7 @@ describe('AdhocFilter', () => {
|
|||||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||||
subject: 'value',
|
subject: 'value',
|
||||||
operator: '>',
|
operator: '>',
|
||||||
|
datasourceWarning: false,
|
||||||
comparator: '10',
|
comparator: '10',
|
||||||
clause: CLAUSES.WHERE,
|
clause: CLAUSES.WHERE,
|
||||||
filterOptionName: adhocFilter.filterOptionName,
|
filterOptionName: adhocFilter.filterOptionName,
|
||||||
|
@ -118,6 +118,7 @@ export default class AdhocFilter {
|
|||||||
}
|
}
|
||||||
this.isExtra = !!adhocFilter.isExtra;
|
this.isExtra = !!adhocFilter.isExtra;
|
||||||
this.isNew = !!adhocFilter.isNew;
|
this.isNew = !!adhocFilter.isNew;
|
||||||
|
this.datasourceWarning = !!adhocFilter.datasourceWarning;
|
||||||
|
|
||||||
this.filterOptionName =
|
this.filterOptionName =
|
||||||
adhocFilter.filterOptionName ||
|
adhocFilter.filterOptionName ||
|
||||||
|
@ -76,6 +76,7 @@ export default class AdhocMetric {
|
|||||||
this.aggregate = null;
|
this.aggregate = null;
|
||||||
}
|
}
|
||||||
this.isNew = !!adhocMetric.isNew;
|
this.isNew = !!adhocMetric.isNew;
|
||||||
|
this.datasourceWarning = !!adhocMetric.datasourceWarning;
|
||||||
this.hasCustomLabel = !!(adhocMetric.hasCustomLabel && adhocMetric.label);
|
this.hasCustomLabel = !!(adhocMetric.hasCustomLabel && adhocMetric.label);
|
||||||
this.label = this.hasCustomLabel
|
this.label = this.hasCustomLabel
|
||||||
? adhocMetric.label
|
? adhocMetric.label
|
||||||
|
@ -34,6 +34,7 @@ describe('AdhocMetric', () => {
|
|||||||
expressionType: EXPRESSION_TYPES.SIMPLE,
|
expressionType: EXPRESSION_TYPES.SIMPLE,
|
||||||
column: valueColumn,
|
column: valueColumn,
|
||||||
aggregate: AGGREGATES.SUM,
|
aggregate: AGGREGATES.SUM,
|
||||||
|
datasourceWarning: false,
|
||||||
label: 'SUM(value)',
|
label: 'SUM(value)',
|
||||||
hasCustomLabel: false,
|
hasCustomLabel: false,
|
||||||
optionName: adhocMetric.optionName,
|
optionName: adhocMetric.optionName,
|
||||||
|
@ -38,6 +38,7 @@ const propTypes = {
|
|||||||
index: PropTypes.number,
|
index: PropTypes.number,
|
||||||
type: PropTypes.string,
|
type: PropTypes.string,
|
||||||
multi: PropTypes.bool,
|
multi: PropTypes.bool,
|
||||||
|
datasourceWarningMessage: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
class AdhocMetricOption extends React.PureComponent {
|
class AdhocMetricOption extends React.PureComponent {
|
||||||
@ -64,6 +65,7 @@ class AdhocMetricOption extends React.PureComponent {
|
|||||||
index,
|
index,
|
||||||
type,
|
type,
|
||||||
multi,
|
multi,
|
||||||
|
datasourceWarningMessage,
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -87,6 +89,7 @@ class AdhocMetricOption extends React.PureComponent {
|
|||||||
withCaret
|
withCaret
|
||||||
isFunction
|
isFunction
|
||||||
multi={multi}
|
multi={multi}
|
||||||
|
datasourceWarningMessage={datasourceWarningMessage}
|
||||||
/>
|
/>
|
||||||
</AdhocMetricPopoverTrigger>
|
</AdhocMetricPopoverTrigger>
|
||||||
);
|
);
|
||||||
|
@ -35,6 +35,7 @@ const propTypes = {
|
|||||||
savedMetricsOptions: PropTypes.arrayOf(savedMetricType),
|
savedMetricsOptions: PropTypes.arrayOf(savedMetricType),
|
||||||
multi: PropTypes.bool,
|
multi: PropTypes.bool,
|
||||||
datasource: PropTypes.object,
|
datasource: PropTypes.object,
|
||||||
|
datasourceWarningMessage: PropTypes.string,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function MetricDefinitionValue({
|
export default function MetricDefinitionValue({
|
||||||
@ -50,6 +51,7 @@ export default function MetricDefinitionValue({
|
|||||||
index,
|
index,
|
||||||
type,
|
type,
|
||||||
multi,
|
multi,
|
||||||
|
datasourceWarningMessage,
|
||||||
}) {
|
}) {
|
||||||
const getSavedMetricByName = metricName =>
|
const getSavedMetricByName = metricName =>
|
||||||
savedMetrics.find(metric => metric.metric_name === metricName);
|
savedMetrics.find(metric => metric.metric_name === metricName);
|
||||||
@ -78,6 +80,7 @@ export default function MetricDefinitionValue({
|
|||||||
savedMetric: savedMetric ?? {},
|
savedMetric: savedMetric ?? {},
|
||||||
type,
|
type,
|
||||||
multi,
|
multi,
|
||||||
|
datasourceWarningMessage,
|
||||||
};
|
};
|
||||||
|
|
||||||
return <AdhocMetricOption {...metricOptionProps} />;
|
return <AdhocMetricOption {...metricOptionProps} />;
|
||||||
|
@ -179,6 +179,7 @@ export const OptionControlLabel = ({
|
|||||||
type,
|
type,
|
||||||
index,
|
index,
|
||||||
isExtra,
|
isExtra,
|
||||||
|
datasourceWarningMessage,
|
||||||
tooltipTitle,
|
tooltipTitle,
|
||||||
multi = true,
|
multi = true,
|
||||||
...props
|
...props
|
||||||
@ -195,7 +196,8 @@ export const OptionControlLabel = ({
|
|||||||
type: string;
|
type: string;
|
||||||
index: number;
|
index: number;
|
||||||
isExtra?: boolean;
|
isExtra?: boolean;
|
||||||
tooltipTitle: string;
|
datasourceWarningMessage?: string;
|
||||||
|
tooltipTitle?: string;
|
||||||
multi?: boolean;
|
multi?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
@ -314,15 +316,18 @@ export const OptionControlLabel = ({
|
|||||||
{isFunction && <Icons.FieldDerived />}
|
{isFunction && <Icons.FieldDerived />}
|
||||||
{getLabelContent()}
|
{getLabelContent()}
|
||||||
</Label>
|
</Label>
|
||||||
{isExtra && (
|
{(!!datasourceWarningMessage || isExtra) && (
|
||||||
<StyledInfoTooltipWithTrigger
|
<StyledInfoTooltipWithTrigger
|
||||||
icon="exclamation-triangle"
|
icon="exclamation-triangle"
|
||||||
placement="top"
|
placement="top"
|
||||||
bsStyle="warning"
|
bsStyle="warning"
|
||||||
tooltip={t(`
|
tooltip={
|
||||||
|
datasourceWarningMessage ||
|
||||||
|
t(`
|
||||||
This filter was inherited from the dashboard's context.
|
This filter was inherited from the dashboard's context.
|
||||||
It won't be saved when saving the chart.
|
It won't be saved when saving the chart.
|
||||||
`)}
|
`)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{withCaret && (
|
{withCaret && (
|
||||||
|
@ -210,6 +210,7 @@ test('SQL ad-hoc metric values', () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
|
datasourceWarning: true,
|
||||||
expressionType: 'SQL',
|
expressionType: 'SQL',
|
||||||
sqlExpression: 'select * from sample_column_1;',
|
sqlExpression: 'select * from sample_column_1;',
|
||||||
});
|
});
|
||||||
@ -279,6 +280,7 @@ test('SQL ad-hoc filter values', () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
).toEqual({
|
).toEqual({
|
||||||
|
datasourceWarning: true,
|
||||||
expressionType: 'SQL',
|
expressionType: 'SQL',
|
||||||
sqlExpression: 'select * from sample_column_1;',
|
sqlExpression: 'select * from sample_column_1;',
|
||||||
});
|
});
|
||||||
|
@ -21,9 +21,9 @@ import { ControlState, Dataset, Metric } from '@superset-ui/chart-controls';
|
|||||||
import {
|
import {
|
||||||
Column,
|
Column,
|
||||||
isAdhocMetricSimple,
|
isAdhocMetricSimple,
|
||||||
|
isAdhocMetricSQL,
|
||||||
isSavedMetric,
|
isSavedMetric,
|
||||||
isSimpleAdhocFilter,
|
isSimpleAdhocFilter,
|
||||||
isFreeFormAdhocFilter,
|
|
||||||
JsonValue,
|
JsonValue,
|
||||||
SimpleAdhocFilter,
|
SimpleAdhocFilter,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
@ -72,7 +72,10 @@ const isControlValueCompatibleWithDatasource = (
|
|||||||
column.column_name === (value as SimpleAdhocFilter).subject,
|
column.column_name === (value as SimpleAdhocFilter).subject,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (isFreeFormAdhocFilter(value)) return true;
|
if (isAdhocMetricSQL(value)) {
|
||||||
|
Object.assign(value, { datasourceWarning: true });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -54,14 +54,15 @@ export default function exploreReducer(state = {}, action) {
|
|||||||
const { prevDatasource, newDatasource } = action;
|
const { prevDatasource, newDatasource } = action;
|
||||||
const controls = { ...state.controls };
|
const controls = { ...state.controls };
|
||||||
const controlsTransferred = [];
|
const controlsTransferred = [];
|
||||||
|
|
||||||
if (
|
if (
|
||||||
prevDatasource.id !== newDatasource.id ||
|
prevDatasource.id !== newDatasource.id ||
|
||||||
prevDatasource.type !== newDatasource.type
|
prevDatasource.type !== newDatasource.type
|
||||||
) {
|
) {
|
||||||
// reset time range filter to default
|
// reset time range filter to default
|
||||||
newFormData.time_range = DEFAULT_TIME_RANGE;
|
newFormData.time_range = DEFAULT_TIME_RANGE;
|
||||||
|
|
||||||
newFormData.datasource = newDatasource.uid;
|
newFormData.datasource = newDatasource.uid;
|
||||||
|
}
|
||||||
|
|
||||||
// reset control values for column/metric related controls
|
// reset control values for column/metric related controls
|
||||||
Object.entries(controls).forEach(([controlName, controlState]) => {
|
Object.entries(controls).forEach(([controlName, controlState]) => {
|
||||||
@ -73,9 +74,6 @@ export default function exploreReducer(state = {}, action) {
|
|||||||
'columns' in controlState ||
|
'columns' in controlState ||
|
||||||
('options' in controlState && !Array.isArray(controlState.options))
|
('options' in controlState && !Array.isArray(controlState.options))
|
||||||
) {
|
) {
|
||||||
controls[controlName] = {
|
|
||||||
...controlState,
|
|
||||||
};
|
|
||||||
newFormData[controlName] = getControlValuesCompatibleWithDatasource(
|
newFormData[controlName] = getControlValuesCompatibleWithDatasource(
|
||||||
newDatasource,
|
newDatasource,
|
||||||
controlState,
|
controlState,
|
||||||
@ -89,7 +87,6 @@ export default function exploreReducer(state = {}, action) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
const newState = {
|
const newState = {
|
||||||
...state,
|
...state,
|
||||||
|
@ -19,7 +19,15 @@
|
|||||||
import shortid from 'shortid';
|
import shortid from 'shortid';
|
||||||
import { compose } from 'redux';
|
import { compose } from 'redux';
|
||||||
import persistState, { StorageAdapter } from 'redux-localstorage';
|
import persistState, { StorageAdapter } from 'redux-localstorage';
|
||||||
import { isEqual, omitBy, isUndefined, isNull } from 'lodash';
|
import {
|
||||||
|
isEqual,
|
||||||
|
omitBy,
|
||||||
|
omit,
|
||||||
|
isUndefined,
|
||||||
|
isNull,
|
||||||
|
isEqualWith,
|
||||||
|
} from 'lodash';
|
||||||
|
import { ensureIsArray } from '@superset-ui/core';
|
||||||
|
|
||||||
export function addToObject(
|
export function addToObject(
|
||||||
state: Record<string, any>,
|
state: Record<string, any>,
|
||||||
@ -181,7 +189,8 @@ export function areObjectsEqual(
|
|||||||
opts: {
|
opts: {
|
||||||
ignoreUndefined?: boolean;
|
ignoreUndefined?: boolean;
|
||||||
ignoreNull?: boolean;
|
ignoreNull?: boolean;
|
||||||
} = { ignoreUndefined: false, ignoreNull: false },
|
ignoreFields?: string[];
|
||||||
|
} = { ignoreUndefined: false, ignoreNull: false, ignoreFields: [] },
|
||||||
) {
|
) {
|
||||||
let comp1 = obj1;
|
let comp1 = obj1;
|
||||||
let comp2 = obj2;
|
let comp2 = obj2;
|
||||||
@ -193,5 +202,14 @@ export function areObjectsEqual(
|
|||||||
comp1 = omitBy(comp1, isNull);
|
comp1 = omitBy(comp1, isNull);
|
||||||
comp2 = omitBy(comp2, isNull);
|
comp2 = omitBy(comp2, isNull);
|
||||||
}
|
}
|
||||||
|
if (opts.ignoreFields?.length) {
|
||||||
|
const ignoreFields = ensureIsArray(opts.ignoreFields);
|
||||||
|
return isEqualWith(comp1, comp2, (val1, val2) =>
|
||||||
|
isEqual(
|
||||||
|
ensureIsArray(val1).map(value => omit(value, ignoreFields)),
|
||||||
|
ensureIsArray(val2).map(value => omit(value, ignoreFields)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
return isEqual(comp1, comp2);
|
return isEqual(comp1, comp2);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user