/** * 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 { AdhocColumn, isAdhocColumn, t, styled, css, DatasourceType, } from '@superset-ui/core'; import { ColumnMeta, isSavedExpression } from '@superset-ui/chart-controls'; import Tabs from 'src/components/Tabs'; import Button from 'src/components/Button'; import { Select } from 'src/components'; import { Form, FormItem } from 'src/components/Form'; import sqlKeywords from 'src/SqlLab/utils/sqlKeywords'; import { SQLEditor } from 'src/components/AsyncAceEditor'; import { EmptyState } from 'src/components/EmptyState'; import { getColumnKeywords } from 'src/explore/controlUtils/getColumnKeywords'; import { StyledColumnOption } from 'src/explore/components/optionRenderers'; import { POPOVER_INITIAL_HEIGHT, POPOVER_INITIAL_WIDTH, } from 'src/explore/constants'; import { ExplorePageState } from 'src/explore/types'; import useResizeButton from './useResizeButton'; const StyledSelect = styled(Select)` .metric-option { & > svg { min-width: ${({ theme }) => `${theme.gridUnit * 4}px`}; } & > .option-label { overflow: hidden; text-overflow: ellipsis; } } `; export interface ColumnSelectPopoverProps { columns: ColumnMeta[]; editedColumn?: ColumnMeta | AdhocColumn; onChange: (column: ColumnMeta | AdhocColumn) => void; onClose: () => void; hasCustomLabel: boolean; setLabel: (title: string) => void; getCurrentTab: (tab: string) => void; label: string; isTemporal?: boolean; setDatasetModal?: Dispatch>; disabledTabs?: Set; } 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'>(), }: ColumnSelectPopoverProps) => { 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 [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], ); const onSqlExpressionChange = useCallback( sqlExpression => { setAdhocColumn({ label, sqlExpression, expressionType: 'SQL' }); setSelectedSimpleColumn(undefined); setSelectedCalculatedColumn(undefined); }, [label], ); const onCalculatedColumnChange = useCallback( selectedColumnName => { const selectedColumn = calculatedColumns.find( col => col.column_name === selectedColumnName, ); setSelectedCalculatedColumn(selectedColumn); setSelectedSimpleColumn(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); setAdhocColumn(undefined); setLabel( selectedColumn?.verbose_name || selectedColumn?.column_name || '', ); }, [setLabel, simpleColumns], ); 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; if (!selectedColumn) { return; } onChange(selectedColumn); onClose(); }, [ adhocColumn, label, onChange, onClose, selectedCalculatedColumn, selectedSimpleColumn, ]); const onResetStateAndClose = useCallback(() => { setSelectedCalculatedColumn(initialCalculatedColumn); setSelectedSimpleColumn(initialSimpleColumn); setAdhocColumn(initialAdhocColumn); onClose(); }, [ initialAdhocColumn, initialCalculatedColumn, initialSimpleColumn, onClose, ]); const onTabChange = useCallback( tab => { getCurrentTab(tab); setSelectedTab(tab); // @ts-ignore sqlEditorRef.current?.editor.focus(); }, [getCurrentTab], ); const onSqlEditorFocus = useCallback(() => { // @ts-ignore sqlEditorRef.current?.editor.resize(); }, []); const setDatasetAndClose = () => { if (setDatasetModal) { setDatasetModal(true); } onClose(); }; const stateIsValid = adhocColumn || selectedCalculatedColumn || selectedSimpleColumn; const hasUnsavedChanges = initialLabel !== label || selectedCalculatedColumn?.column_name !== initialCalculatedColumn?.column_name || selectedSimpleColumn?.column_name !== initialSimpleColumn?.column_name || adhocColumn?.sqlExpression !== initialAdhocColumn?.sqlExpression; const savedExpressionsLabel = t('Saved expressions'); const simpleColumnsLabel = t('Column'); const keywords = useMemo( () => sqlKeywords.concat(getColumnKeywords(columns)), [columns], ); return (
{calculatedColumns.length > 0 ? ( ({ value: calculatedColumn.column_name, label: calculatedColumn.verbose_name || calculatedColumn.column_name, customLabel: ( ), 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')} ) } /> )} {isTemporal && simpleColumns.length === 0 ? ( {t('Create a dataset')} {' '} {t(' to mark a column as a time column')} ) } /> ) : (