/** * 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 { useMemo, useState, useEffect, useRef, RefObject } from 'react'; import { t } from '@apache-superset/core'; import { getTimeFormatter, safeHtmlSpan, TimeFormats } from '@superset-ui/core'; import { css, styled, useTheme } from '@apache-superset/core/ui'; import { GenericDataType } from '@apache-superset/core/api/core'; import { Column } from 'react-table'; import { debounce } from 'lodash'; import { Constants, Button, Icons, Input, Popover, Radio, } from '@superset-ui/core/components'; import { CopyToClipboard } from 'src/components'; import { prepareCopyToClipboardTabularData } from 'src/utils/common'; import { getTimeColumns, setTimeColumns } from './utils'; export const CellNull = styled('span')` color: ${({ theme }) => theme.colorTextTertiary}; `; export const CopyButton = styled(Button)` font-size: ${({ theme }) => theme.fontSizeSM}px; // needed to override button's first-of-type margin: 0 && { margin: 0 ${({ theme }) => theme.sizeUnit * 2}px; } i { padding: 0 ${({ theme }) => theme.sizeUnit}px; } `; export const CopyToClipboardButton = ({ data, columns, }: { data?: Record; columns?: string[]; }) => ( * { line-height: 0; } `} /> } /> ); export const FilterInput = ({ onChangeHandler, shouldFocus = false, }: { onChangeHandler(filterText: string): void; shouldFocus?: boolean; }) => { const inputRef: RefObject = useRef(null); useEffect(() => { // Focus the input element when the component mounts if (inputRef.current && shouldFocus) { inputRef.current.focus(); } }, []); const theme = useTheme(); const debouncedChangeHandler = debounce( onChangeHandler, Constants.SLOW_DEBOUNCE, ); return ( } placeholder={t('Search')} onChange={(event: any) => { const filterText = event.target.value; debouncedChangeHandler(filterText); }} css={css` width: 200px; margin-right: ${theme.sizeUnit * 2}px; `} ref={inputRef} /> ); }; enum FormatPickerValue { Formatted = 'formatted', Original = 'original', } const FormatPicker = ({ onChange, value, }: { onChange: any; value: FormatPickerValue; }) => ( ); const FormatPickerContainer = styled.div` display: flex; flex-direction: column; padding: ${({ theme }) => `${theme.sizeUnit * 4}px`}; `; const FormatPickerLabel = styled.span` font-size: ${({ theme }) => theme.fontSizeSM}px; color: ${({ theme }) => theme.colorText}; margin-bottom: ${({ theme }) => theme.sizeUnit * 2}px; `; const DataTableTemporalHeaderCell = ({ columnName, onTimeColumnChange, datasourceId, isOriginalTimeColumn, displayLabel, }: { columnName: string; onTimeColumnChange: ( columnName: string, columnType: FormatPickerValue, ) => void; datasourceId?: string; isOriginalTimeColumn: boolean; displayLabel?: string; }) => { const theme = useTheme(); const onChange = (e: any) => { onTimeColumnChange(columnName, e.target.value); }; const overlayContent = useMemo( () => datasourceId ? ( // eslint-disable-next-line jsx-a11y/no-static-element-interactions ) => e.stopPropagation()} > {/* hack to disable click propagation from popover content to table header, which triggers sorting column */} {t('Column Formatting')} ) : null, [datasourceId, isOriginalTimeColumn], ); return datasourceId ? ( ) => e.stopPropagation()} /> {displayLabel ?? columnName} ) : ( {displayLabel ?? columnName} ); }; export const useFilteredTableData = ( filterText: string, data?: Record[], ) => { const rowsAsStrings = useMemo( () => data?.map((row: Record) => Object.values(row).map(value => value ? value.toString().toLowerCase() : t('N/A'), ), ) ?? [], [data], ); return useMemo(() => { if (!data?.length) { return []; } return data.filter((_, index: number) => rowsAsStrings[index].some(value => value?.includes(filterText.toLowerCase()), ), ); }, [data, filterText, rowsAsStrings]); }; const timeFormatter = getTimeFormatter(TimeFormats.DATABASE_DATETIME); export const useTableColumns = ( colnames?: string[], coltypes?: GenericDataType[], data?: Record[], datasourceId?: string, isVisible?: boolean, moreConfigs?: { [key: string]: Partial }, allowHTML?: boolean, columnDisplayNames?: Record, ) => { const [originalFormattedTimeColumns, setOriginalFormattedTimeColumns] = useState(getTimeColumns(datasourceId)); const onTimeColumnChange = ( columnName: string, columnType: FormatPickerValue, ) => { if (!datasourceId) { return; } if ( columnType === FormatPickerValue.Original && !originalFormattedTimeColumns.includes(columnName) ) { const cols = getTimeColumns(datasourceId); cols.push(columnName); setTimeColumns(datasourceId, cols); setOriginalFormattedTimeColumns(cols); } else if ( columnType === FormatPickerValue.Formatted && originalFormattedTimeColumns.includes(columnName) ) { const cols = getTimeColumns(datasourceId); cols.splice(cols.indexOf(columnName), 1); setTimeColumns(datasourceId, cols); setOriginalFormattedTimeColumns(cols); } }; useEffect(() => { if (isVisible) { setOriginalFormattedTimeColumns(getTimeColumns(datasourceId)); } }, [datasourceId, isVisible]); return useMemo( () => colnames && data?.length ? colnames .filter((column: string) => Object.keys(data[0]).includes(column)) .map((key, index) => { const colType = coltypes?.[index]; const firstValue = data[0][key]; const headerLabel = columnDisplayNames?.[key] ?? key; const originalFormattedTimeColumnIndex = colType === GenericDataType.Temporal ? originalFormattedTimeColumns.indexOf(key) : -1; const isOriginalTimeColumn = originalFormattedTimeColumns.includes(key); return { // react-table requires a non-empty id, therefore we introduce a fallback value in case the key is empty id: key || index, accessor: (row: Record) => row[key], Header: colType === GenericDataType.Temporal && typeof firstValue !== 'string' ? ( ) : ( headerLabel ), Cell: ({ value }) => { if (value === true) { return Constants.BOOL_TRUE_DISPLAY; } if (value === false) { return Constants.BOOL_FALSE_DISPLAY; } if (value === null) { return {Constants.NULL_DISPLAY}; } if ( colType === GenericDataType.Temporal && originalFormattedTimeColumnIndex === -1 && typeof value === 'number' ) { return timeFormatter(value); } if (typeof value === 'string' && allowHTML) { return safeHtmlSpan(value); } return String(value); }, ...moreConfigs?.[key], } as Column; }) : [], [ colnames, data, coltypes, datasourceId, moreConfigs, originalFormattedTimeColumns, columnDisplayNames, ], ); };