// Copyright (C) The Arvados Authors. All rights reserved. // // SPDX-License-Identifier: AGPL-3.0 import React from "react"; import { CustomStyleRulesCallback } from 'common/custom-theme'; import { Table, TableBody, TableRow, TableCell, TableHead, TableSortLabel, Theme, IconButton, Tooltip, } from "@mui/material"; import { WithStyles } from '@mui/styles'; import withStyles from '@mui/styles/withStyles'; 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 { DataTableMultiselectPopover } from "../data-table-multiselect-popover/data-table-multiselect-popover"; import { DataTableFiltersPopover } from "../data-table-filters/data-table-filters-popover"; import { countNodes, getTreeDirty } from "models/tree"; import { IconType } from "components/icon/icon"; import { SvgIconProps } from "@mui/material/SvgIcon"; import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; import { createTree } from "models/tree"; import { DataTableMultiselectOption } from "../data-table-multiselect-popover/data-table-multiselect-popover"; import { isExactlyOneSelected } from "store/multiselect/multiselect-actions"; import { PendingIcon } from "components/icon/icon"; import { CustomTheme, ArvadosTheme } from "common/custom-theme"; export type DataColumns = Array>; export enum DataTableFetchMode { PAGINATED, INFINITE, } export interface DataTableDataProps { items: I[]; columns: DataColumns; onRowClick: (event: React.MouseEvent, item: I) => void; onContextMenu: (event: React.MouseEvent, item: I) => void; onRowDoubleClick: (event: React.MouseEvent, item: I) => void; onSortToggle: (column: DataColumn) => void; onFiltersChange: (filters: DataTableFilters, column: DataColumn) => void; extractKey?: (item: I) => React.Key; working?: boolean; defaultViewIcon?: IconType; defaultViewMessages?: string[]; toggleMSToolbar: (isVisible: boolean) => void; setCheckedListOnStore: (checkedList: TCheckedList) => void; currentRoute?: string; currentRouteUuid: string; checkedList: TCheckedList; selectedResourceUuid: string; setSelectedUuid: (uuid: string | null) => void; isNotFound?: boolean; } type CssRules = | "tableBody" | "root" | "content" | "noItemsInfo" | "checkBoxHead" | "checkBoxCell" | "clickBox" | "checkBox" | "firstTableCell" | "tableCell" | "firstTableHead" | "tableHead" | "selected" | "hovered" | "arrow" | "arrowButton" | "tableCellWorkflows"; const styles: CustomStyleRulesCallback = (theme: ArvadosTheme) => ({ root: { width: "100%", }, content: { display: "inline-block", width: "100%", }, tableBody: { background: theme.palette.background.paper, overflow: "auto", }, noItemsInfo: { textAlign: "center", padding: theme.spacing(1), }, checkBoxHead: { padding: "0", display: "flex", width: '2rem', height: "1.5rem", paddingLeft: '0.9rem', marginRight: '0.5rem', backgroundColor: theme.palette.background.paper, }, checkBoxCell: { padding: "0", backgroundColor: theme.palette.background.paper, }, clickBox: { display: 'flex', width: '1.6rem', height: "1.5rem", paddingLeft: '0.35rem', paddingTop: '0.1rem', marginLeft: '0.5rem', cursor: "pointer", }, checkBox: { cursor: "pointer", }, tableCell: { wordWrap: "break-word", paddingRight: "24px", }, firstTableCell: { paddingLeft: "5px", }, firstTableHead: { paddingLeft: "5px", }, tableHead: { wordWrap: "break-word", paddingRight: "24px", color: "#737373", fontSize: "0.8125rem", backgroundColor: theme.palette.background.paper, }, selected: { backgroundColor: `${CustomTheme.palette.grey['300']} !important` }, hovered: { backgroundColor: `${CustomTheme.palette.grey['100']} !important` }, 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, }, }); export type TCheckedList = Record; type DataTableState = { isSelected: boolean; isLoaded: boolean; hoveredIndex: number | null; }; type DataTableProps = DataTableDataProps & WithStyles; export const DataTable = withStyles(styles)( class Component extends React.Component> { state: DataTableState = { isSelected: false, isLoaded: false, hoveredIndex: null, }; componentDidMount(): void { this.initializeCheckedList([]); if((this.props.items.length > 0) && !this.state.isLoaded) { this.setState({ isLoaded: true }); } } componentDidUpdate(prevProps: Readonly>, prevState: DataTableState) { const { items, currentRouteUuid, setCheckedListOnStore } = this.props; const { isSelected } = this.state; const singleSelected = isExactlyOneSelected(this.props.checkedList); if (prevProps.items !== items) { if (isSelected === true) this.setState({ isSelected: false }); if (items.length) this.initializeCheckedList(items); else setCheckedListOnStore({}); } if (prevProps.currentRoute !== this.props.currentRoute) { this.initializeCheckedList([]); } if (singleSelected && singleSelected !== isExactlyOneSelected(prevProps.checkedList)) { this.props.setSelectedUuid(singleSelected); } if (!singleSelected && !!currentRouteUuid && !this.isAnySelected()) { this.props.setSelectedUuid(currentRouteUuid); } if (!singleSelected && this.isAnySelected()) { this.props.setSelectedUuid(null); } if(prevProps.working === true && this.props.working === false) { this.setState({ isLoaded: true }); } if((this.props.items.length > 0) && !this.state.isLoaded) { this.setState({ isLoaded: true }); } } componentWillUnmount(): void { this.initializeCheckedList([]); } checkBoxColumn: DataColumn = { name: "checkBoxColumn", selected: true, configurable: false, filters: createTree(), render: uuid => { const { classes, checkedList } = this.props; return (
{ ev.stopPropagation() this.handleSelectOne(uuid) }} onDoubleClick={(ev) => ev.stopPropagation()} > this.handleSelectOne(uuid)} onDoubleClick={(ev) => ev.stopPropagation()} >
); }, }; multiselectOptions: DataTableMultiselectOption[] = [ { name: "All", fn: list => this.handleSelectAll(list) }, { name: "None", fn: list => this.handleSelectNone(list) }, { name: "Invert", fn: list => this.handleInvertSelect(list) }, ]; initializeCheckedList = (uuids: any[]): void => { const newCheckedList = { ...this.props.checkedList }; if(Object.keys(newCheckedList).length === 0){ for(const uuid of uuids){ newCheckedList[uuid] = false } } for (const key in newCheckedList) { if (!uuids.includes(key)) { delete newCheckedList[key]; } } this.props.setCheckedListOnStore(newCheckedList); }; isAllSelected = (list: TCheckedList): boolean => { for (const key in list) { if (list[key] === false) return false; } return true; }; isAnySelected = (): boolean => { const { checkedList } = this.props; if (!checkedList) return false; if (!Object.keys(checkedList).length) return false; for (const key in checkedList) { if (checkedList[key] === true) return true; } return false; }; handleSelectOne = (uuid: string): void => { const { checkedList } = this.props; const newCheckedList = { ...checkedList }; newCheckedList[uuid] = !checkedList[uuid]; this.setState({ isSelected: this.isAllSelected(newCheckedList) }); this.props.setCheckedListOnStore(newCheckedList); }; handleSelectorSelect = (): void => { const { checkedList } = this.props; const { isSelected } = this.state; isSelected ? this.handleSelectNone(checkedList) : this.handleSelectAll(checkedList); }; handleSelectAll = (list: TCheckedList): void => { if (Object.keys(list).length) { const newCheckedList = { ...list }; for (const key in newCheckedList) { newCheckedList[key] = true; } this.setState({ isSelected: true }); this.props.setCheckedListOnStore(newCheckedList); } }; handleSelectNone = (list: TCheckedList): void => { const newCheckedList = { ...list }; for (const key in newCheckedList) { newCheckedList[key] = false; } this.setState({ isSelected: false }); this.props.setCheckedListOnStore(newCheckedList); }; handleInvertSelect = (list: TCheckedList): void => { if (Object.keys(list).length) { const newCheckedList = { ...list }; for (const key in newCheckedList) { newCheckedList[key] = !list[key]; } this.setState({ isSelected: this.isAllSelected(newCheckedList) }); this.props.setCheckedListOnStore(newCheckedList); } }; render() { const { items, classes, columns, isNotFound } = this.props; const { isLoaded } = this.state; if (columns.length && columns[0].name === this.checkBoxColumn.name) columns.shift(); columns.unshift(this.checkBoxColumn); return (
{this.mapVisibleColumns(this.renderHeadCell)} {(isLoaded && !isNotFound) && items.map(this.renderBodyRow)}
{(!isLoaded || isNotFound || items.length === 0) && this.renderNoItemsPlaceholder(this.props.columns)}
); } renderNoItemsPlaceholder = (columns: DataColumns) => { const { isLoaded } = this.state; const { working, isNotFound } = this.props; const dirty = columns.some(column => getTreeDirty("")(column.filters)); if (isNotFound && isLoaded) { return ( ); } else if (isLoaded === false || working === true) { return ( ); } else { // isLoaded && !working && !isNotFound return ( ); } }; renderHeadCell = (column: DataColumn, index: number) => { const { name, key, renderHeader, filters, sort } = column; const { onSortToggle, onFiltersChange, classes, checkedList } = this.props; const { isSelected } = this.state; return column.name === "checkBoxColumn" ? (
) : ( {renderHeader ? ( renderHeader() ) : countNodes(filters) > 0 ? ( onFiltersChange && onFiltersChange(filters, column)} filters={filters}> {name} ) : sort ? ( onSortToggle && onSortToggle(column)}> {name} ) : ( {name} )} ); }; ArrowIcon = ({ className, ...props }: SvgIconProps) => ( ); renderBodyRow = (item: any, index: number) => { const { onRowClick, onRowDoubleClick, extractKey, classes, selectedResourceUuid, currentRoute } = this.props; const { hoveredIndex } = this.state; const isRowSelected = item === selectedResourceUuid; const getClassnames = (colIndex: number) => { if(currentRoute === '/workflows') return classes.tableCellWorkflows; if(colIndex === 0) return classnames(classes.checkBoxCell, isRowSelected ? classes.selected : index === hoveredIndex ? classes.hovered : ""); if(colIndex === 1) return classnames(classes.tableCell, classes.firstTableCell, isRowSelected ? classes.selected : ""); return classnames(classes.tableCell, isRowSelected ? classes.selected : ""); }; const handleHover = (index: number | null) => { this.setState({ hoveredIndex: index }); } return ( onRowClick && onRowClick(event, item)} onContextMenu={this.handleRowContextMenu(item)} onDoubleClick={event => onRowDoubleClick && onRowDoubleClick(event, item)} selected={isRowSelected} className={isRowSelected ? classes.selected : ""} onMouseEnter={()=>handleHover(index)} onMouseLeave={()=>handleHover(null)} > {this.mapVisibleColumns((column, colIndex) => ( {column.render(item)} ))} ); }; mapVisibleColumns = (fn: (column: DataColumn, index: number) => React.ReactElement) => { return this.props.columns.filter(column => column.selected).map(fn); }; handleRowContextMenu = (item: T) => (event: React.MouseEvent) => this.props.onContextMenu(event, item); } );