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