Merge branch '14841-link-workbench1' refs #14841 refs #14720
[arvados-workbench2.git] / src / components / data-table / data-table.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import * as React from 'react';
6 import { Table, TableBody, TableRow, TableCell, TableHead, TableSortLabel, StyleRulesCallback, Theme, WithStyles, withStyles } from '@material-ui/core';
7 import { DataColumn, SortDirection } from './data-column';
8 import { DataTableDefaultView } from '../data-table-default-view/data-table-default-view';
9 import { DataTableFilters } from '../data-table-filters/data-table-filters-tree';
10 import { DataTableFiltersPopover } from '../data-table-filters/data-table-filters-popover';
11 import { countNodes } from '~/models/tree';
12
13 export type DataColumns<T> = Array<DataColumn<T>>;
14
15 export enum DataTableFetchMode {
16     PAGINATED,
17     INFINITE
18 }
19
20 export interface DataTableDataProps<T> {
21     items: T[];
22     columns: DataColumns<T>;
23     onRowClick: (event: React.MouseEvent<HTMLTableRowElement>, item: T) => void;
24     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: T) => void;
25     onRowDoubleClick: (event: React.MouseEvent<HTMLTableRowElement>, item: T) => void;
26     onSortToggle: (column: DataColumn<T>) => void;
27     onFiltersChange: (filters: DataTableFilters, column: DataColumn<T>) => void;
28     extractKey?: (item: T) => React.Key;
29     working?: boolean;
30     defaultView?: React.ReactNode;
31     currentItemUuid?: string;
32     currentRoute?: string;
33 }
34
35 type CssRules = "tableBody" | "root" | "content" | "noItemsInfo" | 'tableCell';
36
37 const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
38     root: {
39         overflowX: 'auto',
40         overflowY: 'hidden'
41     },
42     content: {
43         display: 'inline-block',
44         width: '100%'
45     },
46     tableBody: {
47         background: theme.palette.background.paper
48     },
49     noItemsInfo: {
50         textAlign: "center",
51         padding: theme.spacing.unit
52     },
53     tableCell: {
54         wordWrap: 'break-word'
55     }
56 });
57
58 type DataTableProps<T> = DataTableDataProps<T> & WithStyles<CssRules>;
59
60 export const DataTable = withStyles(styles)(
61     class Component<T> extends React.Component<DataTableProps<T>> {
62         render() {
63             const { items, classes } = this.props;
64             return <div className={classes.root}>
65                 <div className={classes.content}>
66                     <Table>
67                         <TableHead>
68                             <TableRow>
69                                 {this.mapVisibleColumns(this.renderHeadCell)}
70                             </TableRow>
71                         </TableHead>
72                         <TableBody className={classes.tableBody}>
73                             {items.map(this.renderBodyRow)}
74                         </TableBody>
75                     </Table>
76                     {items.length === 0 && this.props.working !== undefined && !this.props.working && this.renderNoItemsPlaceholder()}
77                 </div>
78             </div>;
79         }
80
81         renderNoItemsPlaceholder = () => {
82             return this.props.defaultView
83                 ? this.props.defaultView
84                 : <DataTableDefaultView/>;
85         }
86
87         renderHeadCell = (column: DataColumn<T>, index: number) => {
88             const { name, key, renderHeader, filters, sortDirection } = column;
89             const { onSortToggle, onFiltersChange } = this.props;
90             return <TableCell key={key || index}>
91                 {renderHeader ?
92                     renderHeader() :
93                     countNodes(filters) > 0
94                         ? <DataTableFiltersPopover
95                             name={`${name} filters`}
96                             onChange={filters =>
97                                 onFiltersChange &&
98                                 onFiltersChange(filters, column)}
99                             filters={filters}>
100                             {name}
101                         </DataTableFiltersPopover>
102                         : sortDirection
103                             ? <TableSortLabel
104                                 active={sortDirection !== SortDirection.NONE}
105                                 direction={sortDirection !== SortDirection.NONE ? sortDirection : undefined}
106                                 onClick={() =>
107                                     onSortToggle &&
108                                     onSortToggle(column)}>
109                                 {name}
110                             </TableSortLabel>
111                             : <span>
112                                 {name}
113                             </span>}
114             </TableCell>;
115         }
116
117         renderBodyRow = (item: any, index: number) => {
118             const { onRowClick, onRowDoubleClick, extractKey, classes, currentItemUuid } = this.props;
119             return <TableRow
120                 hover
121                 key={extractKey ? extractKey(item) : index}
122                 onClick={event => onRowClick && onRowClick(event, item)}
123                 onContextMenu={this.handleRowContextMenu(item)}
124                 onDoubleClick={event => onRowDoubleClick && onRowDoubleClick(event, item)}
125                 selected={item === currentItemUuid}>
126                 {this.mapVisibleColumns((column, index) => (
127                     <TableCell key={column.key || index} className={classes.tableCell}>
128                         {column.render(item)}
129                     </TableCell>
130                 ))}
131             </TableRow>;
132         }
133
134         mapVisibleColumns = (fn: (column: DataColumn<T>, index: number) => React.ReactElement<any>) => {
135             return this.props.columns.filter(column => column.selected).map(fn);
136         }
137
138         handleRowContextMenu = (item: T) =>
139             (event: React.MouseEvent<HTMLElement>) =>
140                 this.props.onContextMenu(event, item)
141
142     }
143 );