feat(custom-tooltip): custom tooltip on deck.gl charts (#34276)

This commit is contained in:
Richard Fogaca Nienkotter
2025-09-16 11:11:19 -03:00
committed by GitHub
parent bc6859a99d
commit a66737cb05
62 changed files with 4599 additions and 421 deletions

View File

@@ -34,6 +34,9 @@ import {
styled,
css,
DatasourceType,
Metric,
QueryFormMetric,
// useTheme,
} from '@superset-ui/core';
import { ColumnMeta, isSavedExpression } from '@superset-ui/chart-controls';
import Tabs from '@superset-ui/core/components/Tabs';
@@ -74,10 +77,24 @@ const StyledSelect = styled(Select)`
}
`;
const MetricOptionContainer = styled.div`
display: flex;
align-items: center;
`;
const MetricIcon = styled.span`
margin-right: ${({ theme }) => theme.sizeUnit * 2}px;
color: ${({ theme }) => theme.colorSuccess};
`;
const MetricLabel = styled.span`
color: ${({ theme }) => theme.colorText};
`;
export interface ColumnSelectPopoverProps {
columns: ColumnMeta[];
editedColumn?: ColumnMeta | AdhocColumn;
onChange: (column: ColumnMeta | AdhocColumn) => void;
onChange: (column: ColumnMeta | AdhocColumn | Metric) => void;
onClose: () => void;
hasCustomLabel: boolean;
setLabel: (title: string) => void;
@@ -86,6 +103,8 @@ export interface ColumnSelectPopoverProps {
isTemporal?: boolean;
setDatasetModal?: Dispatch<SetStateAction<boolean>>;
disabledTabs?: Set<string>;
metrics?: Metric[];
selectedMetrics?: QueryFormMetric[];
datasource?: any;
}
@@ -116,8 +135,11 @@ const ColumnSelectPopover = ({
setDatasetModal,
setLabel,
disabledTabs = new Set<'saved' | 'simple' | 'sqlExpression'>(),
metrics = [],
selectedMetrics = [],
datasource,
}: ColumnSelectPopoverProps) => {
// const theme = useTheme(); // Unused variable
const datasourceType = useSelector<ExplorePageState, string | undefined>(
state => state.explore.datasource.type,
);
@@ -134,6 +156,9 @@ const ColumnSelectPopover = ({
const [selectedSimpleColumn, setSelectedSimpleColumn] = useState<
ColumnMeta | undefined
>(initialSimpleColumn);
const [selectedMetric, setSelectedMetric] = useState<Metric | undefined>(
undefined,
);
const [selectedTab, setSelectedTab] = useState<string | null>(null);
const [resizeButton, width, height] = useResizeButton(
@@ -159,11 +184,31 @@ const ColumnSelectPopover = ({
[columns],
);
// Filter metrics that are already selected in the chart
const availableMetrics = useMemo(() => {
if (!metrics?.length) return [];
const selectedMetricsSet = new Set(selectedMetrics);
return metrics.filter(metric => selectedMetricsSet.has(metric.metric_name));
}, [metrics, selectedMetrics]);
const columnMap = useMemo(
() => Object.fromEntries(simpleColumns.map(col => [col.column_name, col])),
[simpleColumns],
);
const metricMap = useMemo(
() =>
Object.fromEntries(
availableMetrics.map(metric => [metric.metric_name, metric]),
),
[availableMetrics],
);
const onSqlExpressionChange = useCallback(
sqlExpression => {
setAdhocColumn({ label, sqlExpression, expressionType: 'SQL' });
setSelectedSimpleColumn(undefined);
setSelectedCalculatedColumn(undefined);
setSelectedMetric(undefined);
},
[label],
);
@@ -175,6 +220,7 @@ const ColumnSelectPopover = ({
);
setSelectedCalculatedColumn(selectedColumn);
setSelectedSimpleColumn(undefined);
setSelectedMetric(undefined);
setAdhocColumn(undefined);
setLabel(
selectedColumn?.verbose_name || selectedColumn?.column_name || '',
@@ -190,6 +236,7 @@ const ColumnSelectPopover = ({
);
setSelectedCalculatedColumn(undefined);
setSelectedSimpleColumn(selectedColumn);
setSelectedMetric(undefined);
setAdhocColumn(undefined);
setLabel(
selectedColumn?.verbose_name || selectedColumn?.column_name || '',
@@ -198,6 +245,38 @@ const ColumnSelectPopover = ({
[setLabel, simpleColumns],
);
const onSimpleMetricChange = useCallback(
selectedMetricName => {
const selectedMetric = availableMetrics.find(
metric => metric.metric_name === selectedMetricName,
);
setSelectedCalculatedColumn(undefined);
setSelectedSimpleColumn(undefined);
setSelectedMetric(selectedMetric);
setAdhocColumn(undefined);
setLabel(
selectedMetric?.verbose_name || selectedMetric?.metric_name || '',
);
},
[setLabel, availableMetrics],
);
const onSimpleItemChange = useCallback(
selectedValue => {
const selectedColumn = columnMap[selectedValue];
if (selectedColumn) {
onSimpleColumnChange(selectedValue);
return;
}
const selectedMetric = metricMap[selectedValue];
if (selectedMetric) {
onSimpleMetricChange(selectedValue);
}
},
[columnMap, metricMap, onSimpleColumnChange, onSimpleMetricChange],
);
const defaultActiveTabKey = initialAdhocColumn
? 'sqlExpression'
: selectedCalculatedColumn
@@ -241,10 +320,11 @@ const ColumnSelectPopover = ({
}
const selectedColumn =
adhocColumn || selectedCalculatedColumn || selectedSimpleColumn;
if (!selectedColumn) {
const selectedItem = selectedColumn || selectedMetric;
if (!selectedItem) {
return;
}
onChange(selectedColumn);
onChange(selectedItem);
onClose();
}, [
adhocColumn,
@@ -253,11 +333,13 @@ const ColumnSelectPopover = ({
onClose,
selectedCalculatedColumn,
selectedSimpleColumn,
selectedMetric,
]);
const onResetStateAndClose = useCallback(() => {
setSelectedCalculatedColumn(initialCalculatedColumn);
setSelectedSimpleColumn(initialSimpleColumn);
setSelectedMetric(undefined);
setAdhocColumn(initialAdhocColumn);
onClose();
}, [
@@ -285,16 +367,20 @@ const ColumnSelectPopover = ({
};
const stateIsValid =
adhocColumn || selectedCalculatedColumn || selectedSimpleColumn;
adhocColumn ||
selectedCalculatedColumn ||
selectedSimpleColumn ||
selectedMetric;
const hasUnsavedChanges =
initialLabel !== label ||
selectedCalculatedColumn?.column_name !==
initialCalculatedColumn?.column_name ||
selectedSimpleColumn?.column_name !== initialSimpleColumn?.column_name ||
selectedMetric?.metric_name !== undefined ||
adhocColumn?.sqlExpression !== initialAdhocColumn?.sqlExpression;
const savedExpressionsLabel = t('Saved expressions');
const simpleColumnsLabel = t('Column');
const simpleColumnsLabel = t('Columns and metrics');
const keywords = useMemo(
() => sqlKeywords.concat(getColumnKeywords(columns)),
[columns],
@@ -313,95 +399,103 @@ const ColumnSelectPopover = ({
width: ${width}px;
`}
items={[
{
key: TABS_KEYS.SAVED,
label: t('Saved'),
disabled: disabledTabs.has('saved'),
children: (
<>
{calculatedColumns.length > 0 ? (
<FormItem label={savedExpressionsLabel}>
<StyledSelect
ariaLabel={savedExpressionsLabel}
value={selectedCalculatedColumn?.column_name}
onChange={onCalculatedColumnChange}
allowClear
autoFocus={!selectedCalculatedColumn}
placeholder={t('%s column(s)', calculatedColumns.length)}
options={calculatedColumns.map(calculatedColumn => ({
value: calculatedColumn.column_name,
label: (
<StyledColumnOption
column={calculatedColumn}
showType
// Only show Saved tab if not disabled
...(disabledTabs.has('saved')
? []
: [
{
key: TABS_KEYS.SAVED,
label: t('Saved'),
children: (
<>
{calculatedColumns.length > 0 ? (
<FormItem label={savedExpressionsLabel}>
<StyledSelect
ariaLabel={savedExpressionsLabel}
value={selectedCalculatedColumn?.column_name}
onChange={onCalculatedColumnChange}
allowClear
autoFocus={!selectedCalculatedColumn}
placeholder={t(
'%s column(s)',
calculatedColumns.length,
)}
options={calculatedColumns.map(
calculatedColumn => ({
value: calculatedColumn.column_name,
label: (
<StyledColumnOption
column={calculatedColumn}
showType
/>
),
key: calculatedColumn.column_name,
}),
)}
/>
),
key: calculatedColumn.column_name,
}))}
/>
</FormItem>
) : datasourceType === DatasourceType.Table ? (
<EmptyState
image="empty.svg"
size="small"
title={
isTemporal
? t('No temporal columns found')
: t('No saved expressions found')
}
description={
isTemporal
? t(
'Add calculated temporal columns to dataset in "Edit datasource" modal',
)
: t(
'Add calculated columns to dataset in "Edit datasource" modal',
)
}
/>
) : (
<EmptyState
image="empty.svg"
size="small"
title={
isTemporal
? t('No temporal columns found')
: t('No saved expressions found')
}
description={
isTemporal ? (
<>
<span
role="button"
tabIndex={0}
onClick={setDatasetAndClose}
>
{t('Create a dataset')}
</span>{' '}
{t(' to mark a column as a time column')}
</>
</FormItem>
) : datasourceType === DatasourceType.Table ? (
<EmptyState
image="empty.svg"
size="small"
title={
isTemporal
? t('No temporal columns found')
: t('No saved expressions found')
}
description={
isTemporal
? t(
'Add calculated temporal columns to dataset in "Edit datasource" modal',
)
: t(
'Add calculated columns to dataset in "Edit datasource" modal',
)
}
/>
) : (
<>
<span
role="button"
tabIndex={0}
onClick={setDatasetAndClose}
>
{t('Create a dataset')}
</span>{' '}
{t(' to add calculated columns')}
</>
)
}
/>
)}
</>
),
},
<EmptyState
image="empty.svg"
size="small"
title={
isTemporal
? t('No temporal columns found')
: t('No saved expressions found')
}
description={
isTemporal ? (
<>
<span
role="button"
tabIndex={0}
onClick={setDatasetAndClose}
>
{t('Create a dataset')}
</span>{' '}
{t(' to mark a column as a time column')}
</>
) : (
<>
<span
role="button"
tabIndex={0}
onClick={setDatasetAndClose}
>
{t('Create a dataset')}
</span>{' '}
{t(' to add calculated columns')}
</>
)
}
/>
)}
</>
),
},
]),
{
key: TABS_KEYS.SIMPLE,
label: t('Simple'),
disabled: disabledTabs.has('simple'),
children: (
<>
{isTemporal && simpleColumns.length === 0 ? (
@@ -432,18 +526,41 @@ const ColumnSelectPopover = ({
<FormItem label={simpleColumnsLabel}>
<Select
ariaLabel={simpleColumnsLabel}
value={selectedSimpleColumn?.column_name}
onChange={onSimpleColumnChange}
value={
selectedSimpleColumn?.column_name ||
selectedMetric?.metric_name
}
onChange={onSimpleItemChange}
allowClear
autoFocus={!selectedSimpleColumn}
placeholder={t('%s column(s)', simpleColumns.length)}
options={simpleColumns.map(simpleColumn => ({
value: simpleColumn.column_name,
label: (
<StyledColumnOption column={simpleColumn} showType />
),
key: simpleColumn.column_name,
}))}
autoFocus={!selectedSimpleColumn && !selectedMetric}
placeholder={t(
'%s item(s)',
simpleColumns.length + availableMetrics.length,
)}
options={[
...simpleColumns.map(simpleColumn => ({
value: simpleColumn.column_name,
label: (
<StyledColumnOption
column={simpleColumn}
showType
/>
),
key: `column-${simpleColumn.column_name}`,
})),
...availableMetrics.map(metric => ({
value: metric.metric_name,
label: (
<MetricOptionContainer>
<MetricIcon>ƒ</MetricIcon>
<MetricLabel>
{metric.verbose_name || metric.metric_name}
</MetricLabel>
</MetricOptionContainer>
),
key: `metric-${metric.metric_name}`,
})),
]}
/>
</FormItem>
)}