mirror of
https://github.com/apache/superset.git
synced 2026-07-19 21:25:38 +00:00
454 lines
13 KiB
TypeScript
454 lines
13 KiB
TypeScript
/**
|
|
* Licensed to the Apache Software Foundation (ASF) under one
|
|
* or more contributor license agreements. See the NOTICE file
|
|
* distributed with this work for additional information
|
|
* regarding copyright ownership. The ASF licenses this file
|
|
* to you under the Apache License, Version 2.0 (the
|
|
* "License"); you may not use this file except in compliance
|
|
* with the License. You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing,
|
|
* software distributed under the License is distributed on an
|
|
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
* KIND, either express or implied. See the License for the
|
|
* specific language governing permissions and limitations
|
|
* under the License.
|
|
*/
|
|
import { useCallback, useMemo, useState } from 'react';
|
|
import { t } from '@apache-superset/core';
|
|
import {
|
|
AdhocColumn,
|
|
isAdhocColumn,
|
|
Metric,
|
|
ensureIsArray,
|
|
Datasource,
|
|
QueryFormMetric,
|
|
QueryFormData,
|
|
} from '@superset-ui/core';
|
|
import { tn } from '@apache-superset/core';
|
|
import { ColumnMeta, isColumnMeta } from '@superset-ui/chart-controls';
|
|
import { isString } from 'lodash';
|
|
import DndSelectLabel from 'src/explore/components/controls/DndColumnSelectControl/DndSelectLabel';
|
|
import OptionWrapper from 'src/explore/components/controls/DndColumnSelectControl/OptionWrapper';
|
|
import { DatasourcePanelDndItem } from 'src/explore/components/DatasourcePanel/types';
|
|
import { DndItemType } from 'src/explore/components/DndItemType';
|
|
import AdhocMetric from 'src/explore/components/controls/MetricControl/AdhocMetric';
|
|
import MetricDefinitionValue from 'src/explore/components/controls/MetricControl/MetricDefinitionValue';
|
|
import ColumnSelectPopoverTrigger from './ColumnSelectPopoverTrigger';
|
|
import { DndControlProps } from './types';
|
|
|
|
const AGGREGATED_DECK_GL_CHART_TYPES = [
|
|
'deck_screengrid',
|
|
'deck_heatmap',
|
|
'deck_contour',
|
|
'deck_hex',
|
|
'deck_grid',
|
|
];
|
|
|
|
const MULTI_VALUE_WARNING_MESSAGE =
|
|
'This metric or column contains many values, they may not be able to be all displayed in the tooltip';
|
|
|
|
interface TooltipItem {
|
|
item_type?: string;
|
|
column_name?: string;
|
|
metric_name?: string;
|
|
label?: string;
|
|
}
|
|
|
|
function isAggregatedDeckGLChart(vizType: string): boolean {
|
|
return AGGREGATED_DECK_GL_CHART_TYPES.includes(vizType);
|
|
}
|
|
|
|
function fieldHasMultipleValues(
|
|
item: TooltipItem | string,
|
|
formData: QueryFormData,
|
|
): boolean {
|
|
if (!formData?.viz_type || !isAggregatedDeckGLChart(formData.viz_type)) {
|
|
return false;
|
|
}
|
|
|
|
if (typeof item === 'string') {
|
|
return true;
|
|
}
|
|
|
|
if (item.item_type === 'metric') {
|
|
return false;
|
|
}
|
|
|
|
if (item.item_type === 'column') {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
const DND_ACCEPTED_TYPES = [DndItemType.Column, DndItemType.Metric];
|
|
|
|
type ColumnMetricValue =
|
|
| string
|
|
| AdhocColumn
|
|
| AdhocMetric
|
|
| { metric_name: string; error_text: string; uuid: string };
|
|
|
|
export type DndColumnMetricSelectProps = DndControlProps<ColumnMetricValue> & {
|
|
columns: ColumnMeta[];
|
|
savedMetrics: Metric[];
|
|
datasource?: Datasource;
|
|
selectedMetrics?: QueryFormMetric[];
|
|
isTemporal?: boolean;
|
|
disabledTabs?: Set<string>;
|
|
formData?: any;
|
|
};
|
|
|
|
const isDictionaryForAdhocMetric = (
|
|
value: unknown,
|
|
): value is Record<string, unknown> =>
|
|
value !== null &&
|
|
value !== undefined &&
|
|
!(value instanceof AdhocMetric) &&
|
|
typeof value === 'object' &&
|
|
'expressionType' in value;
|
|
|
|
function DndColumnMetricSelect(props: DndColumnMetricSelectProps) {
|
|
const {
|
|
value,
|
|
columns = [],
|
|
savedMetrics = [],
|
|
selectedMetrics = [],
|
|
datasource,
|
|
multi = true,
|
|
onChange,
|
|
canDelete = true,
|
|
ghostButtonText,
|
|
name,
|
|
label,
|
|
isTemporal,
|
|
disabledTabs,
|
|
formData,
|
|
} = props;
|
|
|
|
const [newColumnPopoverVisible, setNewColumnPopoverVisible] = useState(false);
|
|
|
|
const combinedOptionsMap = useMemo(() => {
|
|
const optionsMap: Record<string, ColumnMeta> = {};
|
|
|
|
columns.forEach(column => {
|
|
optionsMap[column.column_name] = column;
|
|
});
|
|
|
|
return optionsMap;
|
|
}, [columns]);
|
|
|
|
const isMetricSelected = useCallback(
|
|
(metricName: string) => {
|
|
if (!selectedMetrics || selectedMetrics.length === 0) {
|
|
return false;
|
|
}
|
|
|
|
return selectedMetrics.some(metric => {
|
|
if (isString(metric)) {
|
|
return metric === metricName;
|
|
}
|
|
if (metric instanceof AdhocMetric) {
|
|
return metric.label === metricName;
|
|
}
|
|
if (metric && typeof metric === 'object') {
|
|
if (metric instanceof AdhocMetric) {
|
|
return metric.label === metricName;
|
|
}
|
|
return 'metric_name' in metric && metric.metric_name === metricName;
|
|
}
|
|
return false;
|
|
});
|
|
},
|
|
[selectedMetrics],
|
|
);
|
|
|
|
const coercedValue = useMemo(() => {
|
|
if (!value) return [];
|
|
|
|
const valueArray = ensureIsArray(value);
|
|
return valueArray.map(item => {
|
|
if (isString(item) && combinedOptionsMap[item]) {
|
|
return item;
|
|
}
|
|
|
|
if (isString(item) && savedMetrics.some(m => m.metric_name === item)) {
|
|
return item;
|
|
}
|
|
|
|
if (isAdhocColumn(item)) {
|
|
return item;
|
|
}
|
|
|
|
if (isDictionaryForAdhocMetric(item)) {
|
|
return new AdhocMetric(item);
|
|
}
|
|
|
|
if (item instanceof AdhocMetric) {
|
|
return item;
|
|
}
|
|
|
|
return item;
|
|
});
|
|
}, [value, combinedOptionsMap, savedMetrics]);
|
|
|
|
const onDrop = useCallback(
|
|
(item: DatasourcePanelDndItem) => {
|
|
const newValues = [...coercedValue];
|
|
|
|
if (item.type === DndItemType.Column) {
|
|
const column = item.value as ColumnMeta;
|
|
|
|
if (!multi && newValues.length > 0) {
|
|
newValues[0] = column.column_name;
|
|
} else {
|
|
newValues.push(column.column_name);
|
|
}
|
|
}
|
|
if (item.type === DndItemType.Metric) {
|
|
const metric = item.value as Metric;
|
|
|
|
if (!multi && newValues.length > 0) {
|
|
newValues[0] = metric.metric_name;
|
|
} else {
|
|
newValues.push(metric.metric_name);
|
|
}
|
|
}
|
|
|
|
onChange(multi ? newValues : newValues[0]);
|
|
},
|
|
[onChange, coercedValue, multi],
|
|
);
|
|
|
|
const canDrop = useCallback(
|
|
(item: DatasourcePanelDndItem) => {
|
|
if (item.type === DndItemType.Column) {
|
|
const columnName = (item.value as ColumnMeta).column_name;
|
|
return (
|
|
columnName in combinedOptionsMap &&
|
|
!coercedValue.some(v => v === columnName)
|
|
);
|
|
}
|
|
if (item.type === DndItemType.Metric) {
|
|
const metric = item.value as Metric;
|
|
const metricName = metric.metric_name;
|
|
|
|
return (
|
|
isMetricSelected(metricName) &&
|
|
!coercedValue.some(v => v === metricName)
|
|
);
|
|
}
|
|
return false;
|
|
},
|
|
[combinedOptionsMap, coercedValue, isMetricSelected],
|
|
);
|
|
|
|
const onClickClose = useCallback(
|
|
(index: number) => {
|
|
const newValues = [...coercedValue];
|
|
newValues.splice(index, 1);
|
|
onChange(multi ? newValues : newValues[0]);
|
|
},
|
|
[onChange, coercedValue, multi],
|
|
);
|
|
|
|
const onShiftOptions = useCallback(
|
|
(dragIndex: number, hoverIndex: number) => {
|
|
const newValues = [...coercedValue];
|
|
[newValues[hoverIndex], newValues[dragIndex]] = [
|
|
newValues[dragIndex],
|
|
newValues[hoverIndex],
|
|
];
|
|
onChange(multi ? newValues : newValues[0]);
|
|
},
|
|
[onChange, coercedValue, multi],
|
|
);
|
|
|
|
const isColumnValue = (val: unknown): val is string =>
|
|
isString(val) && !!combinedOptionsMap[val];
|
|
|
|
const valuesRenderer = useCallback(
|
|
() =>
|
|
coercedValue.map((item, idx) => {
|
|
if (isColumnValue(item)) {
|
|
const column = combinedOptionsMap[item];
|
|
const datasourceWarningMessage = undefined;
|
|
const withCaret = true;
|
|
|
|
const columnItem = {
|
|
item_type: 'column',
|
|
column_name: item,
|
|
};
|
|
const hasMultipleValues =
|
|
formData && fieldHasMultipleValues(columnItem, formData);
|
|
const multiValueWarningMessage = hasMultipleValues
|
|
? MULTI_VALUE_WARNING_MESSAGE
|
|
: undefined;
|
|
|
|
return (
|
|
<ColumnSelectPopoverTrigger
|
|
key={`column-${idx}`}
|
|
columns={columns}
|
|
onColumnEdit={newColumn => {
|
|
const newValues = [...coercedValue];
|
|
if (isColumnMeta(newColumn)) {
|
|
newValues[idx] = newColumn.column_name;
|
|
} else {
|
|
newValues[idx] = newColumn;
|
|
}
|
|
onChange(multi ? newValues : newValues[0]);
|
|
}}
|
|
editedColumn={column}
|
|
isTemporal={isTemporal}
|
|
disabledTabs={disabledTabs}
|
|
>
|
|
<OptionWrapper
|
|
key={`column-${idx}`}
|
|
index={idx}
|
|
clickClose={onClickClose}
|
|
onShiftOptions={onShiftOptions}
|
|
type={`${DndItemType.ColumnOption}_${name}_${label}`}
|
|
canDelete={canDelete}
|
|
column={column}
|
|
datasourceWarningMessage={datasourceWarningMessage}
|
|
withCaret={withCaret}
|
|
multiValueWarningMessage={multiValueWarningMessage}
|
|
/>
|
|
</ColumnSelectPopoverTrigger>
|
|
);
|
|
}
|
|
const datasourceWarningMessage =
|
|
(item instanceof AdhocMetric && item.datasourceWarning) ||
|
|
(item &&
|
|
typeof item === 'object' &&
|
|
'error_text' in item &&
|
|
item.error_text)
|
|
? t('This metric might be incompatible with current dataset')
|
|
: undefined;
|
|
|
|
return (
|
|
<MetricDefinitionValue
|
|
key={`metric-${idx}`}
|
|
index={idx}
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
option={item as any}
|
|
onMetricEdit={(changedMetric: Metric | AdhocMetric) => {
|
|
const newValues = [...coercedValue];
|
|
if (changedMetric instanceof AdhocMetric) {
|
|
newValues[idx] = changedMetric;
|
|
} else {
|
|
newValues[idx] = (changedMetric as Metric).metric_name;
|
|
}
|
|
onChange(multi ? newValues : newValues[0]);
|
|
}}
|
|
onRemoveMetric={onClickClose}
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
columns={columns as any}
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
savedMetrics={savedMetrics as any}
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
savedMetricsOptions={savedMetrics as any}
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
datasource={datasource as any}
|
|
onMoveLabel={onShiftOptions}
|
|
onDropLabel={() => {}}
|
|
type={`${DndItemType.AdhocMetricOption}_${name}_${label}`}
|
|
multi={multi}
|
|
datasourceWarningMessage={datasourceWarningMessage}
|
|
/>
|
|
);
|
|
}),
|
|
[
|
|
coercedValue,
|
|
isColumnValue,
|
|
combinedOptionsMap,
|
|
columns,
|
|
onChange,
|
|
multi,
|
|
isTemporal,
|
|
disabledTabs,
|
|
onClickClose,
|
|
onShiftOptions,
|
|
name,
|
|
label,
|
|
canDelete,
|
|
savedMetrics,
|
|
datasource,
|
|
],
|
|
);
|
|
|
|
const labelGhostButtonText = useMemo(
|
|
() =>
|
|
ghostButtonText ??
|
|
tn(
|
|
'Drop a column/metric here or click',
|
|
'Drop columns/metrics here or click',
|
|
multi ? 2 : 1,
|
|
),
|
|
[ghostButtonText, multi],
|
|
);
|
|
|
|
const toggleColumnPopover = useCallback((visible: boolean) => {
|
|
setNewColumnPopoverVisible(visible);
|
|
}, []);
|
|
|
|
const closeColumnPopover = useCallback(() => {
|
|
toggleColumnPopover(false);
|
|
}, [toggleColumnPopover]);
|
|
|
|
const handleClickGhostButton = useCallback(() => {
|
|
toggleColumnPopover(true);
|
|
}, [toggleColumnPopover]);
|
|
|
|
const addNewColumn = useCallback(
|
|
(newItem: ColumnMeta | AdhocColumn | Metric | AdhocMetric) => {
|
|
const newValues = [...coercedValue];
|
|
if (isColumnMeta(newItem)) {
|
|
newValues.push(newItem.column_name);
|
|
} else if (isAdhocColumn(newItem)) {
|
|
newValues.push(newItem);
|
|
} else if ('metric_name' in newItem && newItem.metric_name) {
|
|
newValues.push(newItem.metric_name);
|
|
} else if (newItem instanceof AdhocMetric) {
|
|
newValues.push(newItem);
|
|
}
|
|
onChange(multi ? newValues : newValues[0]);
|
|
},
|
|
[onChange, coercedValue, multi],
|
|
);
|
|
|
|
return (
|
|
<div>
|
|
<DndSelectLabel
|
|
onDrop={onDrop}
|
|
canDrop={canDrop}
|
|
valuesRenderer={valuesRenderer}
|
|
accept={DND_ACCEPTED_TYPES}
|
|
displayGhostButton={multi || coercedValue.length === 0}
|
|
ghostButtonText={labelGhostButtonText}
|
|
onClickGhostButton={handleClickGhostButton}
|
|
{...props}
|
|
/>
|
|
<ColumnSelectPopoverTrigger
|
|
columns={columns}
|
|
onColumnEdit={addNewColumn}
|
|
isControlledComponent
|
|
visible={newColumnPopoverVisible}
|
|
togglePopover={toggleColumnPopover}
|
|
closePopover={closeColumnPopover}
|
|
isTemporal={false}
|
|
disabledTabs={disabledTabs}
|
|
metrics={savedMetrics}
|
|
selectedMetrics={selectedMetrics}
|
|
>
|
|
<div />
|
|
</ColumnSelectPopoverTrigger>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export { DndColumnMetricSelect };
|