diff --git a/superset-frontend/plugins/plugin-chart-table/src/DataTable/DataTable.tsx b/superset-frontend/plugins/plugin-chart-table/src/DataTable/DataTable.tsx index 1e5f07686db5..e945ee277f24 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/DataTable/DataTable.tsx +++ b/superset-frontend/plugins/plugin-chart-table/src/DataTable/DataTable.tsx @@ -27,7 +27,7 @@ import { DragEvent, useEffect, } from 'react'; -import { styled, typedMemo, usePrevious } from '@superset-ui/core'; +import { typedMemo, usePrevious } from '@superset-ui/core'; import { useTable, usePagination, @@ -42,7 +42,7 @@ import { } from 'react-table'; import { matchSorter, rankings } from 'match-sorter'; import { isEqual } from 'lodash'; -import { Space } from '@superset-ui/core/components'; +import { Flex, Space } from '@superset-ui/core/components'; import GlobalFilter, { GlobalFilterProps } from './components/GlobalFilter'; import SelectPageSize, { SelectPageSizeProps, @@ -77,7 +77,7 @@ export interface DataTableProps extends TableOptions { sticky?: boolean; rowCount: number; wrapperRef?: MutableRefObject; - onColumnOrderChange: () => void; + onColumnOrderChange?: () => void; renderGroupingHeaders?: () => JSX.Element; renderTimeComparisonDropdown?: () => JSX.Element; handleSortByChange: (sortBy: SortByItem[]) => void; @@ -98,24 +98,6 @@ const sortTypes = { alphanumeric: sortAlphanumericCaseInsensitive, }; -const StyledSpace = styled(Space)` - display: flex; - justify-content: flex-end; - - .search-select-container { - display: flex; - } - - .search-by-label { - align-self: center; - margin-right: 4px; - } -`; - -const StyledRow = styled.div` - display: flex; -`; - // Be sure to pass our updateMyData and the skipReset option export default typedMemo(function DataTable({ tableClassName, @@ -336,8 +318,7 @@ export default typedMemo(function DataTable({ const colToBeMoved = currentCols.splice(columnBeingDragged, 1); currentCols.splice(newPosition, 0, colToBeMoved[0]); setColumnOrder(currentCols); - // toggle value in TableChart to trigger column width recalc - onColumnOrderChange(); + onColumnOrderChange?.(); } e.preventDefault(); }; @@ -450,30 +431,36 @@ export default typedMemo(function DataTable({ > {hasGlobalControl ? (
- - - {hasPagination ? ( - - ) : null} + + {hasPagination ? ( + + ) : null} + {serverPagination && ( -
- Search by: + + Search by: -
+ )} {searchInput && ( @@ -493,8 +480,8 @@ export default typedMemo(function DataTable({ {renderTimeComparisonDropdown ? renderTimeComparisonDropdown() : null} -
-
+ +
) : null} {wrapStickyTable ? wrapStickyTable(renderTable) : renderTable()} diff --git a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx index 6fe50ffc3377..578ea00a0ea0 100644 --- a/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx +++ b/superset-frontend/plugins/plugin-chart-table/src/TableChart.tsx @@ -195,6 +195,21 @@ function SortIcon({ column }: { column: ColumnInstance }) { return sortIcon; } +/** + * Label that is visually hidden but accessible + */ +const VisuallyHidden = styled.label` + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +`; + function SearchInput({ count, value, @@ -225,10 +240,10 @@ function SelectPageSize({ const { Option } = Select; return ( - <> - + {t('Show')}{' '} id="pageSizeSelect" @@ -252,7 +267,7 @@ function SelectPageSize({ })} {' '} {t('entries per page')} - + ); } @@ -296,12 +311,17 @@ export default function TableChart( serverPageLength, slice_id, } = props; - const comparisonColumns = [ - { key: 'all', label: t('Display all') }, - { key: '#', label: '#' }, - { key: '△', label: '△' }, - { key: '%', label: '%' }, - ]; + + const comparisonColumns = useMemo( + () => [ + { key: 'all', label: t('Display all') }, + { key: '#', label: '#' }, + { key: '△', label: '△' }, + { key: '%', label: '%' }, + ], + [], + ); + const timestampFormatter = useCallback( value => getTimeFormatterForGranularity(timeGrain)(value), [timeGrain], @@ -353,71 +373,74 @@ export default function TableChart( [filters], ); - const getCrossFilterDataMask = (key: string, value: DataRecordValue) => { - let updatedFilters = { ...(filters || {}) }; - if (filters && isActiveFilterValue(key, value)) { - updatedFilters = {}; - } else { - updatedFilters = { - [key]: [value], - }; - } - if ( - Array.isArray(updatedFilters[key]) && - updatedFilters[key].length === 0 - ) { - delete updatedFilters[key]; - } - - const groupBy = Object.keys(updatedFilters); - const groupByValues = Object.values(updatedFilters); - const labelElements: string[] = []; - groupBy.forEach(col => { - const isTimestamp = col === DTTM_ALIAS; - const filterValues = ensureIsArray(updatedFilters?.[col]); - if (filterValues.length) { - const valueLabels = filterValues.map(value => - isTimestamp ? timestampFormatter(value) : value, - ); - labelElements.push(`${valueLabels.join(', ')}`); + const getCrossFilterDataMask = useCallback( + (key: string, value: DataRecordValue) => { + let updatedFilters = { ...(filters || {}) }; + if (filters && isActiveFilterValue(key, value)) { + updatedFilters = {}; + } else { + updatedFilters = { + [key]: [value], + }; + } + if ( + Array.isArray(updatedFilters[key]) && + updatedFilters[key].length === 0 + ) { + delete updatedFilters[key]; } - }); - return { - dataMask: { - extraFormData: { - filters: - groupBy.length === 0 - ? [] - : groupBy.map(col => { - const val = ensureIsArray(updatedFilters?.[col]); - if (!val.length) + const groupBy = Object.keys(updatedFilters); + const groupByValues = Object.values(updatedFilters); + const labelElements: string[] = []; + groupBy.forEach(col => { + const isTimestamp = col === DTTM_ALIAS; + const filterValues = ensureIsArray(updatedFilters?.[col]); + if (filterValues.length) { + const valueLabels = filterValues.map(value => + isTimestamp ? timestampFormatter(value) : value, + ); + labelElements.push(`${valueLabels.join(', ')}`); + } + }); + + return { + dataMask: { + extraFormData: { + filters: + groupBy.length === 0 + ? [] + : groupBy.map(col => { + const val = ensureIsArray(updatedFilters?.[col]); + if (!val.length) + return { + col, + op: 'IS NULL' as const, + }; return { col, - op: 'IS NULL' as const, + op: 'IN' as const, + val: val.map(el => + el instanceof Date ? el.getTime() : el!, + ), + grain: col === DTTM_ALIAS ? timeGrain : undefined, }; - return { - col, - op: 'IN' as const, - val: val.map(el => - el instanceof Date ? el.getTime() : el!, - ), - grain: col === DTTM_ALIAS ? timeGrain : undefined, - }; - }), - }, - filterState: { - label: labelElements.join(', '), - value: groupByValues.length ? groupByValues : null, - filters: - updatedFilters && Object.keys(updatedFilters).length - ? updatedFilters - : null, + }), + }, + filterState: { + label: labelElements.join(', '), + value: groupByValues.length ? groupByValues : null, + filters: + updatedFilters && Object.keys(updatedFilters).length + ? updatedFilters + : null, + }, }, - }, - isCurrentValueSelected: isActiveFilterValue(key, value), - }; - }; + isCurrentValueSelected: isActiveFilterValue(key, value), + }; + }, + [filters, isActiveFilterValue, timestampFormatter, timeGrain], + ); const toggleFilter = useCallback( function toggleFilter(key: string, val: DataRecordValue) { @@ -429,17 +452,21 @@ export default function TableChart( [emitCrossFilters, getCrossFilterDataMask, setDataMask], ); - const getSharedStyle = (column: DataColumnMeta): CSSProperties => { - const { isNumeric, config = {} } = column; - const textAlign = - config.horizontalAlign || - (isNumeric && !isUsingTimeComparison ? 'right' : 'left'); - return { - textAlign, - }; - }; + const getSharedStyle = useCallback( + (column: DataColumnMeta): CSSProperties => { + const { isNumeric, config = {} } = column; + const textAlign = + config.horizontalAlign || + (isNumeric && !isUsingTimeComparison ? 'right' : 'left'); + return { + textAlign, + }; + }, + [isUsingTimeComparison], + ); + + const comparisonLabels = useMemo(() => [t('Main'), '#', '△', '%'], []); - const comparisonLabels = [t('Main'), '#', '△', '%']; const filteredColumnsMeta = useMemo(() => { if (!isUsingTimeComparison) { return columnsMeta; @@ -471,79 +498,86 @@ export default function TableChart( selectedComparisonColumns, ]); - const handleContextMenu = - onContextMenu && !isRawRecords - ? ( - value: D, - cellPoint: { - key: string; - value: DataRecordValue; - isMetric?: boolean; - }, - clientX: number, - clientY: number, - ) => { - const drillToDetailFilters: BinaryQueryObjectFilterClause[] = []; - filteredColumnsMeta.forEach(col => { - if (!col.isMetric) { - const dataRecordValue = value[col.key]; - drillToDetailFilters.push({ - col: col.key, - op: '==', - val: dataRecordValue as string | number | boolean, - formattedVal: formatColumnValue(col, dataRecordValue)[1], - }); - } - }); - onContextMenu(clientX, clientY, { - drillToDetail: drillToDetailFilters, - crossFilter: cellPoint.isMetric - ? undefined - : getCrossFilterDataMask(cellPoint.key, cellPoint.value), - drillBy: cellPoint.isMetric - ? undefined - : { - filters: [ - { - col: cellPoint.key, - op: '==', - val: cellPoint.value as string | number | boolean, - }, - ], - groupbyFieldName: 'groupby', - }, - }); - } - : undefined; + const handleContextMenu = useMemo(() => { + if (onContextMenu && !isRawRecords) { + return ( + value: D, + cellPoint: { + key: string; + value: DataRecordValue; + isMetric?: boolean; + }, + clientX: number, + clientY: number, + ) => { + const drillToDetailFilters: BinaryQueryObjectFilterClause[] = []; + filteredColumnsMeta.forEach(col => { + if (!col.isMetric) { + const dataRecordValue = value[col.key]; + drillToDetailFilters.push({ + col: col.key, + op: '==', + val: dataRecordValue as string | number | boolean, + formattedVal: formatColumnValue(col, dataRecordValue)[1], + }); + } + }); + onContextMenu(clientX, clientY, { + drillToDetail: drillToDetailFilters, + crossFilter: cellPoint.isMetric + ? undefined + : getCrossFilterDataMask(cellPoint.key, cellPoint.value), + drillBy: cellPoint.isMetric + ? undefined + : { + filters: [ + { + col: cellPoint.key, + op: '==', + val: cellPoint.value as string | number | boolean, + }, + ], + groupbyFieldName: 'groupby', + }, + }); + }; + } + return undefined; + }, [ + onContextMenu, + isRawRecords, + filteredColumnsMeta, + getCrossFilterDataMask, + ]); - const getHeaderColumns = ( - columnsMeta: DataColumnMeta[], - enableTimeComparison?: boolean, - ) => { - const resultMap: Record = {}; + const getHeaderColumns = useCallback( + (columnsMeta: DataColumnMeta[], enableTimeComparison?: boolean) => { + const resultMap: Record = {}; - if (!enableTimeComparison) { - return resultMap; - } + if (!enableTimeComparison) { + return resultMap; + } - columnsMeta.forEach((element, index) => { - // Check if element's label is one of the comparison labels - if (comparisonLabels.includes(element.label)) { - // Extract the key portion after the space, assuming the format is always "label key" - const keyPortion = element.key.substring(element.label.length); - - // If the key portion is not in the map, initialize it with the current index - if (!resultMap[keyPortion]) { - resultMap[keyPortion] = [index]; - } else { - // Add the index to the existing array - resultMap[keyPortion].push(index); + columnsMeta.forEach((element, index) => { + // Check if element's label is one of the comparison labels + if (comparisonLabels.includes(element.label)) { + // Extract the key portion after the space, assuming the format is always "label key" + const keyPortion = element.key.substring(element.label.length); + + // If the key portion is not in the map, initialize it with the current index + if (!resultMap[keyPortion]) { + resultMap[keyPortion] = [index]; + } else { + // Add the index to the existing array + resultMap[keyPortion].push(index); + } } - } - }); + }); - return resultMap; - }; + return resultMap; + }, + [comparisonLabels], + ); const renderTimeComparisonDropdown = (): JSX.Element => { const allKey = comparisonColumns[0].key; @@ -638,6 +672,11 @@ export default function TableChart( ); }; + const groupHeaderColumns = useMemo( + () => getHeaderColumns(filteredColumnsMeta, isUsingTimeComparison), + [filteredColumnsMeta, getHeaderColumns, isUsingTimeComparison], + ); + const renderGroupingHeaders = (): JSX.Element => { // TODO: Make use of ColumnGroup to render the aditional headers const headers: any = []; @@ -719,11 +758,6 @@ export default function TableChart( ); }; - const groupHeaderColumns = useMemo( - () => getHeaderColumns(filteredColumnsMeta, isUsingTimeComparison), - [filteredColumnsMeta, isUsingTimeComparison], - ); - const getColumnConfigs = useCallback( ( column: DataColumnMeta, @@ -1086,19 +1120,27 @@ export default function TableChart( }; }, [ + getSharedStyle, defaultAlignPN, defaultColorPN, - emitCrossFilters, - getValueRange, - isActiveFilterValue, - isRawRecords, + columnColorFormatters, + isUsingTimeComparison, + basicColorFormatters, showCellBars, - sortDesc, - toggleFilter, + isRawRecords, + getValueRange, + emitCrossFilters, + comparisonLabels, totals, - columnColorFormatters, - columnOrderToggle, theme, + sortDesc, + groupHeaderColumns, + allowRenderHtml, + basicColorColumnFormatters, + isActiveFilterValue, + toggleFilter, + handleContextMenu, + allowRearrangeColumns, ], ); @@ -1131,7 +1173,7 @@ export default function TableChart( if (!isEqual(options, searchOptions)) { setSearchOptions(options || []); } - }, [columns]); + }, [columns, searchOptions]); const handleServerPaginationChange = useCallback( (pageNumber: number, pageSize: number) => { @@ -1142,7 +1184,7 @@ export default function TableChart( }; updateTableOwnState(setDataMask, modifiedOwnState); }, - [setDataMask], + [serverPaginationData, setDataMask], ); useEffect(() => { @@ -1154,7 +1196,12 @@ export default function TableChart( }; updateTableOwnState(setDataMask, modifiedOwnState); } - }, []); + }, [ + hasServerPageLengthChanged, + serverPageLength, + serverPaginationData, + setDataMask, + ]); const handleSizeChange = useCallback( ({ width, height }: { width: number; height: number }) => { @@ -1200,7 +1247,7 @@ export default function TableChart( }; updateTableOwnState(setDataMask, modifiedOwnState); }, - [setDataMask, serverPagination], + [serverPagination, serverPaginationData, setDataMask], ); const handleSearch = (searchText: string) => {