geido commented on code in PR #32964: URL: https://github.com/apache/superset/pull/32964#discussion_r2030871121
########## superset-frontend/src/components/TableCollection/index.tsx: ########## @@ -32,299 +33,131 @@ interface TableCollectionProps { loading: boolean; highlightRowId?: number; columnsForWrapText?: string[]; + setSortBy?: (updater: SortingRule<any>[]) => void; Review Comment: Do we have a better type than any? ########## superset-frontend/src/components/TableCollection/index.tsx: ########## @@ -32,299 +33,131 @@ interface TableCollectionProps { loading: boolean; highlightRowId?: number; columnsForWrapText?: string[]; + setSortBy?: (updater: SortingRule<any>[]) => void; + bulkSelectEnabled?: boolean; + selectedFlatRows?: any[]; Review Comment: Let's improve the type here ########## superset-frontend/src/pages/SavedQueryList/SavedQueryList.test.tsx: ########## @@ -120,6 +120,7 @@ describe('SavedQueryList', () => { await waitFor(() => { mockQueries.forEach(query => { expect(screen.getByText(query.label)).toBeInTheDocument(); + screen.debug(); Review Comment: ```suggestion ``` ########## superset-frontend/src/components/TableCollection/index.tsx: ########## @@ -32,299 +33,131 @@ interface TableCollectionProps { loading: boolean; highlightRowId?: number; columnsForWrapText?: string[]; + setSortBy?: (updater: SortingRule<any>[]) => void; + bulkSelectEnabled?: boolean; + selectedFlatRows?: any[]; + toggleRowSelected?: (rowId: string, value: boolean) => void; + toggleAllRowsSelected?: (value?: boolean) => void; + sticky?: boolean; } -export const Table = styled.table` +const StyledTable = styled(Table)` ${({ theme }) => ` - background-color: ${theme.colorBgContainer}; - border-collapse: separate; - border-radius: ${theme.borderRadius}px; - - thead > tr > th { - border: 0; - } - - tbody { - tr:first-of-type > td { - border-top: 0; - } - tr > td { - border-top: 1px solid ${theme.colorSplit}; - } - } - th { - position: sticky; - top: 0; - - &:first-of-type { - padding-left: ${theme.sizeUnit * 4}px; - } - - &.xs { - min-width: 25px; - } - &.sm { - min-width: 50px; - } - &.md { - min-width: 75px; - } - &.lg { - min-width: 100px; - } - &.xl { - min-width: 150px; - } - &.xxl { - min-width: 200px; - } - - span { - white-space: nowrap; - display: flex; - align-items: center; - line-height: 2; - } - - svg { - display: inline-block; - position: relative; - } - } - - td { - &.xs { - width: 25px; - } - &.sm { - width: 50px; - } - &.md { - width: 75px; - } - &.lg { - width: 100px; - } - &.xl { - width: 150px; - } - &.xxl { - width: 200px; - } - } - - .table-cell-loader { - position: relative; - - .loading-bar { - background-color: ${theme.colorBgTextHover}; - border-radius: 7px; - - span { - visibility: hidden; - } - } - - .empty-loading-bar { - display: inline-block; - width: 100%; - height: 1.2em; - } + th.antd5-column-cell { + min-width: fit-content; } - .actions { + opacity: 0; + font-size: ${theme.fontSizeXL}px; + display: flex; white-space: nowrap; min-width: 100px; - svg, i { margin-right: 8px; - &:hover { path { fill: ${theme.colorPrimary}; } } } } - - .table-row { + .antd5-table-row:hover { .actions { - opacity: 0; - font-size: ${theme.fontSizeXL}px; - display: flex; - } - - &:hover { - background-color: ${theme.colorBgTextHover}; - - .actions { - opacity: 1; - transition: opacity ease-in ${theme.motionDurationMid}; - } - } - } - - .table-row-selected { - background-color: ${theme.colorPrimaryBgHover}; - - &:hover { - background-color: ${theme.colorPrimaryBgHover}; + opacity: 1; + transition: opacity ease-in ${theme.motionDurationMid}; } } - - .table-cell { + .antd5-table-cell { font-feature-settings: 'tnum' 1; text-overflow: ellipsis; overflow: hidden; max-width: 320px; line-height: 1; vertical-align: middle; - &:first-of-type { - padding-left: ${theme.sizeUnit * 4}px; - } - &__wrap { - white-space: normal; - } - &__nowrap { - white-space: nowrap; - } + padding-left: ${theme.sizeUnit * 4}px; + white-space: nowrap; } - - @keyframes loading-shimmer { - 40% { - background-position: 100% 0; - } - - 100% { - background-position: 100% 0; - } + .antd5-table-placeholder .antd5-table-cell { + border-bottom: 0; } `} `; - -Table.displayName = 'table'; - export default memo( ({ - getTableProps, - getTableBodyProps, - prepareRow, - headerGroups, columns, rows, loading, - highlightRowId, + setSortBy, + headerGroups, columnsForWrapText, - }: TableCollectionProps) => ( - <Table - {...getTableProps()} - className="table table-hover" - data-test="listview-table" - > - <thead> - {headerGroups.map(headerGroup => ( - <tr {...headerGroup.getHeaderGroupProps()}> - {headerGroup.headers.map(column => { - let sortIcon = <Icons.Sort />; - if (column.isSorted && column.isSortedDesc) { - sortIcon = <Icons.SortDesc />; - } else if (column.isSorted && !column.isSortedDesc) { - sortIcon = <Icons.SortAsc />; - } - return column.hidden ? null : ( - <th - {...column.getHeaderProps( - column.canSort ? column.getSortByToggleProps() : {}, - )} - data-test="sort-header" - className={cx({ - [column.size || '']: column.size, - })} - > - <span> - {column.render('Header')} - {column.canSort && sortIcon} - </span> - </th> - ); - })} - </tr> - ))} - </thead> - <tbody {...getTableBodyProps()}> - {loading && - rows.length === 0 && - [...new Array(12)].map((_, i) => ( - <tr key={i}> - {columns.map((column, i2) => { - if (column.hidden) return null; - return ( - <td - key={i2} - className={cx('table-cell', { - 'table-cell-loader': loading, - })} - > - <span - className="loading-bar empty-loading-bar" - role="progressbar" - aria-label="loading" - /> - </td> - ); - })} - </tr> - ))} - {rows.length > 0 && - rows.map(row => { - prepareRow(row); - // @ts-ignore - const rowId = row.original.id; - return ( - <tr - data-test="table-row" - {...row.getRowProps()} - className={cx('table-row', { - 'table-row-selected': - row.isSelected || - (typeof rowId !== 'undefined' && rowId === highlightRowId), - })} - > - {row.cells.map(cell => { - if (cell.column.hidden) return null; - const columnCellProps = cell.column.cellProps || {}; - const isWrapText = columnsForWrapText?.includes( - cell.column.Header as string, - ); - return ( - <td - data-test="table-row-cell" - className={cx( - `table-cell table-cell__${ - isWrapText ? 'wrap' : 'nowrap' - }`, - { - 'table-cell-loader': loading, - [cell.column.size || '']: cell.column.size, - }, - )} - {...cell.getCellProps()} - {...columnCellProps} - > - <span - className={cx({ 'loading-bar': loading })} - role={loading ? 'progressbar' : undefined} - > - <span data-test="cell-text">{cell.render('Cell')}</span> - </span> - </td> - ); - })} - </tr> - ); - })} - </tbody> - </Table> - ), + bulkSelectEnabled = false, + selectedFlatRows = [], + toggleRowSelected, + toggleAllRowsSelected, + prepareRow, + sticky, + }: TableCollectionProps) => { + const mappedColumns = mapColumns(columns, headerGroups, columnsForWrapText); + const mappedRows = mapRows(rows, prepareRow); + + const selectedRowKeys = useMemo( + () => selectedFlatRows?.map(row => row.id) || [], + [selectedFlatRows], + ); + + const rowSelection: TableRowSelection | undefined = useMemo(() => { + if (!bulkSelectEnabled) return undefined; + + return { + selectedRowKeys, + onSelect: (record, selected) => { + toggleRowSelected?.(record.rowId, selected); + }, + onSelectAll: (selected: boolean) => { + toggleAllRowsSelected?.(selected); + }, + }; + }, [ + bulkSelectEnabled, + selectedRowKeys, + toggleRowSelected, + toggleAllRowsSelected, + ]); + return ( + <StyledTable + loading={loading} + sticky={sticky ?? false} + columns={mappedColumns} + data={mappedRows} + size={TableSize.Middle} + data-test="listview-table" + pagination={false} + tableLayout="auto" + rowKey="rowId" + rowSelection={rowSelection} + locale={{ emptyText: null }} + sortDirections={['ascend', 'descend', 'ascend']} // HACK: To disable default sorting + components={{ + header: { + cell: (props: any) => ( + <th {...props} data-test="sort-header" role="columnheader" /> + ), + }, + body: { + row: (props: any) => <tr {...props} data-test="table-row" />, + cell: (props: any) => <td {...props} data-test="table-row-cell" />, + }, + }} + onChange={(_pagination, _filters, sorter: any) => { + setSortBy?.([ + { + id: sorter.field, + desc: sorter.order === 'descend', + }, + ] as SortingRule<any>[]); Review Comment: Check for a better type definition ########## superset-frontend/src/features/roles/RoleListEditModal.tsx: ########## @@ -58,24 +57,28 @@ const userColumns = [ { accessor: 'first_name', Header: 'First Name', + id: 'first_name', }, { accessor: 'last_name', Header: 'Last Name', + id: 'last_name', }, { accessor: 'username', Header: 'User Name', + id: 'username', }, { accessor: 'email', Header: 'Email', + id: 'email', }, { accessor: 'active', Header: 'Is Active?', - Cell: ({ cell }: CellProps<{ active: boolean }>) => - cell.value ? 'Yes' : 'No', + Cell: ({ value }: { value: boolean }) => (value ? 'Yes' : 'No'), Review Comment: Should Yes or No be localized? ########## superset-frontend/src/components/TableCollection/utils.tsx: ########## @@ -0,0 +1,104 @@ +/** + * 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. + */ + +/** + * This file contains utility functions for mapping columns and rows. + * These functions act as a compatibility layer between Ant Design Table and react-table. + */ + +import { ReactNode } from 'react'; +import { ColumnInstance, HeaderGroup, Row } from 'react-table'; +import { ColumnsType } from 'src/components/Table'; + +const COLUMN_SIZE_MAP: Record<string, number> = { + xs: 25, + sm: 50, + md: 75, + lg: 100, + xl: 150, + xxl: 200, +}; + +function getSortingInfo( + headerGroups: HeaderGroup[], + headerId: string, +): { + isSorted: boolean; + isSortedDesc: boolean; +} { + for (const headerGroup of headerGroups) { + const header = headerGroup.headers.find(h => h.id === headerId); + if (header) { + return { + isSorted: header.isSorted ?? false, + isSortedDesc: header.isSortedDesc ?? false, + }; + } + } + return { isSorted: false, isSortedDesc: false }; +} + +export const mapColumns = ( + columns: ColumnInstance[], + headerGroups: HeaderGroup[], + columnsForWrapText?: string[], +): ColumnsType => + columns.map(column => { + const { isSorted, isSortedDesc } = getSortingInfo(headerGroups, column.id); + return { + title: column.Header, + dataIndex: column.id.includes('.') ? column.id.split('.') : column.id, + hidden: column.hidden, + key: column.id, + minWidth: column.size ? COLUMN_SIZE_MAP[column.size] : COLUMN_SIZE_MAP.md, + ellipsis: !columnsForWrapText?.includes(column.id), + defaultSortOrder: isSorted + ? isSortedDesc + ? 'descend' + : 'ascend' + : undefined, + sorter: !column.disableSortBy, + render: (val: any, record: any): ReactNode => { Review Comment: Let's check for a better solution all these `any` ########## superset-frontend/src/components/TableCollection/index.tsx: ########## @@ -32,299 +33,131 @@ interface TableCollectionProps { loading: boolean; highlightRowId?: number; columnsForWrapText?: string[]; + setSortBy?: (updater: SortingRule<any>[]) => void; + bulkSelectEnabled?: boolean; + selectedFlatRows?: any[]; + toggleRowSelected?: (rowId: string, value: boolean) => void; + toggleAllRowsSelected?: (value?: boolean) => void; + sticky?: boolean; } -export const Table = styled.table` +const StyledTable = styled(Table)` ${({ theme }) => ` - background-color: ${theme.colorBgContainer}; - border-collapse: separate; - border-radius: ${theme.borderRadius}px; - - thead > tr > th { - border: 0; - } - - tbody { - tr:first-of-type > td { - border-top: 0; - } - tr > td { - border-top: 1px solid ${theme.colorSplit}; - } - } - th { - position: sticky; - top: 0; - - &:first-of-type { - padding-left: ${theme.sizeUnit * 4}px; - } - - &.xs { - min-width: 25px; - } - &.sm { - min-width: 50px; - } - &.md { - min-width: 75px; - } - &.lg { - min-width: 100px; - } - &.xl { - min-width: 150px; - } - &.xxl { - min-width: 200px; - } - - span { - white-space: nowrap; - display: flex; - align-items: center; - line-height: 2; - } - - svg { - display: inline-block; - position: relative; - } - } - - td { - &.xs { - width: 25px; - } - &.sm { - width: 50px; - } - &.md { - width: 75px; - } - &.lg { - width: 100px; - } - &.xl { - width: 150px; - } - &.xxl { - width: 200px; - } - } - - .table-cell-loader { - position: relative; - - .loading-bar { - background-color: ${theme.colorBgTextHover}; - border-radius: 7px; - - span { - visibility: hidden; - } - } - - .empty-loading-bar { - display: inline-block; - width: 100%; - height: 1.2em; - } + th.antd5-column-cell { + min-width: fit-content; } - .actions { + opacity: 0; + font-size: ${theme.fontSizeXL}px; + display: flex; white-space: nowrap; min-width: 100px; - svg, i { margin-right: 8px; - &:hover { path { fill: ${theme.colorPrimary}; } } } } - - .table-row { + .antd5-table-row:hover { .actions { - opacity: 0; - font-size: ${theme.fontSizeXL}px; - display: flex; - } - - &:hover { - background-color: ${theme.colorBgTextHover}; - - .actions { - opacity: 1; - transition: opacity ease-in ${theme.motionDurationMid}; - } - } - } - - .table-row-selected { - background-color: ${theme.colorPrimaryBgHover}; - - &:hover { - background-color: ${theme.colorPrimaryBgHover}; + opacity: 1; + transition: opacity ease-in ${theme.motionDurationMid}; } } - - .table-cell { + .antd5-table-cell { font-feature-settings: 'tnum' 1; text-overflow: ellipsis; overflow: hidden; max-width: 320px; line-height: 1; vertical-align: middle; - &:first-of-type { - padding-left: ${theme.sizeUnit * 4}px; - } - &__wrap { - white-space: normal; - } - &__nowrap { - white-space: nowrap; - } + padding-left: ${theme.sizeUnit * 4}px; + white-space: nowrap; } - - @keyframes loading-shimmer { - 40% { - background-position: 100% 0; - } - - 100% { - background-position: 100% 0; - } + .antd5-table-placeholder .antd5-table-cell { + border-bottom: 0; } `} `; - -Table.displayName = 'table'; - export default memo( ({ - getTableProps, - getTableBodyProps, - prepareRow, - headerGroups, columns, rows, loading, - highlightRowId, + setSortBy, + headerGroups, columnsForWrapText, - }: TableCollectionProps) => ( - <Table - {...getTableProps()} - className="table table-hover" - data-test="listview-table" - > - <thead> - {headerGroups.map(headerGroup => ( - <tr {...headerGroup.getHeaderGroupProps()}> - {headerGroup.headers.map(column => { - let sortIcon = <Icons.Sort />; - if (column.isSorted && column.isSortedDesc) { - sortIcon = <Icons.SortDesc />; - } else if (column.isSorted && !column.isSortedDesc) { - sortIcon = <Icons.SortAsc />; - } - return column.hidden ? null : ( - <th - {...column.getHeaderProps( - column.canSort ? column.getSortByToggleProps() : {}, - )} - data-test="sort-header" - className={cx({ - [column.size || '']: column.size, - })} - > - <span> - {column.render('Header')} - {column.canSort && sortIcon} - </span> - </th> - ); - })} - </tr> - ))} - </thead> - <tbody {...getTableBodyProps()}> - {loading && - rows.length === 0 && - [...new Array(12)].map((_, i) => ( - <tr key={i}> - {columns.map((column, i2) => { - if (column.hidden) return null; - return ( - <td - key={i2} - className={cx('table-cell', { - 'table-cell-loader': loading, - })} - > - <span - className="loading-bar empty-loading-bar" - role="progressbar" - aria-label="loading" - /> - </td> - ); - })} - </tr> - ))} - {rows.length > 0 && - rows.map(row => { - prepareRow(row); - // @ts-ignore - const rowId = row.original.id; - return ( - <tr - data-test="table-row" - {...row.getRowProps()} - className={cx('table-row', { - 'table-row-selected': - row.isSelected || - (typeof rowId !== 'undefined' && rowId === highlightRowId), - })} - > - {row.cells.map(cell => { - if (cell.column.hidden) return null; - const columnCellProps = cell.column.cellProps || {}; - const isWrapText = columnsForWrapText?.includes( - cell.column.Header as string, - ); - return ( - <td - data-test="table-row-cell" - className={cx( - `table-cell table-cell__${ - isWrapText ? 'wrap' : 'nowrap' - }`, - { - 'table-cell-loader': loading, - [cell.column.size || '']: cell.column.size, - }, - )} - {...cell.getCellProps()} - {...columnCellProps} - > - <span - className={cx({ 'loading-bar': loading })} - role={loading ? 'progressbar' : undefined} - > - <span data-test="cell-text">{cell.render('Cell')}</span> - </span> - </td> - ); - })} - </tr> - ); - })} - </tbody> - </Table> - ), + bulkSelectEnabled = false, + selectedFlatRows = [], + toggleRowSelected, + toggleAllRowsSelected, + prepareRow, + sticky, + }: TableCollectionProps) => { + const mappedColumns = mapColumns(columns, headerGroups, columnsForWrapText); + const mappedRows = mapRows(rows, prepareRow); + + const selectedRowKeys = useMemo( + () => selectedFlatRows?.map(row => row.id) || [], + [selectedFlatRows], + ); + + const rowSelection: TableRowSelection | undefined = useMemo(() => { + if (!bulkSelectEnabled) return undefined; + + return { + selectedRowKeys, + onSelect: (record, selected) => { + toggleRowSelected?.(record.rowId, selected); + }, + onSelectAll: (selected: boolean) => { + toggleAllRowsSelected?.(selected); + }, + }; + }, [ + bulkSelectEnabled, + selectedRowKeys, + toggleRowSelected, + toggleAllRowsSelected, + ]); + return ( + <StyledTable + loading={loading} + sticky={sticky ?? false} + columns={mappedColumns} + data={mappedRows} + size={TableSize.Middle} + data-test="listview-table" + pagination={false} + tableLayout="auto" + rowKey="rowId" + rowSelection={rowSelection} + locale={{ emptyText: null }} + sortDirections={['ascend', 'descend', 'ascend']} // HACK: To disable default sorting Review Comment: ```suggestion sortDirections={['ascend', 'descend', 'ascend']} ``` -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: notifications-unsubscr...@superset.apache.org For queries about this service, please contact Infrastructure at: us...@infra.apache.org --------------------------------------------------------------------- To unsubscribe, e-mail: notifications-unsubscr...@superset.apache.org For additional commands, e-mail: notifications-h...@superset.apache.org