/** * 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. */ /* eslint-disable camelcase */ import { Dispatch, SetStateAction, useCallback, useEffect, useMemo, useRef, useState, } from 'react'; import { useSelector } from 'react-redux'; import { t } from '@apache-superset/core'; import { AdhocColumn, isAdhocColumn, DatasourceType, Metric, QueryFormMetric, } from '@superset-ui/core'; import { styled, css } from '@apache-superset/core/ui'; import { ColumnMeta, isSavedExpression } from '@superset-ui/chart-controls'; import Tabs from '@superset-ui/core/components/Tabs'; import { Button, Form, FormItem, Select, EmptyState, } from '@superset-ui/core/components'; import sqlKeywords from 'src/SqlLab/utils/sqlKeywords'; import { getColumnKeywords } from 'src/explore/controlUtils/getColumnKeywords'; import { StyledColumnOption } from 'src/explore/components/optionRenderers'; import SQLEditorWithValidation from 'src/components/SQLEditorWithValidation'; import { POPOVER_INITIAL_HEIGHT, POPOVER_INITIAL_WIDTH, } from 'src/explore/constants'; import { ExplorePageState } from 'src/explore/types'; import useResizeButton from './useResizeButton'; const TABS_KEYS = { SAVED: 'saved', SIMPLE: 'simple', SQL_EXPRESSION: 'sqlExpression', }; const StyledSelect = styled(Select)` .metric-option { & > svg { min-width: ${({ theme }) => `${theme.sizeUnit * 4}px`}; } & > .option-label { overflow: hidden; text-overflow: ellipsis; } } `; 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 | Metric) => void; onClose: () => void; hasCustomLabel: boolean; setLabel: (title: string) => void; getCurrentTab: (tab: string) => void; label: string; isTemporal?: boolean; setDatasetModal?: Dispatch>; disabledTabs?: Set; metrics?: Metric[]; selectedMetrics?: QueryFormMetric[]; datasource?: any; } const getInitialColumnValues = ( editedColumn?: ColumnMeta | AdhocColumn, ): [AdhocColumn?, ColumnMeta?, ColumnMeta?] => { if (!editedColumn) { return [undefined, undefined, undefined]; } if (isAdhocColumn(editedColumn)) { return [editedColumn, undefined, undefined]; } if (isSavedExpression(editedColumn)) { return [undefined, editedColumn, undefined]; } return [undefined, undefined, editedColumn]; }; const ColumnSelectPopover = ({ columns, editedColumn, getCurrentTab, hasCustomLabel, isTemporal, label, onChange, onClose, setDatasetModal, setLabel, disabledTabs = new Set<'saved' | 'simple' | 'sqlExpression'>(), metrics = [], selectedMetrics = [], datasource, }: ColumnSelectPopoverProps) => { // const theme = useTheme(); // Unused variable const datasourceType = useSelector( state => state.explore.datasource.type, ); const [initialLabel] = useState(label); const [initialAdhocColumn, initialCalculatedColumn, initialSimpleColumn] = getInitialColumnValues(editedColumn); const [adhocColumn, setAdhocColumn] = useState( initialAdhocColumn, ); const [selectedCalculatedColumn, setSelectedCalculatedColumn] = useState< ColumnMeta | undefined >(initialCalculatedColumn); const [selectedSimpleColumn, setSelectedSimpleColumn] = useState< ColumnMeta | undefined >(initialSimpleColumn); const [selectedMetric, setSelectedMetric] = useState( undefined, ); const [selectedTab, setSelectedTab] = useState(null); const [resizeButton, width, height] = useResizeButton( POPOVER_INITIAL_WIDTH, POPOVER_INITIAL_HEIGHT, ); const sqlEditorRef = useRef(null); const [calculatedColumns, simpleColumns] = useMemo( () => columns?.reduce( (acc: [ColumnMeta[], ColumnMeta[]], column: ColumnMeta) => { if (column.expression) { acc[0].push(column); } else { acc[1].push(column); } return acc; }, [[], []], ), [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], ); const onCalculatedColumnChange = useCallback( selectedColumnName => { const selectedColumn = calculatedColumns.find( col => col.column_name === selectedColumnName, ); setSelectedCalculatedColumn(selectedColumn); setSelectedSimpleColumn(undefined); setSelectedMetric(undefined); setAdhocColumn(undefined); setLabel( selectedColumn?.verbose_name || selectedColumn?.column_name || '', ); }, [calculatedColumns, setLabel], ); const onSimpleColumnChange = useCallback( selectedColumnName => { const selectedColumn = simpleColumns.find( col => col.column_name === selectedColumnName, ); setSelectedCalculatedColumn(undefined); setSelectedSimpleColumn(selectedColumn); setSelectedMetric(undefined); setAdhocColumn(undefined); setLabel( selectedColumn?.verbose_name || selectedColumn?.column_name || '', ); }, [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 ? 'saved' : 'simple'; useEffect(() => { getCurrentTab(defaultActiveTabKey); setSelectedTab(defaultActiveTabKey); }, [defaultActiveTabKey, getCurrentTab, setSelectedTab]); useEffect(() => { /* if the adhoc column is not set (because it was never edited) but the * tab is selected and the label has changed, then we need to set the * adhoc column manually */ if ( adhocColumn === undefined && selectedTab === 'sqlExpression' && hasCustomLabel ) { const sqlExpression = selectedSimpleColumn?.column_name || selectedCalculatedColumn?.expression || ''; setAdhocColumn({ label, sqlExpression, expressionType: 'SQL' }); } }, [ adhocColumn, defaultActiveTabKey, hasCustomLabel, getCurrentTab, label, selectedCalculatedColumn, selectedSimpleColumn, selectedTab, ]); const onSave = useCallback(() => { if (adhocColumn && adhocColumn.label !== label) { adhocColumn.label = label; } const selectedColumn = adhocColumn || selectedCalculatedColumn || selectedSimpleColumn; const selectedItem = selectedColumn || selectedMetric; if (!selectedItem) { return; } onChange(selectedItem); onClose(); }, [ adhocColumn, label, onChange, onClose, selectedCalculatedColumn, selectedSimpleColumn, selectedMetric, ]); const onResetStateAndClose = useCallback(() => { setSelectedCalculatedColumn(initialCalculatedColumn); setSelectedSimpleColumn(initialSimpleColumn); setSelectedMetric(undefined); setAdhocColumn(initialAdhocColumn); onClose(); }, [ initialAdhocColumn, initialCalculatedColumn, initialSimpleColumn, onClose, ]); const onTabChange = useCallback( tab => { getCurrentTab(tab); setSelectedTab(tab); // @ts-ignore sqlEditorRef.current?.editor.focus(); }, [getCurrentTab], ); const setDatasetAndClose = () => { if (setDatasetModal) { setDatasetModal(true); } onClose(); }; const stateIsValid = 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('Columns and metrics'); const keywords = useMemo( () => sqlKeywords.concat(getColumnKeywords(columns)), [columns], ); return (
{calculatedColumns.length > 0 ? ( ({ value: calculatedColumn.column_name, label: ( ), key: calculatedColumn.column_name, }), )} /> ) : datasourceType === DatasourceType.Table ? ( ) : ( {t('Create a dataset')} {' '} {t(' to mark a column as a time column')} ) : ( <> {t('Create a dataset')} {' '} {t(' to add calculated columns')} ) } /> )} ), }, ]), { key: TABS_KEYS.SIMPLE, label: t('Simple'), children: ( <> {isTemporal && simpleColumns.length === 0 ? ( {t('Create a dataset')} {' '} {t(' to mark a column as a time column')} ) } /> ) : (