/** * 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 { useContext, useMemo, useState } from 'react'; import { css, DatasourceType, Metric, QueryFormData, styled, t, } from '@superset-ui/core'; import { ControlConfig } from '@superset-ui/chart-controls'; import AutoSizer from 'react-virtualized-auto-sizer'; import { FixedSizeList as List } from 'react-window'; import { matchSorter, rankings } from 'match-sorter'; import Alert from 'src/components/Alert'; import { SaveDatasetModal } from 'src/SqlLab/components/SaveDatasetModal'; import { getDatasourceAsSaveableDataset } from 'src/utils/datasourceUtils'; import { Input } from 'src/components/Input'; import { FAST_DEBOUNCE } from 'src/constants'; import { ExploreActions } from 'src/explore/actions/exploreActions'; import Control from 'src/explore/components/Control'; import { useDebounceValue } from 'src/hooks/useDebounceValue'; import DatasourcePanelItem, { ITEM_HEIGHT, DataSourcePanelColumn, DEFAULT_MAX_COLUMNS_LENGTH, DEFAULT_MAX_METRICS_LENGTH, } from './DatasourcePanelItem'; import { DndItemType } from '../DndItemType'; import { DndItemValue } from './types'; import { DropzoneContext } from '../ExploreContainer'; interface DatasourceControl extends Omit { datasource?: IDatasource; } export interface IDatasource { metrics: Metric[]; columns: DataSourcePanelColumn[]; id: number; type: DatasourceType; database: { id: number; }; sql?: string | null; datasource_name?: string | null; name?: string | null; schema?: string | null; } export interface Props { datasource: IDatasource; controls: { datasource: DatasourceControl; }; actions: Partial & Pick; // we use this props control force update when this panel resize width: number; formData?: QueryFormData; } const DatasourceContainer = styled.div` ${({ theme }) => css` background-color: ${theme.colors.grayscale.light5}; position: relative; height: 100%; display: flex; flex-direction: column; max-height: 100%; .ant-collapse { height: auto; } .field-selections { padding: 0 0 ${theme.gridUnit}px; overflow: auto; height: 100%; } .field-length { margin-bottom: ${theme.gridUnit * 2}px; font-size: ${theme.typography.sizes.s}px; color: ${theme.colors.grayscale.light1}; } .form-control.input-md { display: inline-flex; width: calc(100% - ${theme.gridUnit * 8}px); height: ${theme.gridUnit * 8}px; margin: ${theme.gridUnit * 2}px auto; } .type-label { font-size: ${theme.typography.sizes.s}px; color: ${theme.colors.grayscale.base}; } .Control { padding-bottom: 0; } `}; `; const StyledInfoboxWrapper = styled.div` ${({ theme }) => css` margin: 0 ${theme.gridUnit * 2.5}px; span { text-decoration: underline; } `} `; const BORDER_WIDTH = 2; const sortCertifiedFirst = (slice: DataSourcePanelColumn[]) => slice.sort((a, b) => (b?.is_certified ?? 0) - (a?.is_certified ?? 0)); export default function DataSourcePanel({ datasource, formData, controls: { datasource: datasourceControl }, actions, width, }: Props) { const [dropzones] = useContext(DropzoneContext); const { columns: _columns, metrics } = datasource; const allowedColumns = useMemo(() => { const validators = Object.values(dropzones); if (!Array.isArray(_columns)) return []; return _columns.filter(column => validators.some(validator => validator({ value: column as DndItemValue, type: DndItemType.Column, }), ), ); }, [dropzones, _columns]); // display temporal column first const columns = useMemo( () => [...allowedColumns].sort((col1, col2) => { if (col1?.is_dttm && !col2?.is_dttm) { return -1; } if (col2?.is_dttm && !col1?.is_dttm) { return 1; } return 0; }), [allowedColumns], ); const allowedMetrics = useMemo(() => { const validators = Object.values(dropzones); return metrics.filter(metric => validators.some(validator => validator({ value: metric, type: DndItemType.Metric }), ), ); }, [dropzones, metrics]); const hiddenColumnCount = _columns.length - allowedColumns.length; const hiddenMetricCount = metrics.length - allowedMetrics.length; const [showSaveDatasetModal, setShowSaveDatasetModal] = useState(false); const [inputValue, setInputValue] = useState(''); const [showAllMetrics, setShowAllMetrics] = useState(false); const [showAllColumns, setShowAllColumns] = useState(false); const [collapseMetrics, setCollapseMetrics] = useState(false); const [collapseColumns, setCollapseColumns] = useState(false); const searchKeyword = useDebounceValue(inputValue, FAST_DEBOUNCE); const filteredColumns = useMemo(() => { if (!searchKeyword) { return columns ?? []; } return matchSorter(columns, searchKeyword, { keys: [ { key: 'verbose_name', threshold: rankings.CONTAINS, }, { key: 'column_name', threshold: rankings.CONTAINS, }, { key: item => [item?.description ?? '', item?.expression ?? ''].map( x => x?.replace(/[_\n\s]+/g, ' ') || '', ), threshold: rankings.CONTAINS, maxRanking: rankings.CONTAINS, }, ], keepDiacritics: true, }); }, [columns, searchKeyword]); const filteredMetrics = useMemo(() => { if (!searchKeyword) { return allowedMetrics ?? []; } return matchSorter(allowedMetrics, searchKeyword, { keys: [ { key: 'verbose_name', threshold: rankings.CONTAINS, }, { key: 'metric_name', threshold: rankings.CONTAINS, }, { key: item => [item?.description ?? '', item?.expression ?? ''].map( x => x?.replace(/[_\n\s]+/g, ' ') || '', ), threshold: rankings.CONTAINS, maxRanking: rankings.CONTAINS, }, ], keepDiacritics: true, baseSort: (a, b) => Number(b?.item?.is_certified ?? 0) - Number(a?.item?.is_certified ?? 0) || String(a?.rankedValue ?? '').localeCompare(b?.rankedValue ?? ''), }); }, [allowedMetrics, searchKeyword]); const metricSlice = useMemo( () => showAllMetrics ? filteredMetrics : filteredMetrics?.slice?.(0, DEFAULT_MAX_METRICS_LENGTH), [filteredMetrics, showAllMetrics], ); const columnSlice = useMemo( () => showAllColumns ? sortCertifiedFirst(filteredColumns) : sortCertifiedFirst( filteredColumns?.slice?.(0, DEFAULT_MAX_COLUMNS_LENGTH), ), [filteredColumns, showAllColumns], ); const showInfoboxCheck = () => { try { if (sessionStorage.getItem('showInfobox') === 'false') return false; } catch (error) { // continue regardless of error } return true; }; const saveableDatasets = { query: DatasourceType.Query, saved_query: DatasourceType.SavedQuery, }; const datasourceIsSaveable = datasource.type && saveableDatasets[datasource.type]; const mainBody = useMemo( () => ( <> { setInputValue(evt.target.value); }} value={inputValue} className="form-control input-md" placeholder={t('Search Metrics & Columns')} />
{datasourceIsSaveable && showInfoboxCheck() && ( { try { sessionStorage.setItem('showInfobox', 'false'); } catch (error) { // continue regardless of error } }} type="info" message="" description={ <> setShowSaveDatasetModal(true)} className="add-dataset-alert-description" > {t('Create a dataset')} {t(' to edit or add columns and metrics.')} } /> )} {({ height }: { height: number }) => ( {DatasourcePanelItem} )}
), // eslint-disable-next-line react-hooks/exhaustive-deps [ columnSlice, inputValue, filteredColumns.length, filteredMetrics.length, metricSlice, showAllColumns, showAllMetrics, collapseMetrics, collapseColumns, datasourceIsSaveable, width, ], ); return ( {datasourceIsSaveable && showSaveDatasetModal && ( setShowSaveDatasetModal(false)} buttonTextOnSave={t('Save')} buttonTextOnOverwrite={t('Overwrite')} datasource={getDatasourceAsSaveableDataset(datasource)} openWindow={false} formData={formData} /> )} {/* @ts-ignore */} {datasource.id != null && mainBody} ); }