Merge branch '21249-group-paging' into main. Closes #21249
[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 } 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" | "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     root: {
39         height: "100%",
40     },
41     moreOptionsButton: {
42         padding: 0,
43     },
44     title: {
45         display: "inline-block",
46         paddingLeft: theme.spacing.unit * 2,
47         paddingTop: theme.spacing.unit * 2,
48         fontSize: "18px",
49         paddingRight: "10px",
50     },
51     subProcessTitle: {
52         display: "inline-block",
53         paddingLeft: theme.spacing.unit * 2,
54         paddingTop: theme.spacing.unit * 2,
55         fontSize: "18px",
56         flexGrow: 0,
57         paddingRight: "10px",
58     },
59     dataTable: {
60         height: "100%",
61         overflow: "auto",
62     },
63     container: {
64         height: "100%",
65     },
66     headerMenu: {
67         marginLeft: "auto",
68         flexBasis: "initial",
69         flexGrow: 0,
70     },
71 });
72
73 interface DataExplorerDataProps<T> {
74     fetchMode: DataTableFetchMode;
75     items: T[];
76     itemsAvailable: number;
77     columns: DataColumns<T, any>;
78     searchLabel?: string;
79     searchValue: string;
80     rowsPerPage: number;
81     rowsPerPageOptions: number[];
82     page: number;
83     contextMenuColumn: boolean;
84     defaultViewIcon?: IconType;
85     defaultViewMessages?: string[];
86     working?: boolean;
87     currentRoute?: string;
88     hideColumnSelector?: boolean;
89     paperProps?: PaperProps;
90     actions?: React.ReactNode;
91     hideSearchInput?: boolean;
92     title?: React.ReactNode;
93     progressBar?: React.ReactNode;
94     paperKey?: string;
95     currentItemUuid: string;
96     elementPath?: string;
97     isMSToolbarVisible: boolean;
98     checkedList: TCheckedList;
99     isNotFound: boolean;
100 }
101
102 interface DataExplorerActionProps<T> {
103     onSetColumns: (columns: DataColumns<T, any>) => void;
104     onSearch: (value: string) => void;
105     onRowClick: (item: T) => void;
106     onRowDoubleClick: (item: T) => void;
107     onColumnToggle: (column: DataColumn<T, any>) => void;
108     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: T) => void;
109     onSortToggle: (column: DataColumn<T, any>) => void;
110     onFiltersChange: (filters: DataTableFilters, column: DataColumn<T, any>) => void;
111     onChangePage: (page: number) => void;
112     onChangeRowsPerPage: (rowsPerPage: number) => void;
113     onLoadMore: (page: number) => void;
114     extractKey?: (item: T) => React.Key;
115     toggleMSToolbar: (isVisible: boolean) => void;
116     setCheckedListOnStore: (checkedList: TCheckedList) => void;
117 }
118
119 type DataExplorerProps<T> = DataExplorerDataProps<T> & DataExplorerActionProps<T> & WithStyles<CssRules> & MPVPanelProps;
120
121 export const DataExplorer = withStyles(styles)(
122     class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
123
124         multiSelectToolbarInTitle = !this.props.title && !this.props.progressBar;
125
126         componentDidMount() {
127             if (this.props.onSetColumns) {
128                 this.props.onSetColumns(this.props.columns);
129             }
130         }
131
132         render() {
133             const {
134                 columns,
135                 onContextMenu,
136                 onFiltersChange,
137                 onSortToggle,
138                 extractKey,
139                 rowsPerPage,
140                 rowsPerPageOptions,
141                 onColumnToggle,
142                 searchLabel,
143                 searchValue,
144                 onSearch,
145                 items,
146                 itemsAvailable,
147                 onRowClick,
148                 onRowDoubleClick,
149                 classes,
150                 defaultViewIcon,
151                 defaultViewMessages,
152                 hideColumnSelector,
153                 actions,
154                 paperProps,
155                 hideSearchInput,
156                 paperKey,
157                 fetchMode,
158                 currentItemUuid,
159                 currentRoute,
160                 title,
161                 progressBar,
162                 doHidePanel,
163                 doMaximizePanel,
164                 doUnMaximizePanel,
165                 panelName,
166                 panelMaximized,
167                 elementPath,
168                 toggleMSToolbar,
169                 setCheckedListOnStore,
170                 checkedList,
171                 working,
172             } = this.props;
173             return (
174                 <Paper
175                     className={classes.root}
176                     {...paperProps}
177                     key={paperKey}
178                     data-cy={this.props["data-cy"]}
179                 >
180                     <Grid
181                         container
182                         direction="column"
183                         wrap="nowrap"
184                         className={classes.container}
185                     >
186                         <div className={classes.titleWrapper} style={currentRoute?.includes('search-results') || !!progressBar ? {marginBottom: '-20px'} : {}}>
187                             {title && (
188                                 <Grid
189                                     item
190                                     xs
191                                     className={!!progressBar ? classes.subProcessTitle : classes.title}
192                                 >
193                                     {title}
194                                 </Grid>
195                             )}
196                             {!!progressBar && progressBar}
197                             {this.multiSelectToolbarInTitle && <MultiselectToolbar />}
198                             {(!hideColumnSelector || !hideSearchInput || !!actions) && (
199                                 <Grid
200                                     className={classes.headerMenu}
201                                     item
202                                     xs
203                                 >
204                                     <Toolbar className={classes.toolbar}>
205                                         <Grid container justify="space-between" wrap="nowrap" alignItems="center">
206                                             {!hideSearchInput && (
207                                                 <div className={classes.searchBox}>
208                                                     {!hideSearchInput && (
209                                                         <SearchInput
210                                                             label={searchLabel}
211                                                             value={searchValue}
212                                                             selfClearProp={""}
213                                                             onSearch={onSearch}
214                                                         />
215                                                     )}
216                                                 </div>
217                                             )}
218                                             {actions}
219                                             {!hideColumnSelector && (
220                                                 <ColumnSelector
221                                                     columns={columns}
222                                                     onColumnToggle={onColumnToggle}
223                                                 />
224                                             )}
225                                         </Grid>
226                                         {doUnMaximizePanel && panelMaximized && (
227                                             <Tooltip
228                                                 title={`Unmaximize ${panelName || "panel"}`}
229                                                 disableFocusListener
230                                             >
231                                                 <IconButton onClick={doUnMaximizePanel}>
232                                                     <UnMaximizeIcon />
233                                                 </IconButton>
234                                             </Tooltip>
235                                         )}
236                                         {doMaximizePanel && !panelMaximized && (
237                                             <Tooltip
238                                                 title={`Maximize ${panelName || "panel"}`}
239                                                 disableFocusListener
240                                             >
241                                                 <IconButton onClick={doMaximizePanel}>
242                                                     <MaximizeIcon />
243                                                 </IconButton>
244                                             </Tooltip>
245                                         )}
246                                         {doHidePanel && (
247                                             <Tooltip
248                                                 title={`Close ${panelName || "panel"}`}
249                                                 disableFocusListener
250                                             >
251                                                 <IconButton
252                                                     disabled={panelMaximized}
253                                                     onClick={doHidePanel}
254                                                 >
255                                                     <CloseIcon />
256                                                 </IconButton>
257                                             </Tooltip>
258                                         )}
259                                     </Toolbar>
260                                 </Grid>
261                             )}
262                         </div>
263                         {!this.multiSelectToolbarInTitle && <MultiselectToolbar />}
264                         <Grid
265                             item
266                             xs="auto"
267                             className={classes.dataTable}
268                             style={currentRoute?.includes('search-results')  || !!progressBar ? {marginTop: '-10px'} : {}}
269                         >
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                                 defaultViewIcon={defaultViewIcon}
280                                 defaultViewMessages={defaultViewMessages}
281                                 currentItemUuid={currentItemUuid}
282                                 currentRoute={paperKey}
283                                 toggleMSToolbar={toggleMSToolbar}
284                                 setCheckedListOnStore={setCheckedListOnStore}
285                                 checkedList={checkedList}
286                                 working={working}
287                                 isNotFound={this.props.isNotFound}
288                             />
289                         </Grid>
290                         <Grid
291                             item
292                             xs
293                         >
294                             <Toolbar className={classes.footer}>
295                                 {elementPath && (
296                                     <Grid container>
297                                         <span data-cy="element-path">{elementPath}</span>
298                                     </Grid>
299                                 )}
300                                 <Grid
301                                     container={!elementPath}
302                                     justify="flex-end"
303                                 >
304                                     {fetchMode === DataTableFetchMode.PAGINATED ? (
305                                         <TablePagination
306                                             count={itemsAvailable}
307                                             rowsPerPage={rowsPerPage}
308                                             rowsPerPageOptions={rowsPerPageOptions}
309                                             page={this.props.page}
310                                             onChangePage={this.changePage}
311                                             onChangeRowsPerPage={this.changeRowsPerPage}
312                                             // Disable next button on empty lists since that's not default behavior
313                                             nextIconButtonProps={itemsAvailable > 0 ? {} : { disabled: true }}
314                                             component="div"
315                                         />
316                                     ) : (
317                                         <Button
318                                             variant="text"
319                                             size="medium"
320                                             onClick={this.loadMore}
321                                         >
322                                             Load more
323                                         </Button>
324                                     )}
325                                 </Grid>
326                             </Toolbar>
327                         </Grid>
328                     </Grid>
329                 </Paper>
330             );
331         }
332
333         changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
334             this.props.onChangePage(page);
335         };
336
337         changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = event => {
338             this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
339         };
340
341         loadMore = () => {
342             this.props.onLoadMore(this.props.page + 1);
343         };
344
345         renderContextMenuTrigger = (item: T) => (
346             <Grid
347                 container
348                 justify="center"
349             >
350                 <Tooltip
351                     title="More options"
352                     disableFocusListener
353                 >
354                     <IconButton
355                         className={this.props.classes.moreOptionsButton}
356                         onClick={event => {
357                             event.stopPropagation()
358                             this.props.onContextMenu(event, item)
359                         }}
360                     >
361                         <MoreVerticalIcon />
362                     </IconButton>
363                 </Tooltip>
364             </Grid>
365         );
366
367         contextMenuColumn: DataColumn<any, any> = {
368             name: "Actions",
369             selected: true,
370             configurable: false,
371             filters: createTree(),
372             key: "context-actions",
373             render: this.renderContextMenuTrigger,
374         };
375     }
376 );