ea95648e7b8b48bfd7866461e22f8fc9d67fbe00
[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                             />
242                         </Grid>
243                         <Grid item xs>
244                             <Toolbar className={classes.footer}>
245                                 {elementPath && (
246                                     <Grid container>
247                                         <span data-cy='element-path'>{elementPath}</span>
248                                     </Grid>
249                                 )}
250                                 <Grid container={!elementPath} justify='flex-end'>
251                                     {fetchMode === DataTableFetchMode.PAGINATED ? (
252                                         <TablePagination
253                                             count={itemsAvailable}
254                                             rowsPerPage={rowsPerPage}
255                                             rowsPerPageOptions={rowsPerPageOptions}
256                                             page={this.props.page}
257                                             onChangePage={this.changePage}
258                                             onChangeRowsPerPage={this.changeRowsPerPage}
259                                             // Disable next button on empty lists since that's not default behavior
260                                             nextIconButtonProps={itemsAvailable > 0 ? {} : { disabled: true }}
261                                             component='div'
262                                         />
263                                     ) : (
264                                         <Button variant='text' size='medium' onClick={this.loadMore}>
265                                             Load more
266                                         </Button>
267                                     )}
268                                 </Grid>
269                             </Toolbar>
270                         </Grid>
271                     </Grid>
272                 </Paper>
273             );
274         }
275
276         changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
277             this.props.onChangePage(page);
278         };
279
280         changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = (event) => {
281             this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
282         };
283
284         loadMore = () => {
285             this.props.onLoadMore(this.props.page + 1);
286         };
287
288         renderContextMenuTrigger = (item: T) => (
289             <Grid container justify='center'>
290                 <Tooltip title='More options' disableFocusListener>
291                     <IconButton className={this.props.classes.moreOptionsButton} onClick={(event) => this.props.onContextMenu(event, item)}>
292                         <MoreOptionsIcon />
293                     </IconButton>
294                 </Tooltip>
295             </Grid>
296         );
297
298         contextMenuColumn: DataColumn<any, any> = {
299             name: 'Actions',
300             selected: true,
301             configurable: false,
302             filters: createTree(),
303             key: 'context-actions',
304             render: this.renderContextMenuTrigger,
305         };
306     }
307 );