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