15768: state update on checkbox tick Arvados-DCO-1.1-Signed-off-by: Lisa Knox <lisa...
[arvados-workbench2.git] / src / components / data-explorer / data-explorer.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 { Grid, Paper, Toolbar, StyleRulesCallback, withStyles, WithStyles, TablePagination, IconButton, Tooltip, Button } from '@material-ui/core';
7 import { ColumnSelector } from 'components/column-selector/column-selector';
8 import { DataTable, DataColumns, DataTableFetchMode } from 'components/data-table/data-table';
9 import { DataColumn } from 'components/data-table/data-column';
10 import { SearchInput } from 'components/search-input/search-input';
11 import { ArvadosTheme } from 'common/custom-theme';
12 import { createTree } from 'models/tree';
13 import { DataTableFilters } from 'components/data-table-filters/data-table-filters-tree';
14 import { CloseIcon, IconType, MaximizeIcon, UnMaximizeIcon, MoreOptionsIcon } from 'components/icon/icon';
15 import { PaperProps } from '@material-ui/core/Paper';
16 import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
17
18 type CssRules = 'searchBox' | 'headerMenu' | 'toolbar' | 'footer' | 'root' | 'moreOptionsButton' | 'title' | 'dataTable' | 'container';
19
20 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
21     searchBox: {
22         paddingBottom: 0,
23     },
24     toolbar: {
25         paddingTop: 0,
26         paddingRight: theme.spacing.unit,
27     },
28     footer: {
29         overflow: 'auto',
30     },
31     root: {
32         height: '100%',
33     },
34     moreOptionsButton: {
35         padding: 0,
36     },
37     title: {
38         display: 'inline-block',
39         paddingLeft: theme.spacing.unit * 2,
40         paddingTop: theme.spacing.unit * 2,
41         fontSize: '18px',
42     },
43     dataTable: {
44         height: '100%',
45         overflow: 'auto',
46     },
47     container: {
48         height: '100%',
49     },
50     headerMenu: {
51         float: 'right',
52         display: 'inline-block',
53     },
54 });
55
56 interface DataExplorerDataProps<T> {
57     fetchMode: DataTableFetchMode;
58     items: T[];
59     itemsAvailable: number;
60     columns: DataColumns<T, any>;
61     searchLabel?: string;
62     searchValue: string;
63     rowsPerPage: number;
64     rowsPerPageOptions: number[];
65     page: number;
66     contextMenuColumn: boolean;
67     defaultViewIcon?: IconType;
68     defaultViewMessages?: string[];
69     working?: boolean;
70     currentRefresh?: string;
71     currentRoute?: string;
72     hideColumnSelector?: boolean;
73     paperProps?: PaperProps;
74     actions?: React.ReactNode;
75     hideSearchInput?: boolean;
76     title?: React.ReactNode;
77     paperKey?: string;
78     currentItemUuid: string;
79     elementPath?: string;
80 }
81
82 interface DataExplorerActionProps<T> {
83     onSetColumns: (columns: DataColumns<T, any>) => void;
84     onSearch: (value: string) => void;
85     onRowClick: (item: T) => void;
86     onRowDoubleClick: (item: T) => void;
87     onColumnToggle: (column: DataColumn<T, any>) => void;
88     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: T) => void;
89     onSortToggle: (column: DataColumn<T, any>) => void;
90     onFiltersChange: (filters: DataTableFilters, column: DataColumn<T, any>) => void;
91     onChangePage: (page: number) => void;
92     onChangeRowsPerPage: (rowsPerPage: number) => void;
93     onLoadMore: (page: number) => void;
94     extractKey?: (item: T) => React.Key;
95 }
96
97 type DataExplorerProps<T> = DataExplorerDataProps<T> & DataExplorerActionProps<T> & WithStyles<CssRules> & MPVPanelProps;
98
99 export const DataExplorer = withStyles(styles)(
100     class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
101         state = {
102             showLoading: false,
103             prevRefresh: '',
104             prevRoute: '',
105         };
106
107         componentDidUpdate(prevProps: DataExplorerProps<T>) {
108             const currentRefresh = this.props.currentRefresh || '';
109             const currentRoute = this.props.currentRoute || '';
110
111             if (currentRoute !== this.state.prevRoute) {
112                 // Component already mounted, but the user comes from a route change,
113                 // like browsing through a project hierarchy.
114                 this.setState({
115                     showLoading: this.props.working,
116                     prevRoute: currentRoute,
117                 });
118             }
119
120             if (currentRefresh !== this.state.prevRefresh) {
121                 // Component already mounted, but the user just clicked the
122                 // refresh button.
123                 this.setState({
124                     showLoading: this.props.working,
125                     prevRefresh: currentRefresh,
126                 });
127             }
128             if (this.state.showLoading && !this.props.working) {
129                 this.setState({
130                     showLoading: false,
131                 });
132             }
133         }
134
135         componentDidMount() {
136             if (this.props.onSetColumns) {
137                 this.props.onSetColumns(this.props.columns);
138             }
139             // Component just mounted, so we need to show the loading indicator.
140             this.setState({
141                 showLoading: this.props.working,
142                 prevRefresh: this.props.currentRefresh || '',
143                 prevRoute: this.props.currentRoute || '',
144             });
145         }
146
147         render() {
148             const {
149                 columns,
150                 onContextMenu,
151                 onFiltersChange,
152                 onSortToggle,
153                 extractKey,
154                 rowsPerPage,
155                 rowsPerPageOptions,
156                 onColumnToggle,
157                 searchLabel,
158                 searchValue,
159                 onSearch,
160                 items,
161                 itemsAvailable,
162                 onRowClick,
163                 onRowDoubleClick,
164                 classes,
165                 defaultViewIcon,
166                 defaultViewMessages,
167                 hideColumnSelector,
168                 actions,
169                 paperProps,
170                 hideSearchInput,
171                 paperKey,
172                 fetchMode,
173                 currentItemUuid,
174                 title,
175                 doHidePanel,
176                 doMaximizePanel,
177                 doUnMaximizePanel,
178                 panelName,
179                 panelMaximized,
180                 elementPath,
181             } = this.props;
182             return (
183                 <Paper className={classes.root} {...paperProps} key={paperKey} data-cy={this.props['data-cy']}>
184                     <Grid container direction='column' wrap='nowrap' className={classes.container}>
185                         <div>
186                             {title && (
187                                 <Grid item xs className={classes.title}>
188                                     {title}
189                                 </Grid>
190                             )}
191                             {(!hideColumnSelector || !hideSearchInput || !!actions) && (
192                                 <Grid className={classes.headerMenu} item xs>
193                                     <Toolbar className={classes.toolbar}>
194                                         {!hideSearchInput && (
195                                             <div className={classes.searchBox}>
196                                                 {!hideSearchInput && <SearchInput label={searchLabel} value={searchValue} selfClearProp={''} onSearch={onSearch} />}
197                                             </div>
198                                         )}
199                                         {actions}
200                                         {!hideColumnSelector && <ColumnSelector columns={columns} onColumnToggle={onColumnToggle} />}
201                                         {doUnMaximizePanel && panelMaximized && (
202                                             <Tooltip title={`Unmaximize ${panelName || 'panel'}`} disableFocusListener>
203                                                 <IconButton onClick={doUnMaximizePanel}>
204                                                     <UnMaximizeIcon />
205                                                 </IconButton>
206                                             </Tooltip>
207                                         )}
208                                         {doMaximizePanel && !panelMaximized && (
209                                             <Tooltip title={`Maximize ${panelName || 'panel'}`} disableFocusListener>
210                                                 <IconButton onClick={doMaximizePanel}>
211                                                     <MaximizeIcon />
212                                                 </IconButton>
213                                             </Tooltip>
214                                         )}
215                                         {doHidePanel && (
216                                             <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
217                                                 <IconButton disabled={panelMaximized} onClick={doHidePanel}>
218                                                     <CloseIcon />
219                                                 </IconButton>
220                                             </Tooltip>
221                                         )}
222                                     </Toolbar>
223                                 </Grid>
224                             )}
225                         </div>
226                         <Grid item xs='auto' className={classes.dataTable}>
227                             <DataTable
228                                 columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
229                                 items={items}
230                                 onRowClick={(_, item: T) => onRowClick(item)}
231                                 onContextMenu={onContextMenu}
232                                 onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
233                                 onFiltersChange={onFiltersChange}
234                                 onSortToggle={onSortToggle}
235                                 extractKey={extractKey}
236                                 working={this.state.showLoading}
237                                 defaultViewIcon={defaultViewIcon}
238                                 defaultViewMessages={defaultViewMessages}
239                                 currentItemUuid={currentItemUuid}
240                                 currentRoute={paperKey}
241                                 checkedList={{}}
242                             />
243                         </Grid>
244                         <Grid item xs>
245                             <Toolbar className={classes.footer}>
246                                 {elementPath && (
247                                     <Grid container>
248                                         <span data-cy='element-path'>{elementPath}</span>
249                                     </Grid>
250                                 )}
251                                 <Grid container={!elementPath} justify='flex-end'>
252                                     {fetchMode === DataTableFetchMode.PAGINATED ? (
253                                         <TablePagination
254                                             count={itemsAvailable}
255                                             rowsPerPage={rowsPerPage}
256                                             rowsPerPageOptions={rowsPerPageOptions}
257                                             page={this.props.page}
258                                             onChangePage={this.changePage}
259                                             onChangeRowsPerPage={this.changeRowsPerPage}
260                                             // Disable next button on empty lists since that's not default behavior
261                                             nextIconButtonProps={itemsAvailable > 0 ? {} : { disabled: true }}
262                                             component='div'
263                                         />
264                                     ) : (
265                                         <Button variant='text' size='medium' onClick={this.loadMore}>
266                                             Load more
267                                         </Button>
268                                     )}
269                                 </Grid>
270                             </Toolbar>
271                         </Grid>
272                     </Grid>
273                 </Paper>
274             );
275         }
276
277         changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
278             this.props.onChangePage(page);
279         };
280
281         changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = (event) => {
282             this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
283         };
284
285         loadMore = () => {
286             this.props.onLoadMore(this.props.page + 1);
287         };
288
289         renderContextMenuTrigger = (item: T) => (
290             <Grid container justify='center'>
291                 <Tooltip title='More options' disableFocusListener>
292                     <IconButton className={this.props.classes.moreOptionsButton} onClick={(event) => this.props.onContextMenu(event, item)}>
293                         <MoreOptionsIcon />
294                     </IconButton>
295                 </Tooltip>
296             </Grid>
297         );
298
299         contextMenuColumn: DataColumn<any, any> = {
300             name: 'Actions',
301             selected: true,
302             configurable: false,
303             filters: createTree(),
304             key: 'context-actions',
305             render: this.renderContextMenuTrigger,
306         };
307     }
308 );