15768: multiselect popover pops over Arvados-DCO-1.1-Signed-off-by: Lisa Knox <lisa...
[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 React from 'react';
6 import {
7     Table,
8     TableBody,
9     TableRow,
10     TableCell,
11     TableHead,
12     TableSortLabel,
13     StyleRulesCallback,
14     Theme,
15     WithStyles,
16     withStyles,
17     IconButton,
18     Tooltip,
19 } from '@material-ui/core';
20 import classnames from 'classnames';
21 import { DataColumn, SortDirection } from './data-column';
22 import { DataTableDefaultView } from '../data-table-default-view/data-table-default-view';
23 import { DataTableFilters } from '../data-table-filters/data-table-filters-tree';
24 import { DataTableMultiselectPopover } from '../data-table-multiselect-popover/data-table-multiselect-popover';
25 import { DataTableFiltersPopover } from '../data-table-filters/data-table-filters-popover';
26 import { countNodes, getTreeDirty } from 'models/tree';
27 import { IconType, PendingIcon } from 'components/icon/icon';
28 import { SvgIconProps } from '@material-ui/core/SvgIcon';
29 import ArrowDownwardIcon from '@material-ui/icons/ArrowDownward';
30 import { createTree } from 'models/tree';
31 import arraysAreCongruent from 'validators/arrays-are-congruent';
32
33 export type DataColumns<I, R> = Array<DataColumn<I, R>>;
34
35 export enum DataTableFetchMode {
36     PAGINATED,
37     INFINITE,
38 }
39
40 export interface DataTableDataProps<I> {
41     items: I[];
42     columns: DataColumns<I, any>;
43     onRowClick: (event: React.MouseEvent<HTMLTableRowElement>, item: I) => void;
44     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: I) => void;
45     onRowDoubleClick: (event: React.MouseEvent<HTMLTableRowElement>, item: I) => void;
46     onSortToggle: (column: DataColumn<I, any>) => void;
47     onFiltersChange: (filters: DataTableFilters, column: DataColumn<I, any>) => void;
48     extractKey?: (item: I) => React.Key;
49     working?: boolean;
50     defaultViewIcon?: IconType;
51     defaultViewMessages?: string[];
52     currentItemUuid?: string;
53     currentRoute?: string;
54 }
55
56 type CssRules =
57     | 'tableBody'
58     | 'root'
59     | 'content'
60     | 'noItemsInfo'
61     | 'checkBoxHead'
62     | 'checkBoxCell'
63     | 'checkBox'
64     | 'tableCell'
65     | 'arrow'
66     | 'arrowButton'
67     | 'tableCellWorkflows'
68     | 'loader';
69
70 const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
71     root: {
72         width: '100%',
73     },
74     content: {
75         display: 'inline-block',
76         width: '100%',
77     },
78     tableBody: {
79         background: theme.palette.background.paper,
80     },
81     loader: {
82         left: '50%',
83         marginLeft: '-84px',
84         position: 'absolute',
85     },
86     noItemsInfo: {
87         textAlign: 'center',
88         padding: theme.spacing.unit,
89     },
90     checkBoxHead: {
91         padding: '0',
92         display: 'flex',
93     },
94     checkBoxCell: {
95         padding: '0',
96         paddingLeft: '10px',
97     },
98     checkBox: {
99         cursor: 'pointer',
100     },
101     tableCell: {
102         wordWrap: 'break-word',
103         paddingRight: '24px',
104         color: '#737373',
105     },
106     tableCellWorkflows: {
107         '&:nth-last-child(2)': {
108             padding: '0px',
109             maxWidth: '48px',
110         },
111         '&:last-child': {
112             padding: '0px',
113             paddingRight: '24px',
114             width: '48px',
115         },
116     },
117     arrow: {
118         margin: 0,
119     },
120     arrowButton: {
121         color: theme.palette.text.primary,
122     },
123 });
124
125 type DataTableState = {
126     isSelected: boolean;
127     checkedList: Record<string, boolean>;
128 };
129
130 type DataTableProps<T> = DataTableDataProps<T> & WithStyles<CssRules>;
131
132 export const DataTable = withStyles(styles)(
133     class Component<T> extends React.Component<DataTableProps<T>> {
134         state: DataTableState = {
135             isSelected: false,
136             checkedList: {},
137         };
138
139         componentDidMount(): void {
140             this.initializeCheckedList(this.props.items);
141         }
142
143         componentDidUpdate(prevProps: Readonly<DataTableProps<T>>) {
144             // console.log(this.state.checkedList);
145             if (!arraysAreCongruent(prevProps.items, this.props.items)) {
146                 this.initializeCheckedList(this.props.items);
147             }
148         }
149
150         checkBoxColumn: DataColumn<any, any> = {
151             name: 'checkBoxColumn',
152             selected: true,
153             configurable: false,
154             filters: createTree(),
155             render: (uuid) => (
156                 <input
157                     type='checkbox'
158                     name={uuid}
159                     className={this.props.classes.checkBox}
160                     checked={this.state.checkedList[uuid] ?? false}
161                     onChange={() => this.handleCheck(uuid)}
162                     onDoubleClick={(ev) => ev.stopPropagation()}
163                 ></input>
164             ),
165         };
166
167         initializeCheckedList = (uuids: any[]): void => {
168             const { checkedList } = this.state;
169             uuids.forEach((uuid) => {
170                 if (!checkedList.hasOwnProperty[uuid]) {
171                     checkedList[uuid] = false;
172                 }
173             });
174         };
175
176         handleSelectorSelect = (): void => {
177             const { isSelected, checkedList } = this.state;
178             const newCheckedList = { ...checkedList };
179             for (const key in newCheckedList) {
180                 newCheckedList[key] = !isSelected;
181             }
182             this.setState({ isSelected: !isSelected, checkedList: newCheckedList });
183         };
184
185         handleCheck = (uuid: string): void => {
186             const { checkedList } = this.state;
187             const newCheckedList = { ...checkedList };
188             newCheckedList[uuid] = !checkedList[uuid];
189             this.setState({ checkedList: newCheckedList });
190             // console.log(newCheckedList);
191         };
192
193         handleInvertSelect = (): void => {
194             const { checkedList } = this.state;
195             const newCheckedList = { ...checkedList };
196             for (const key in newCheckedList) {
197                 newCheckedList[key] = !checkedList[key];
198             }
199             this.setState({ checkedList: newCheckedList });
200         };
201
202         render() {
203             const { items, classes, working, columns } = this.props;
204             if (columns[0].name === this.checkBoxColumn.name) columns.shift();
205             columns.unshift(this.checkBoxColumn);
206
207             return (
208                 <div className={classes.root}>
209                     <div className={classes.content}>
210                         <Table>
211                             <TableHead>
212                                 <TableRow>{this.mapVisibleColumns(this.renderHeadCell)}</TableRow>
213                             </TableHead>
214                             <TableBody className={classes.tableBody}>{!working && items.map(this.renderBodyRow)}</TableBody>
215                         </Table>
216                         {!!working && (
217                             <div className={classes.loader}>
218                                 <DataTableDefaultView icon={PendingIcon} messages={['Loading data, please wait.']} />
219                             </div>
220                         )}
221                         {items.length === 0 && !working && this.renderNoItemsPlaceholder(this.props.columns)}
222                     </div>
223                 </div>
224             );
225         }
226
227         renderNoItemsPlaceholder = (columns: DataColumns<T, any>) => {
228             const dirty = columns.some((column) => getTreeDirty('')(column.filters));
229             return <DataTableDefaultView icon={this.props.defaultViewIcon} messages={this.props.defaultViewMessages} filtersApplied={dirty} />;
230         };
231
232         renderHeadCell = (column: DataColumn<T, any>, index: number) => {
233             const { name, key, renderHeader, filters, sort } = column;
234             const { onSortToggle, onFiltersChange, classes } = this.props;
235             return index === 0 ? (
236                 <TableCell key={key || index} className={classes.checkBoxCell}>
237                     <div className={classes.checkBoxHead}>
238                         <Tooltip title={this.state.isSelected ? 'Deselect All' : 'Select All'}>
239                             <input type='checkbox' className={classes.checkBox} checked={this.state.isSelected} onChange={this.handleSelectorSelect}></input>
240                         </Tooltip>
241                         <DataTableMultiselectPopover
242                             name={`${name} filters`}
243                             mutuallyExclusive={column.mutuallyExclusiveFilters}
244                             onChange={(filters) => onFiltersChange && onFiltersChange(filters, column)}
245                             filters={filters}
246                         ></DataTableMultiselectPopover>
247                     </div>
248                 </TableCell>
249             ) : (
250                 <TableCell className={classes.tableCell} key={key || index}>
251                     {renderHeader ? (
252                         renderHeader()
253                     ) : countNodes(filters) > 0 ? (
254                         <DataTableFiltersPopover
255                             name={`${name} filters`}
256                             mutuallyExclusive={column.mutuallyExclusiveFilters}
257                             onChange={(filters) => onFiltersChange && onFiltersChange(filters, column)}
258                             filters={filters}
259                         >
260                             {name}
261                         </DataTableFiltersPopover>
262                     ) : sort ? (
263                         <TableSortLabel
264                             active={sort.direction !== SortDirection.NONE}
265                             direction={sort.direction !== SortDirection.NONE ? sort.direction : undefined}
266                             IconComponent={this.ArrowIcon}
267                             hideSortIcon
268                             onClick={() => onSortToggle && onSortToggle(column)}
269                         >
270                             {name}
271                         </TableSortLabel>
272                     ) : (
273                         <span>{name}</span>
274                     )}
275                 </TableCell>
276             );
277         };
278
279         ArrowIcon = ({ className, ...props }: SvgIconProps) => (
280             <IconButton component='span' className={this.props.classes.arrowButton} tabIndex={-1}>
281                 <ArrowDownwardIcon {...props} className={classnames(className, this.props.classes.arrow)} />
282             </IconButton>
283         );
284
285         renderBodyRow = (item: any, index: number) => {
286             const { onRowClick, onRowDoubleClick, extractKey, classes, currentItemUuid, currentRoute } = this.props;
287             return (
288                 <TableRow
289                     hover
290                     key={extractKey ? extractKey(item) : index}
291                     onClick={(event) => onRowClick && onRowClick(event, item)}
292                     onContextMenu={this.handleRowContextMenu(item)}
293                     onDoubleClick={(event) => onRowDoubleClick && onRowDoubleClick(event, item)}
294                     selected={item === currentItemUuid}
295                 >
296                     {this.mapVisibleColumns((column, index) => (
297                         <TableCell
298                             key={column.key || index}
299                             className={currentRoute === '/workflows' ? classes.tableCellWorkflows : index === 0 ? classes.checkBoxCell : classes.tableCell}
300                         >
301                             {column.render(item)}
302                         </TableCell>
303                     ))}
304                 </TableRow>
305             );
306         };
307
308         mapVisibleColumns = (fn: (column: DataColumn<T, any>, index: number) => React.ReactElement<any>) => {
309             return this.props.columns.filter((column) => column.selected).map(fn);
310         };
311
312         handleRowContextMenu = (item: T) => (event: React.MouseEvent<HTMLElement>) => this.props.onContextMenu(event, item);
313     }
314 );