21364: stopped itemsAvailable count from resetting on load more click
[arvados.git] / services / workbench2 / 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, Typography } 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 { MultiselectToolbar } from "components/multiselect-toolbar/MultiselectToolbar";
13 import { TCheckedList } from "components/data-table/data-table";
14 import { createTree } from "models/tree";
15 import { DataTableFilters } from "components/data-table-filters/data-table-filters-tree";
16 import { CloseIcon, IconType, MaximizeIcon, UnMaximizeIcon, MoreVerticalIcon } from "components/icon/icon";
17 import { PaperProps } from "@material-ui/core/Paper";
18 import { MPVPanelProps } from "components/multi-panel-view/multi-panel-view";
19
20 type CssRules = "titleWrapper" | "searchBox" | "headerMenu" | "toolbar" | "footer" | "loadMoreContainer" | "numResults" | "root" | "moreOptionsButton" | "title" | 'subProcessTitle' | "dataTable" | "container";
21
22 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
23     titleWrapper: {
24         display: "flex",
25         justifyContent: "space-between",
26     },
27     searchBox: {
28         paddingBottom: 0,
29     },
30     toolbar: {
31         paddingTop: 0,
32         paddingRight: theme.spacing.unit,
33         paddingLeft: "10px",
34     },
35     footer: {
36         overflow: "auto",
37     },
38     loadMoreContainer: {
39         minWidth: '8rem',
40     },
41     root: {
42         height: "100%",
43     },
44     moreOptionsButton: {
45         padding: 0,
46     },
47     numResults: {
48         marginTop: 0,
49         fontSize: "10px",
50         marginLeft: "10px",
51         marginBottom: '-0.5rem',
52         minWidth: '8.5rem',
53     },
54     title: {
55         display: "inline-block",
56         paddingLeft: theme.spacing.unit * 2,
57         paddingTop: theme.spacing.unit * 2,
58         fontSize: "18px",
59         paddingRight: "10px",
60     },
61     subProcessTitle: {
62         display: "inline-block",
63         paddingLeft: theme.spacing.unit * 2,
64         paddingTop: theme.spacing.unit * 2,
65         fontSize: "18px",
66         flexGrow: 0,
67         paddingRight: "10px",
68     },
69     dataTable: {
70         height: "100%",
71         overflow: "auto",
72     },
73     container: {
74         height: "100%",
75     },
76     headerMenu: {
77         marginLeft: "auto",
78         flexBasis: "initial",
79         flexGrow: 0,
80     },
81 });
82
83 interface DataExplorerDataProps<T> {
84     fetchMode: DataTableFetchMode;
85     items: T[];
86     itemsAvailable: number;
87     columns: DataColumns<T, any>;
88     searchLabel?: string;
89     searchValue: string;
90     rowsPerPage: number;
91     rowsPerPageOptions: number[];
92     page: number;
93     contextMenuColumn: boolean;
94     defaultViewIcon?: IconType;
95     defaultViewMessages?: string[];
96     working?: boolean;
97     currentRoute?: string;
98     hideColumnSelector?: boolean;
99     paperProps?: PaperProps;
100     actions?: React.ReactNode;
101     hideSearchInput?: boolean;
102     title?: React.ReactNode;
103     progressBar?: React.ReactNode;
104     paperKey?: string;
105     currentItemUuid: string;
106     elementPath?: string;
107     isMSToolbarVisible: boolean;
108     checkedList: TCheckedList;
109     isNotFound: boolean;
110     searchBarValue: string;
111 }
112
113 interface DataExplorerActionProps<T> {
114     onSetColumns: (columns: DataColumns<T, any>) => void;
115     onSearch: (value: string) => void;
116     onRowClick: (item: T) => void;
117     onRowDoubleClick: (item: T) => void;
118     onColumnToggle: (column: DataColumn<T, any>) => void;
119     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: T) => void;
120     onSortToggle: (column: DataColumn<T, any>) => void;
121     onFiltersChange: (filters: DataTableFilters, column: DataColumn<T, any>) => void;
122     onChangePage: (page: number) => void;
123     onChangeRowsPerPage: (rowsPerPage: number) => void;
124     onLoadMore: (page: number) => void;
125     extractKey?: (item: T) => React.Key;
126     toggleMSToolbar: (isVisible: boolean) => void;
127     setCheckedListOnStore: (checkedList: TCheckedList) => void;
128 }
129
130 type DataExplorerProps<T> = DataExplorerDataProps<T> & DataExplorerActionProps<T> & WithStyles<CssRules> & MPVPanelProps;
131
132 export const DataExplorer = withStyles(styles)(
133     class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
134
135         multiSelectToolbarInTitle = !this.props.title && !this.props.progressBar;
136         maxItemsAvailable = 0;
137
138         componentDidMount() {
139             if (this.props.onSetColumns) {
140                 this.props.onSetColumns(this.props.columns);
141             }
142         }
143
144         componentDidUpdate( prevProps: Readonly<DataExplorerProps<T>>, prevState: Readonly<{}>, snapshot?: any ): void {
145             if (this.props.itemsAvailable !== prevProps.itemsAvailable) {
146                 this.maxItemsAvailable = Math.max(this.maxItemsAvailable, this.props.itemsAvailable);
147             }
148             if (this.props.searchBarValue !== prevProps.searchBarValue) {
149                 this.maxItemsAvailable = 0;
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                 currentRoute,
181                 title,
182                 progressBar,
183                 doHidePanel,
184                 doMaximizePanel,
185                 doUnMaximizePanel,
186                 panelName,
187                 panelMaximized,
188                 elementPath,
189                 toggleMSToolbar,
190                 setCheckedListOnStore,
191                 checkedList,
192                 working,
193             } = this.props;
194             return (
195                 <Paper
196                     className={classes.root}
197                     {...paperProps}
198                     key={paperKey}
199                     data-cy={this.props["data-cy"]}
200                 >
201                     <Grid
202                         container
203                         direction="column"
204                         wrap="nowrap"
205                         className={classes.container}
206                     >
207                         <div className={classes.titleWrapper} style={currentRoute?.includes('search-results') || !!progressBar ? {marginBottom: '-20px'} : {}}>
208                             {title && (
209                                 <Grid
210                                     item
211                                     xs
212                                     className={!!progressBar ? classes.subProcessTitle : classes.title}
213                                 >
214                                     {title}
215                                 </Grid>
216                             )}
217                             {!!progressBar && progressBar}
218                             {this.multiSelectToolbarInTitle && <MultiselectToolbar />}
219                             {(!hideColumnSelector || !hideSearchInput || !!actions) && (
220                                 <Grid
221                                     className={classes.headerMenu}
222                                     item
223                                     xs
224                                 >
225                                     <Toolbar className={classes.toolbar}>
226                                         <Grid container justify="space-between" wrap="nowrap" alignItems="center">
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
242                                                     columns={columns}
243                                                     onColumnToggle={onColumnToggle}
244                                                 />
245                                             )}
246                                         </Grid>
247                                         {doUnMaximizePanel && panelMaximized && (
248                                             <Tooltip
249                                                 title={`Unmaximize ${panelName || "panel"}`}
250                                                 disableFocusListener
251                                             >
252                                                 <IconButton onClick={doUnMaximizePanel}>
253                                                     <UnMaximizeIcon />
254                                                 </IconButton>
255                                             </Tooltip>
256                                         )}
257                                         {doMaximizePanel && !panelMaximized && (
258                                             <Tooltip
259                                                 title={`Maximize ${panelName || "panel"}`}
260                                                 disableFocusListener
261                                             >
262                                                 <IconButton onClick={doMaximizePanel}>
263                                                     <MaximizeIcon />
264                                                 </IconButton>
265                                             </Tooltip>
266                                         )}
267                                         {doHidePanel && (
268                                             <Tooltip
269                                                 title={`Close ${panelName || "panel"}`}
270                                                 disableFocusListener
271                                             >
272                                                 <IconButton
273                                                     disabled={panelMaximized}
274                                                     onClick={doHidePanel}
275                                                 >
276                                                     <CloseIcon />
277                                                 </IconButton>
278                                             </Tooltip>
279                                         )}
280                                     </Toolbar>
281                                 </Grid>
282                             )}
283                         </div>
284                         {!this.multiSelectToolbarInTitle && <MultiselectToolbar />}
285                         <Grid
286                             item
287                             xs="auto"
288                             className={classes.dataTable}
289                             style={currentRoute?.includes('search-results')  || !!progressBar ? {marginTop: '-10px'} : {}}
290                         >
291                             <DataTable
292                                 columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
293                                 items={items}
294                                 onRowClick={(_, item: T) => onRowClick(item)}
295                                 onContextMenu={onContextMenu}
296                                 onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
297                                 onFiltersChange={onFiltersChange}
298                                 onSortToggle={onSortToggle}
299                                 extractKey={extractKey}
300                                 defaultViewIcon={defaultViewIcon}
301                                 defaultViewMessages={defaultViewMessages}
302                                 currentItemUuid={currentItemUuid}
303                                 currentRoute={paperKey}
304                                 toggleMSToolbar={toggleMSToolbar}
305                                 setCheckedListOnStore={setCheckedListOnStore}
306                                 checkedList={checkedList}
307                                 working={working}
308                                 isNotFound={this.props.isNotFound}
309                             />
310                         </Grid>
311                         <Grid
312                             item
313                             xs
314                         >
315                             <Toolbar className={classes.footer}>
316                                 {elementPath && (
317                                     <Grid container>
318                                         <span data-cy="element-path">{elementPath.length > 2 ? elementPath : ''}</span>
319                                     </Grid>
320                                 )}
321                                 <Grid
322                                     container={!elementPath}
323                                     justify="flex-end"
324                                 >
325                                     {fetchMode === DataTableFetchMode.PAGINATED ? (
326                                         <TablePagination
327                                             count={itemsAvailable}
328                                             rowsPerPage={rowsPerPage}
329                                             rowsPerPageOptions={rowsPerPageOptions}
330                                             page={this.props.page}
331                                             onChangePage={this.changePage}
332                                             onChangeRowsPerPage={this.changeRowsPerPage}
333                                             // Disable next button on empty lists since that's not default behavior
334                                             nextIconButtonProps={itemsAvailable > 0 ? {} : { disabled: true }}
335                                             component="div"
336                                         />
337                                     ) : (
338                                         <Grid className={classes.loadMoreContainer}>
339                                             <Typography  className={classes.numResults}>
340                                                 Showing {items.length} / {this.maxItemsAvailable} results
341                                             </Typography>
342                                             <Button
343                                                 size="small"
344                                                 onClick={this.loadMore}
345                                                 variant="contained"
346                                                 color="primary"  
347                                                 style={{width: '100%', margin: '10px'}}
348                                                 disabled={working || items.length >= itemsAvailable}
349                                             >
350                                                 Load more
351                                             </Button>
352                                         </Grid>
353                                     )}
354                                 </Grid>
355                             </Toolbar>
356                         </Grid>
357                     </Grid>
358                 </Paper>
359             );
360         }
361
362         changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
363             this.props.onChangePage(page);
364         };
365
366         changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = event => {
367             this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
368         };
369
370         loadMore = () => {
371             this.props.onLoadMore(this.props.page + 1);
372         };
373
374         renderContextMenuTrigger = (item: T) => (
375             <Grid
376                 container
377                 justify="center"
378             >
379                 <Tooltip
380                     title="More options"
381                     disableFocusListener
382                 >
383                     <IconButton
384                         className={this.props.classes.moreOptionsButton}
385                         onClick={event => {
386                             event.stopPropagation()
387                             this.props.onContextMenu(event, item)
388                         }}
389                     >
390                         <MoreVerticalIcon />
391                     </IconButton>
392                 </Tooltip>
393             </Grid>
394         );
395
396         contextMenuColumn: DataColumn<any, any> = {
397             name: "Actions",
398             selected: true,
399             configurable: false,
400             filters: createTree(),
401             key: "context-actions",
402             render: this.renderContextMenuTrigger,
403         };
404     }
405 );