/** * 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 { css, GenericDataType, getTimeFormatter, safeHtmlSpan, styled, t, TimeFormats, useTheme, } from '@superset-ui/core'; import { Global } from '@emotion/react'; import { Column } from 'react-table'; import { debounce } from 'lodash'; import { Space } from 'src/components'; import { Input } from 'src/components/Input'; import { BOOL_FALSE_DISPLAY, BOOL_TRUE_DISPLAY, NULL_DISPLAY, SLOW_DEBOUNCE, } from 'src/constants'; import { Radio } from 'src/components/Radio'; import Icons from 'src/components/Icons'; import Button from 'src/components/Button'; import Popover from 'src/components/Popover'; import { prepareCopyToClipboardTabularData } from 'src/utils/common'; import CopyToClipboard from 'src/components/CopyToClipboard'; import { getTimeColumns, setTimeColumns } from './utils'; export const CellNull = styled('span')` color: ${({ theme }) => theme.colors.grayscale.light1}; `; export const CopyButton = styled(Button)` font-size: ${({ theme }) => theme.typography.sizes.s}px; // needed to override button's first-of-type margin: 0 && { margin: 0 ${({ theme }) => theme.gridUnit * 2}px; } i { padding: 0 ${({ theme }) => theme.gridUnit}px; } `; export const CopyToClipboardButton = ({ data, columns, }: { data?: Record; columns?: string[]; }) => { const theme = useTheme(); return ( * { 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, SLOW_DEBOUNCE); return ( } placeholder={t('Search')} onChange={(event: any) => { const filterText = event.target.value; debouncedChangeHandler(filterText); }} css={css` width: 200px; margin-right: ${theme.gridUnit * 2}px; `} ref={inputRef} /> ); }; enum FormatPickerValue { Formatted = 'formatted', Original = 'original', } const FormatPicker = ({ onChange, value, }: { onChange: any; value: FormatPickerValue; }) => ( {t('Formatted date')} {t('Original value')} ); const FormatPickerContainer = styled.div` display: flex; flex-direction: column; padding: ${({ theme }) => `${theme.gridUnit * 4}px`}; `; const FormatPickerLabel = styled.span` font-size: ${({ theme }) => theme.typography.sizes.s}px; color: ${({ theme }) => theme.colors.grayscale.base}; margin-bottom: ${({ theme }) => theme.gridUnit * 2}px; text-transform: uppercase; `; const DataTableTemporalHeaderCell = ({ columnName, onTimeColumnChange, datasourceId, isOriginalTimeColumn, }: { columnName: string; onTimeColumnChange: ( columnName: string, columnType: FormatPickerValue, ) => void; datasourceId?: string; isOriginalTimeColumn: boolean; }) => { 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()} /> {columnName} ) : ( {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, ) => { 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 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 => row[key], Header: colType === GenericDataType.Temporal && typeof firstValue !== 'string' ? ( ) : ( key ), Cell: ({ value }) => { if (value === true) { return BOOL_TRUE_DISPLAY; } if (value === false) { return BOOL_FALSE_DISPLAY; } if (value === null) { return {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, ], ); };