19231: Add smaller page sizes (10 and 20 items) to load faster
[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, 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: theme.spacing.unit * 2
23     },
24     toolbar: {
25         paddingTop: theme.spacing.unit,
26         paddingRight: theme.spacing.unit * 2,
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 * 3,
40         paddingTop: theme.spacing.unit * 3,
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>;
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>) => void;
84     onSearch: (value: string) => void;
85     onRowClick: (item: T) => void;
86     onRowDoubleClick: (item: T) => void;
87     onColumnToggle: (column: DataColumn<T>) => void;
88     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: T) => void;
89     onSortToggle: (column: DataColumn<T>) => void;
90     onFiltersChange: (filters: DataTableFilters, column: DataColumn<T>) => 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> &
98     DataExplorerActionProps<T> & WithStyles<CssRules> & MPVPanelProps;
99
100 export const DataExplorer = withStyles(styles)(
101     class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
102         state = {
103             showLoading: false,
104             prevRefresh: '',
105             prevRoute: '',
106         };
107
108         componentDidUpdate(prevProps: DataExplorerProps<T>) {
109             const currentRefresh = this.props.currentRefresh || '';
110             const currentRoute = this.props.currentRoute || '';
111
112             if (currentRoute !== this.state.prevRoute) {
113                 // Component already mounted, but the user comes from a route change,
114                 // like browsing through a project hierarchy.
115                 this.setState({
116                     showLoading: this.props.working,
117                     prevRoute: currentRoute,
118                 });
119             }
120
121             if (currentRefresh !== this.state.prevRefresh) {
122                 // Component already mounted, but the user just clicked the
123                 // refresh button.
124                 this.setState({
125                     showLoading: this.props.working,
126                     prevRefresh: currentRefresh,
127                 });
128             }
129             if (this.state.showLoading && !this.props.working) {
130                 this.setState({
131                     showLoading: false,
132                 });
133             }
134         }
135
136         componentDidMount() {
137             if (this.props.onSetColumns) {
138                 this.props.onSetColumns(this.props.columns);
139             }
140             // Component just mounted, so we need to show the loading indicator.
141             this.setState({
142                 showLoading: this.props.working,
143                 prevRefresh: this.props.currentRefresh || '',
144                 prevRoute: this.props.currentRoute || '',
145             });
146         }
147
148         render() {
149             const {
150                 columns, onContextMenu, onFiltersChange, onSortToggle, extractKey,
151                 rowsPerPage, rowsPerPageOptions, onColumnToggle, searchLabel, searchValue, onSearch,
152                 items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
153                 defaultViewIcon, defaultViewMessages, hideColumnSelector, actions, paperProps, hideSearchInput,
154                 paperKey, fetchMode, currentItemUuid, title,
155                 doHidePanel, doMaximizePanel, panelName, panelMaximized, elementPath
156             } = this.props;
157
158             return <Paper className={classes.root} {...paperProps} key={paperKey} data-cy={this.props["data-cy"]}>
159                 <Grid container direction="column" wrap="nowrap" className={classes.container}>
160                     <div>
161                         {title && <Grid item xs className={classes.title}>{title}</Grid>}
162                         {
163                             (!hideColumnSelector || !hideSearchInput || !!actions) &&
164                             <Grid className={classes.headerMenu} item xs>
165                                 <Toolbar className={classes.toolbar}>
166                                     <Grid container justify="space-between" wrap="nowrap" alignItems="center">
167                                         {!hideSearchInput && <div className={classes.searchBox}>
168                                             {!hideSearchInput && <SearchInput
169                                                 label={searchLabel}
170                                                 value={searchValue}
171                                                 selfClearProp={currentItemUuid}
172                                                 onSearch={onSearch} />}
173                                         </div>}
174                                         {actions}
175                                         {!hideColumnSelector && <ColumnSelector
176                                             columns={columns}
177                                             onColumnToggle={onColumnToggle} />}
178                                     </Grid>
179                                     { doMaximizePanel && !panelMaximized &&
180                                         <Tooltip title={`Maximize ${panelName || 'panel'}`} disableFocusListener>
181                                             <IconButton onClick={doMaximizePanel}><MaximizeIcon /></IconButton>
182                                         </Tooltip> }
183                                     { doHidePanel &&
184                                         <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
185                                             <IconButton onClick={doHidePanel}><CloseIcon /></IconButton>
186                                         </Tooltip> }
187                                 </Toolbar>
188                             </Grid>
189                         }
190                     </div>
191                 <Grid item xs="auto" className={classes.dataTable}><DataTable
192                     columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
193                     items={items}
194                     onRowClick={(_, item: T) => onRowClick(item)}
195                     onContextMenu={onContextMenu}
196                     onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
197                     onFiltersChange={onFiltersChange}
198                     onSortToggle={onSortToggle}
199                     extractKey={extractKey}
200                     working={this.state.showLoading}
201                     defaultViewIcon={defaultViewIcon}
202                     defaultViewMessages={defaultViewMessages}
203                     currentItemUuid={currentItemUuid}
204                     currentRoute={paperKey} /></Grid>
205                 <Grid item xs><Toolbar className={classes.footer}>
206                     {
207                         elementPath &&
208                         <Grid container>
209                             <span data-cy="element-path">
210                                 {elementPath}
211                             </span>
212                         </Grid>
213                     }
214                     <Grid container={!elementPath} justify="flex-end">
215                         {fetchMode === DataTableFetchMode.PAGINATED ? <TablePagination
216                             count={itemsAvailable}
217                             rowsPerPage={rowsPerPage}
218                             rowsPerPageOptions={rowsPerPageOptions}
219                             page={this.props.page}
220                             onChangePage={this.changePage}
221                             onChangeRowsPerPage={this.changeRowsPerPage}
222                             // Disable next button on empty lists since that's not default behavior
223                             nextIconButtonProps={(itemsAvailable > 0) ? {} : {disabled: true}}
224                             component="div" /> : <Button
225                                 variant="text"
226                                 size="medium"
227                                 onClick={this.loadMore}
228                             >Load more</Button>}
229                     </Grid>
230                 </Toolbar></Grid>
231                 </Grid>
232             </Paper>;
233         }
234
235         changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
236             this.props.onChangePage(page);
237         }
238
239         changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = (event) => {
240             this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
241         }
242
243         loadMore = () => {
244             this.props.onLoadMore(this.props.page + 1);
245         }
246
247         renderContextMenuTrigger = (item: T) =>
248             <Grid container justify="center">
249                 <Tooltip title="More options" disableFocusListener>
250                     <IconButton className={this.props.classes.moreOptionsButton} onClick={event => this.props.onContextMenu(event, item)}>
251                         <MoreOptionsIcon />
252                     </IconButton>
253                 </Tooltip>
254             </Grid>
255
256         contextMenuColumn: DataColumn<any> = {
257             name: "Actions",
258             selected: true,
259             configurable: false,
260             filters: createTree(),
261             key: "context-actions",
262             render: this.renderContextMenuTrigger
263         };
264     }
265 );