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