15768: cleanup Arvados-DCO-1.1-Signed-off-by: Lisa Knox <lisa.knox@curii.com>
[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 import { DataTableMultiselectOption } from '../data-table-multiselect-popover/data-table-multiselect-popover';
33
34 export type DataColumns<I, R> = Array<DataColumn<I, R>>;
35
36 export enum DataTableFetchMode {
37     PAGINATED,
38     INFINITE,
39 }
40
41 export interface DataTableDataProps<I> {
42     items: I[];
43     columns: DataColumns<I, any>;
44     onRowClick: (event: React.MouseEvent<HTMLTableRowElement>, item: I) => void;
45     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: I) => void;
46     onRowDoubleClick: (event: React.MouseEvent<HTMLTableRowElement>, item: I) => void;
47     onSortToggle: (column: DataColumn<I, any>) => void;
48     onFiltersChange: (filters: DataTableFilters, column: DataColumn<I, any>) => void;
49     extractKey?: (item: I) => React.Key;
50     working?: boolean;
51     defaultViewIcon?: IconType;
52     defaultViewMessages?: string[];
53     currentItemUuid?: string;
54     currentRoute?: string;
55 }
56
57 type CssRules =
58     | 'tableBody'
59     | 'root'
60     | 'content'
61     | 'noItemsInfo'
62     | 'checkBoxHead'
63     | 'checkBoxCell'
64     | 'checkBox'
65     | 'tableCell'
66     | 'arrow'
67     | 'arrowButton'
68     | 'tableCellWorkflows'
69     | 'loader';
70
71 const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
72     root: {
73         width: '100%',
74     },
75     content: {
76         display: 'inline-block',
77         width: '100%',
78     },
79     tableBody: {
80         background: theme.palette.background.paper,
81     },
82     loader: {
83         left: '50%',
84         marginLeft: '-84px',
85         position: 'absolute',
86     },
87     noItemsInfo: {
88         textAlign: 'center',
89         padding: theme.spacing.unit,
90     },
91     checkBoxHead: {
92         padding: '0',
93         display: 'flex',
94     },
95     checkBoxCell: {
96         padding: '0',
97         paddingLeft: '10px',
98     },
99     checkBox: {
100         cursor: 'pointer',
101     },
102     tableCell: {
103         wordWrap: 'break-word',
104         paddingRight: '24px',
105         color: '#737373',
106     },
107     tableCellWorkflows: {
108         '&:nth-last-child(2)': {
109             padding: '0px',
110             maxWidth: '48px',
111         },
112         '&:last-child': {
113             padding: '0px',
114             paddingRight: '24px',
115             width: '48px',
116         },
117     },
118     arrow: {
119         margin: 0,
120     },
121     arrowButton: {
122         color: theme.palette.text.primary,
123     },
124 });
125
126 type DataTableState = {
127     isSelected: boolean;
128     checkedList: Record<string, boolean>;
129 };
130
131 type DataTableProps<T> = DataTableDataProps<T> & WithStyles<CssRules>;
132
133 const multiselectOptions: DataTableMultiselectOption[] = [
134     { name: 'First Option', fn: (checkedList) => console.log('one', checkedList) },
135     { name: 'Second Option', fn: (checkedList) => console.log('two', checkedList) },
136     { name: 'Third Option', fn: (checkedList) => console.log('three', checkedList) },
137 ];
138
139 export const DataTable = withStyles(styles)(
140     class Component<T> extends React.Component<DataTableProps<T>> {
141         state: DataTableState = {
142             isSelected: false,
143             checkedList: {},
144         };
145
146         componentDidMount(): void {
147             this.initializeCheckedList(this.props.items);
148         }
149
150         componentDidUpdate(prevProps: Readonly<DataTableProps<T>>) {
151             if (!arraysAreCongruent(prevProps.items, this.props.items)) {
152                 this.initializeCheckedList(this.props.items);
153             }
154         }
155
156         checkBoxColumn: DataColumn<any, any> = {
157             name: 'checkBoxColumn',
158             selected: true,
159             configurable: false,
160             filters: createTree(),
161             render: (uuid) => (
162                 <input
163                     type='checkbox'
164                     name={uuid}
165                     className={this.props.classes.checkBox}
166                     checked={this.state.checkedList[uuid] ?? false}
167                     onChange={() => this.handleCheck(uuid)}
168                     onDoubleClick={(ev) => ev.stopPropagation()}
169                 ></input>
170             ),
171         };
172
173         initializeCheckedList = (uuids: any[]): void => {
174             const { checkedList } = this.state;
175             uuids.forEach((uuid) => {
176                 if (!checkedList.hasOwnProperty[uuid]) {
177                     checkedList[uuid] = false;
178                 }
179             });
180         };
181
182         handleSelectorSelect = (): void => {
183             const { isSelected, checkedList } = this.state;
184             const newCheckedList = { ...checkedList };
185             for (const key in newCheckedList) {
186                 newCheckedList[key] = !isSelected;
187             }
188             this.setState({ isSelected: !isSelected, checkedList: newCheckedList });
189         };
190
191         handleCheck = (uuid: string): void => {
192             const { checkedList } = this.state;
193             const newCheckedList = { ...checkedList };
194             newCheckedList[uuid] = !checkedList[uuid];
195             this.setState({ checkedList: newCheckedList });
196         };
197
198         handleInvertSelect = (): void => {
199             const { checkedList } = this.state;
200             const newCheckedList = { ...checkedList };
201             for (const key in newCheckedList) {
202                 newCheckedList[key] = !checkedList[key];
203             }
204             this.setState({ checkedList: newCheckedList });
205         };
206
207         render() {
208             const { items, classes, working, columns } = this.props;
209             if (columns[0].name === this.checkBoxColumn.name) columns.shift();
210             columns.unshift(this.checkBoxColumn);
211
212             return (
213                 <div className={classes.root}>
214                     <div className={classes.content}>
215                         <Table>
216                             <TableHead>
217                                 <TableRow>{this.mapVisibleColumns(this.renderHeadCell)}</TableRow>
218                             </TableHead>
219                             <TableBody className={classes.tableBody}>{!working && items.map(this.renderBodyRow)}</TableBody>
220                         </Table>
221                         {!!working && (
222                             <div className={classes.loader}>
223                                 <DataTableDefaultView icon={PendingIcon} messages={['Loading data, please wait.']} />
224                             </div>
225                         )}
226                         {items.length === 0 && !working && this.renderNoItemsPlaceholder(this.props.columns)}
227                     </div>
228                 </div>
229             );
230         }
231
232         renderNoItemsPlaceholder = (columns: DataColumns<T, any>) => {
233             const dirty = columns.some((column) => getTreeDirty('')(column.filters));
234             return <DataTableDefaultView icon={this.props.defaultViewIcon} messages={this.props.defaultViewMessages} filtersApplied={dirty} />;
235         };
236
237         renderHeadCell = (column: DataColumn<T, any>, index: number) => {
238             const { name, key, renderHeader, filters, sort } = column;
239             const { onSortToggle, onFiltersChange, classes } = this.props;
240             return index === 0 ? (
241                 <TableCell key={key || index} className={classes.checkBoxCell}>
242                     <div className={classes.checkBoxHead}>
243                         <Tooltip title={this.state.isSelected ? 'Deselect All' : 'Select All'}>
244                             <input type='checkbox' className={classes.checkBox} checked={this.state.isSelected} onChange={this.handleSelectorSelect}></input>
245                         </Tooltip>
246                         <DataTableMultiselectPopover name={`Options`} options={multiselectOptions} checkedList={this.state.checkedList}></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 );