// 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, Tooltip, } 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 { 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, PendingIcon } from "components/icon/icon"; import { SvgIconProps } from "@material-ui/core/SvgIcon"; import ArrowDownwardIcon from "@material-ui/icons/ArrowDownward"; import { createTree } from "models/tree"; import { DataTableMultiselectOption } from "../data-table-multiselect-popover/data-table-multiselect-popover"; 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[]; currentItemUuid?: string; currentRoute?: string; toggleMSToolbar: (isVisible: boolean) => void; setCheckedListOnStore: (checkedList: TCheckedList) => void; checkedList: TCheckedList; } type CssRules = | "tableBody" | "root" | "content" | "noItemsInfo" | "checkBoxHead" | "checkBoxCell" | "checkBox" | "firstTableCell" | "tableCell" | "arrow" | "arrowButton" | "tableCellWorkflows" | "loader"; const styles: StyleRulesCallback = (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, }, checkBoxHead: { padding: "0", display: "flex", }, checkBoxCell: { padding: "0", paddingLeft: "10px", }, checkBox: { cursor: "pointer", }, tableCell: { wordWrap: "break-word", paddingRight: "24px", color: "#737373", }, firstTableCell: { paddingLeft: "5px", }, 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; }; type DataTableProps = DataTableDataProps & WithStyles; export const DataTable = withStyles(styles)( class Component extends React.Component> { state: DataTableState = { isSelected: false, }; componentDidMount(): void { this.initializeCheckedList([]); } componentDidUpdate(prevProps: Readonly>, prevState: DataTableState) { const { items, setCheckedListOnStore } = this.props; const { isSelected } = this.state; 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([]) } } componentWillUnmount(): void { this.initializeCheckedList([]) } checkBoxColumn: DataColumn = { name: "checkBoxColumn", selected: true, configurable: false, filters: createTree(), render: uuid => { const { classes, checkedList } = this.props; return ( 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 }; uuids.forEach(uuid => { if (!newCheckedList.hasOwnProperty(uuid)) { 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 (!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, working, columns } = this.props; if (columns[0].name === this.checkBoxColumn.name) columns.shift(); columns.unshift(this.checkBoxColumn); return (
{this.mapVisibleColumns(this.renderHeadCell)} {!working && items.map(this.renderBodyRow)}
{!!working && (
)} {items.length === 0 && !working && this.renderNoItemsPlaceholder(this.props.columns)}
); } renderNoItemsPlaceholder = (columns: DataColumns) => { const dirty = columns.some(column => getTreeDirty("")(column.filters)); 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, currentItemUuid, currentRoute } = this.props; return ( onRowClick && onRowClick(event, item)} onContextMenu={this.handleRowContextMenu(item)} onDoubleClick={event => onRowDoubleClick && onRowDoubleClick(event, item)} selected={item === currentItemUuid}> {this.mapVisibleColumns((column, index) => ( {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); } );