15768: removeMany dialog good, button styling Arvados-DCO-1.1-Signed-off-by: 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 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                                     <MultiselectToolbar buttons={defaultActions} />
236                                 </Grid>
237                             )}
238                         </div>
239                         <Grid item xs='auto' className={classes.dataTable}>
240                             <DataTable
241                                 columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
242                                 items={items}
243                                 onRowClick={(_, item: T) => onRowClick(item)}
244                                 onContextMenu={onContextMenu}
245                                 onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
246                                 onFiltersChange={onFiltersChange}
247                                 onSortToggle={onSortToggle}
248                                 extractKey={extractKey}
249                                 working={this.state.showLoading}
250                                 defaultViewIcon={defaultViewIcon}
251                                 defaultViewMessages={defaultViewMessages}
252                                 currentItemUuid={currentItemUuid}
253                                 currentRoute={paperKey}
254                                 toggleMSToolbar={toggleMSToolbar}
255                                 setCheckedListOnStore={setCheckedListOnStore}
256                             />
257                         </Grid>
258                         <Grid item xs>
259                             <Toolbar className={classes.footer}>
260                                 {elementPath && (
261                                     <Grid container>
262                                         <span data-cy='element-path'>{elementPath}</span>
263                                     </Grid>
264                                 )}
265                                 <Grid container={!elementPath} justify='flex-end'>
266                                     {fetchMode === DataTableFetchMode.PAGINATED ? (
267                                         <TablePagination
268                                             count={itemsAvailable}
269                                             rowsPerPage={rowsPerPage}
270                                             rowsPerPageOptions={rowsPerPageOptions}
271                                             page={this.props.page}
272                                             onChangePage={this.changePage}
273                                             onChangeRowsPerPage={this.changeRowsPerPage}
274                                             // Disable next button on empty lists since that's not default behavior
275                                             nextIconButtonProps={itemsAvailable > 0 ? {} : { disabled: true }}
276                                             component='div'
277                                         />
278                                     ) : (
279                                         <Button variant='text' size='medium' onClick={this.loadMore}>
280                                             Load more
281                                         </Button>
282                                     )}
283                                 </Grid>
284                             </Toolbar>
285                         </Grid>
286                     </Grid>
287                 </Paper>
288             );
289         }
290
291         changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
292             this.props.onChangePage(page);
293         };
294
295         changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = (event) => {
296             this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
297         };
298
299         loadMore = () => {
300             this.props.onLoadMore(this.props.page + 1);
301         };
302
303         renderContextMenuTrigger = (item: T) => (
304             <Grid container justify='center'>
305                 <Tooltip title='More options' disableFocusListener>
306                     <IconButton className={this.props.classes.moreOptionsButton} onClick={(event) => this.props.onContextMenu(event, item)}>
307                         <MoreOptionsIcon />
308                     </IconButton>
309                 </Tooltip>
310             </Grid>
311         );
312
313         contextMenuColumn: DataColumn<any, any> = {
314             name: 'Actions',
315             selected: true,
316             configurable: false,
317             filters: createTree(),
318             key: 'context-actions',
319             render: this.renderContextMenuTrigger,
320         };
321     }
322 );