// Copyright (C) The Arvados Authors. All rights reserved. // // SPDX-License-Identifier: AGPL-3.0 import React from 'react'; import { Table, TableBody, TableRow, TableCell, TableHead, TableSortLabel, StyleRulesCallback, Theme, WithStyles, withStyles, IconButton } from '@material-ui/core'; import classnames from 'classnames'; import { DataColumn, SortDirection } from './data-column'; import { DataTableDefaultView } from '../data-table-default-view/data-table-default-view'; import { DataTableFilters } from '../data-table-filters/data-table-filters-tree'; import { DataTableFiltersPopover } from '../data-table-filters/data-table-filters-popover'; import { countNodes, getTreeDirty } from 'models/tree'; import { IconType, PendingIcon } from 'components/icon/icon'; import { SvgIconProps } from '@material-ui/core/SvgIcon'; import ArrowDownwardIcon from '@material-ui/icons/ArrowDownward'; export type DataColumns<T> = Array<DataColumn<T>>; export enum DataTableFetchMode { PAGINATED, INFINITE } export interface DataTableDataProps<T> { items: T[]; columns: DataColumns<T>; onRowClick: (event: React.MouseEvent<HTMLTableRowElement>, item: T) => void; onContextMenu: (event: React.MouseEvent<HTMLElement>, item: T) => void; onRowDoubleClick: (event: React.MouseEvent<HTMLTableRowElement>, item: T) => void; onSortToggle: (column: DataColumn<T>) => void; onFiltersChange: (filters: DataTableFilters, column: DataColumn<T>) => void; extractKey?: (item: T) => React.Key; working?: boolean; defaultViewIcon?: IconType; defaultViewMessages?: string[]; currentItemUuid?: string; currentRoute?: string; } type CssRules = "tableBody" | "root" | "content" | "noItemsInfo" | 'tableCell' | 'arrow' | 'arrowButton' | 'tableCellWorkflows' | 'loader'; const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({ root: { width: '100%', }, content: { display: 'inline-block', width: '100%', }, tableBody: { background: theme.palette.background.paper }, loader: { left: '50%', marginLeft: '-84px', position: 'absolute' }, noItemsInfo: { textAlign: "center", padding: theme.spacing.unit }, tableCell: { wordWrap: 'break-word', paddingRight: '24px', color: '#737373' }, tableCellWorkflows: { '&:nth-last-child(2)': { padding: '0px', maxWidth: '48px' }, '&:last-child': { padding: '0px', paddingRight: '24px', width: '48px' } }, arrow: { margin: 0 }, arrowButton: { color: theme.palette.text.primary } }); type DataTableProps<T> = DataTableDataProps<T> & WithStyles<CssRules>; export const DataTable = withStyles(styles)( class Component<T> extends React.Component<DataTableProps<T>> { render() { const { items, classes, working } = this.props; return <div className={classes.root}> <div className={classes.content}> <Table> <TableHead> <TableRow> {this.mapVisibleColumns(this.renderHeadCell)} </TableRow> </TableHead> <TableBody className={classes.tableBody}> { !working && items.map(this.renderBodyRow) } </TableBody> </Table> { !!working && <div className={classes.loader}> <DataTableDefaultView icon={PendingIcon} messages={['Loading data, please wait.']} /> </div> } {items.length === 0 && !working && this.renderNoItemsPlaceholder(this.props.columns)} </div> </div>; } renderNoItemsPlaceholder = (columns: DataColumns<T>) => { const dirty = columns.some((column) => getTreeDirty('')(column.filters)); return <DataTableDefaultView icon={this.props.defaultViewIcon} messages={this.props.defaultViewMessages} filtersApplied={dirty} />; } renderHeadCell = (column: DataColumn<T>, index: number) => { const { name, key, renderHeader, filters, sortDirection } = column; const { onSortToggle, onFiltersChange, classes } = this.props; return <TableCell className={classes.tableCell} key={key || index}> {renderHeader ? renderHeader() : countNodes(filters) > 0 ? <DataTableFiltersPopover name={`${name} filters`} mutuallyExclusive={column.mutuallyExclusiveFilters} onChange={filters => onFiltersChange && onFiltersChange(filters, column)} filters={filters}> {name} </DataTableFiltersPopover> : sortDirection ? <TableSortLabel active={sortDirection !== SortDirection.NONE} direction={sortDirection !== SortDirection.NONE ? sortDirection : undefined} IconComponent={this.ArrowIcon} hideSortIcon onClick={() => onSortToggle && onSortToggle(column)}> {name} </TableSortLabel> : <span> {name} </span>} </TableCell>; } ArrowIcon = ({ className, ...props }: SvgIconProps) => ( <IconButton component='span' className={this.props.classes.arrowButton} tabIndex={-1}> <ArrowDownwardIcon {...props} className={classnames(className, this.props.classes.arrow)} /> </IconButton> ) renderBodyRow = (item: any, index: number) => { const { onRowClick, onRowDoubleClick, extractKey, classes, currentItemUuid, currentRoute } = this.props; return <TableRow hover key={extractKey ? extractKey(item) : index} onClick={event => onRowClick && onRowClick(event, item)} onContextMenu={this.handleRowContextMenu(item)} onDoubleClick={event => onRowDoubleClick && onRowDoubleClick(event, item)} selected={item === currentItemUuid}> {this.mapVisibleColumns((column, index) => <TableCell key={column.key || index} className={currentRoute === '/workflows' ? classes.tableCellWorkflows : classes.tableCell}> {column.render(item)} </TableCell> )} </TableRow>; } mapVisibleColumns = (fn: (column: DataColumn<T>, index: number) => React.ReactElement<any>) => { return this.props.columns.filter(column => column.selected).map(fn); } handleRowContextMenu = (item: T) => (event: React.MouseEvent<HTMLElement>) => this.props.onContextMenu(event, item) } );