22141: Refactoring to reduce circular import dependencies
[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 { CustomStyleRulesCallback } from 'common/custom-theme';
7 import {
8     Grid,
9     Paper,
10     Toolbar,
11     TablePagination,
12     IconButton,
13     Tooltip,
14     Button,
15     Typography,
16 } from "@mui/material";
17 import { WithStyles } from '@mui/styles';
18 import withStyles from '@mui/styles/withStyles';
19 import { ColumnSelector } from "components/column-selector/column-selector";
20 import { DataColumns } from "components/data-table/data-column";
21 import { DataTable, DataTableFetchMode } from "components/data-table/data-table";
22 import { DataColumn } from "components/data-table/data-column";
23 import { SearchInput } from "components/search-input/search-input";
24 import { ArvadosTheme } from "common/custom-theme";
25 import { MultiselectToolbar } from "components/multiselect-toolbar/MultiselectToolbar";
26 import { TCheckedList } from "components/data-table/data-table";
27 import { createTree } from "models/tree";
28 import { DataTableFilters } from "components/data-table-filters/data-table-filters";
29 import { CloseIcon, IconType, MaximizeIcon, UnMaximizeIcon, MoreVerticalIcon } from "components/icon/icon";
30 import { PaperProps } from "@mui/material/Paper";
31 import { MPVPanelProps } from "components/multi-panel-view/multi-panel-view";
32 import classNames from "classnames";
33 import { InlinePulser } from "components/loading/inline-pulser";
34
35 type CssRules =
36     | 'titleWrapper'
37     | 'msToolbarStyles'
38     | 'searchBox'
39     | 'headerMenu'
40     | 'toolbar'
41     | 'footer'
42     | 'loadMoreContainer'
43     | 'numResults'
44     | 'root'
45     | 'moreOptionsButton'
46     | 'title'
47     | 'subProcessTitle'
48     | 'dataTable'
49     | 'container'
50     | 'paginationLabel'
51     | 'paginationRoot'
52     | "subToolbarWrapper"
53     | 'progressWrapper'
54     | 'progressWrapperNoTitle';
55
56 const styles: CustomStyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
57     titleWrapper: {
58         display: "flex",
59         justifyContent: "space-between",
60     },
61     msToolbarStyles: {
62         paddingTop: "0.6rem",
63     },
64     subToolbarWrapper: {
65         height: "48px",
66         paddingTop: 0,
67         marginBottom: "-20px",
68         marginTop: "-10px",
69         flexShrink: 0,
70     },
71     searchBox: {
72         paddingBottom: 0,
73     },
74     toolbar: {
75         paddingTop: 0,
76         paddingRight: theme.spacing(1),
77         paddingLeft: "10px",
78     },
79     footer: {
80         overflow: "auto",
81     },
82     loadMoreContainer: {
83         minWidth: '8rem',
84     },
85     root: {
86         height: "100%",
87         flex: 1,
88         overflowY: "auto",
89     },
90     moreOptionsButton: {
91         padding: 0,
92     },
93     numResults: {
94         marginTop: 0,
95         fontSize: "10px",
96         marginLeft: "10px",
97         marginBottom: '-0.5rem',
98         minWidth: '8.5rem',
99     },
100     title: {
101         display: "inline-block",
102         paddingLeft: theme.spacing(2),
103         paddingTop: theme.spacing(2),
104         fontSize: "18px",
105         paddingRight: "10px",
106     },
107     subProcessTitle: {
108         display: "inline-block",
109         paddingLeft: theme.spacing(2),
110         paddingTop: theme.spacing(2),
111         fontSize: "18px",
112         flexGrow: 0,
113         paddingRight: "10px",
114     },
115     progressWrapper: {
116         margin: "28px 0 0",
117         flexGrow: 1,
118         flexBasis: "100px",
119     },
120     progressWrapperNoTitle: {
121         paddingLeft: "10px",
122     },
123     dataTable: {
124         height: "100%",
125         overflowY: "auto",
126     },
127     container: {
128         height: "100%",
129     },
130     headerMenu: {
131         marginLeft: "auto",
132         flexBasis: "initial",
133         flexGrow: 0,
134     },
135     paginationLabel: {
136         margin: 0,
137         padding: 0,
138         fontSize: '0.75rem',
139     },
140     paginationRoot: {
141         fontSize: '0.75rem',
142         color: theme.palette.grey["600"],
143     },
144 });
145
146 interface DataExplorerDataProps<T> {
147     fetchMode: DataTableFetchMode;
148     items: T[];
149     itemsAvailable: number;
150     loadingItemsAvailable: boolean;
151     columns: DataColumns<T, any>;
152     searchLabel?: string;
153     searchValue: string;
154     rowsPerPage: number;
155     rowsPerPageOptions: number[];
156     page: number;
157     contextMenuColumn: boolean;
158     defaultViewIcon?: IconType;
159     defaultViewMessages?: string[];
160     working?: boolean;
161     hideColumnSelector?: boolean;
162     paperProps?: PaperProps;
163     actions?: React.ReactNode;
164     hideSearchInput?: boolean;
165     title?: React.ReactNode;
166     progressBar?: React.ReactNode;
167     path?: string;
168     currentRouteUuid: string;
169     selectedResourceUuid: string;
170     elementPath?: string;
171     isMSToolbarVisible: boolean;
172     checkedList: TCheckedList;
173     isNotFound: boolean;
174     searchBarValue: string;
175     paperClassName?: string;
176     forceMultiSelectMode?: boolean;
177 }
178
179 interface DataExplorerActionProps<T> {
180     onSetColumns: (columns: DataColumns<T, any>) => void;
181     onSearch: (value: string) => void;
182     onRowClick: (item: T) => void;
183     onRowDoubleClick: (item: T) => void;
184     onColumnToggle: (column: DataColumn<T, any>) => void;
185     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: T) => void;
186     onSortToggle: (column: DataColumn<T, any>) => void;
187     onFiltersChange: (filters: DataTableFilters, column: DataColumn<T, any>) => void;
188     onPageChange: (page: number) => void;
189     onChangeRowsPerPage: (rowsPerPage: number) => void;
190     onLoadMore: (page: number) => void;
191     extractKey?: (item: T) => React.Key;
192     toggleMSToolbar: (isVisible: boolean) => void;
193     setCheckedListOnStore: (checkedList: TCheckedList) => void;
194     setSelectedUuid: (uuid: string) => void;
195     usesDetailsCard: (uuid: string) => boolean;
196 }
197
198 type DataExplorerProps<T> = DataExplorerDataProps<T> & DataExplorerActionProps<T> & WithStyles<CssRules> & MPVPanelProps;
199
200 export const DataExplorer = withStyles(styles)(
201     class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
202         state = {
203             hideToolbar: true,
204         };
205
206         multiSelectToolbarInTitle = !this.props.title && !this.props.progressBar;
207         maxItemsAvailable = 0;
208
209         componentDidMount() {
210             if (this.props.onSetColumns) {
211                 this.props.onSetColumns(this.props.columns);
212             }
213         }
214
215         componentDidUpdate( prevProps: Readonly<DataExplorerProps<T>>, prevState: Readonly<{}>, snapshot?: any ): void {
216             const { selectedResourceUuid, currentRouteUuid, path, usesDetailsCard } = this.props;
217             if(selectedResourceUuid !== prevProps.selectedResourceUuid || currentRouteUuid !== prevProps.currentRouteUuid) {
218                 this.setState({
219                     hideToolbar: usesDetailsCard(path || '') ? selectedResourceUuid === this.props.currentRouteUuid : false,
220                 })
221             }
222             if (this.props.itemsAvailable !== prevProps.itemsAvailable) {
223                 this.maxItemsAvailable = Math.max(this.maxItemsAvailable, this.props.itemsAvailable);
224             }
225             if (this.props.searchBarValue !== prevProps.searchBarValue) {
226                 this.maxItemsAvailable = 0;
227             }
228         }
229
230         render() {
231             const {
232                 columns,
233                 onContextMenu,
234                 onFiltersChange,
235                 onSortToggle,
236                 extractKey,
237                 rowsPerPage,
238                 rowsPerPageOptions,
239                 onColumnToggle,
240                 searchLabel,
241                 searchValue,
242                 onSearch,
243                 items,
244                 itemsAvailable,
245                 loadingItemsAvailable,
246                 onRowClick,
247                 onRowDoubleClick,
248                 classes,
249                 defaultViewIcon,
250                 defaultViewMessages,
251                 hideColumnSelector,
252                 actions,
253                 paperProps,
254                 hideSearchInput,
255                 path,
256                 fetchMode,
257                 selectedResourceUuid,
258                 title,
259                 progressBar,
260                 doHidePanel,
261                 doMaximizePanel,
262                 doUnMaximizePanel,
263                 panelName,
264                 panelMaximized,
265                 elementPath,
266                 toggleMSToolbar,
267                 setCheckedListOnStore,
268                 checkedList,
269                 working,
270                 paperClassName,
271                 forceMultiSelectMode,
272             } = this.props;
273             return (
274                 <Paper
275                     className={classNames(classes.root, paperClassName)}
276                     {...paperProps}
277                     key={path}
278                     data-cy={this.props["data-cy"]}
279                 >
280                     <Grid
281                         container
282                         direction="column"
283                         wrap="nowrap"
284                         className={classes.container}
285                     >
286                         <div data-cy="title-wrapper" className={classes.titleWrapper}>
287                             {title && (
288                                 <Grid
289                                     item
290                                     xs
291                                     className={!!progressBar ? classes.subProcessTitle : classes.title}
292                                 >
293                                     {title}
294                                 </Grid>
295                             )}
296                             {!!progressBar &&
297                                 <div className={classNames({
298                                     [classes.progressWrapper]: true,
299                                     [classes.progressWrapperNoTitle]: !title,
300                                 })}>{progressBar}</div>
301                             }
302                             {this.multiSelectToolbarInTitle && !this.state.hideToolbar && <MultiselectToolbar injectedStyles={classes.msToolbarStyles} />}
303                             {(!hideColumnSelector || !hideSearchInput || !!actions) && (
304                                 <Grid
305                                     className={classes.headerMenu}
306                                     item
307                                     xs
308                                 >
309                                     <Toolbar className={classes.toolbar}>
310                                         <Grid container justifyContent="space-between" wrap="nowrap" alignItems="center">
311                                             {!hideSearchInput && (
312                                                 <div className={classes.searchBox}>
313                                                     {!hideSearchInput && (
314                                                         <SearchInput
315                                                             label={searchLabel}
316                                                             value={searchValue}
317                                                             selfClearProp={""}
318                                                             onSearch={onSearch}
319                                                         />
320                                                     )}
321                                                 </div>
322                                             )}
323                                             {actions}
324                                             {!hideColumnSelector && (
325                                                 <ColumnSelector
326                                                     columns={columns}
327                                                     onColumnToggle={onColumnToggle}
328                                                 />
329                                             )}
330                                         </Grid>
331                                         {doUnMaximizePanel && panelMaximized && (
332                                             <Tooltip
333                                                 title={`Unmaximize ${panelName || "panel"}`}
334                                                 disableFocusListener
335                                             >
336                                                 <IconButton onClick={doUnMaximizePanel} size="large">
337                                                     <UnMaximizeIcon />
338                                                 </IconButton>
339                                             </Tooltip>
340                                         )}
341                                         {doMaximizePanel && !panelMaximized && (
342                                             <Tooltip
343                                                 title={`Maximize ${panelName || "panel"}`}
344                                                 disableFocusListener
345                                             >
346                                                 <IconButton onClick={doMaximizePanel} size="large">
347                                                     <MaximizeIcon />
348                                                 </IconButton>
349                                             </Tooltip>
350                                         )}
351                                         {doHidePanel && (
352                                             <Tooltip
353                                                 title={`Close ${panelName || "panel"}`}
354                                                 disableFocusListener
355                                             >
356                                                 <IconButton disabled={panelMaximized} onClick={doHidePanel} size="large">
357                                                     <CloseIcon />
358                                                 </IconButton>
359                                             </Tooltip>
360                                         )}
361                                     </Toolbar>
362                                 </Grid>
363                             )}
364                         </div>
365                         {!this.multiSelectToolbarInTitle &&
366                             <div className={classes.subToolbarWrapper}>
367                                 {!this.state.hideToolbar && <MultiselectToolbar
368                                     forceMultiSelectMode={forceMultiSelectMode}
369                                 />}
370                             </div>
371                         }
372                         <Grid
373                             item
374                             className={classes.dataTable}
375                         >
376                             <DataTable
377                                 columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
378                                 items={items}
379                                 onRowClick={(_, item: T) => onRowClick(item)}
380                                 onContextMenu={onContextMenu}
381                                 onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
382                                 onFiltersChange={onFiltersChange}
383                                 onSortToggle={onSortToggle}
384                                 extractKey={extractKey}
385                                 defaultViewIcon={defaultViewIcon}
386                                 defaultViewMessages={defaultViewMessages}
387                                 currentRoute={path}
388                                 toggleMSToolbar={toggleMSToolbar}
389                                 setCheckedListOnStore={setCheckedListOnStore}
390                                 checkedList={checkedList}
391                                 selectedResourceUuid={selectedResourceUuid}
392                                 setSelectedUuid={this.props.setSelectedUuid}
393                                 currentRouteUuid={this.props.currentRouteUuid}
394                                 working={working}
395                                 isNotFound={this.props.isNotFound}
396                             />
397                         </Grid>
398                         <Grid
399                             item
400                             xs
401                         >
402                             <Toolbar className={classes.footer}>
403                                 {elementPath && (
404                                     <Grid container>
405                                         <span data-cy="element-path">{elementPath.length > 2 ? elementPath : ''}</span>
406                                     </Grid>
407                                 )}
408                                 <Grid
409                                     container={!elementPath}
410                                     justifyContent="flex-end"
411                                 >
412                                     {fetchMode === DataTableFetchMode.PAGINATED ? (
413                                         <TablePagination
414                                         data-cy="table-pagination"
415                                             count={itemsAvailable}
416                                             rowsPerPage={rowsPerPage}
417                                             rowsPerPageOptions={rowsPerPageOptions}
418                                             page={this.props.page}
419                                             onPageChange={this.changePage}
420                                             onRowsPerPageChange={this.changeRowsPerPage}
421                                             labelDisplayedRows={renderPaginationLabel(loadingItemsAvailable)}
422                                             nextIconButtonProps={getPaginiationButtonProps(itemsAvailable, loadingItemsAvailable)}
423                                             component="div"
424                                             classes={{
425                                                 root: classes.paginationRoot,
426                                                 selectLabel: classes.paginationLabel,
427                                                 displayedRows: classes.paginationLabel,
428                                             }}
429                                         />
430                                     ) : (
431                                         <Grid className={classes.loadMoreContainer}>
432                                             <Typography  className={classes.numResults}>
433                                                 Showing {items.length} / {this.maxItemsAvailable} results
434                                             </Typography>
435                                             <Button
436                                                 size="small"
437                                                 onClick={this.loadMore}
438                                                 variant="contained"
439                                                 color="primary"
440                                                 style={{width: '100%', margin: '10px'}}
441                                                 disabled={working || items.length >= itemsAvailable}
442                                             >
443                                                 Load more
444                                             </Button>
445                                         </Grid>
446                                     )}
447                                 </Grid>
448                             </Toolbar>
449                         </Grid>
450                     </Grid>
451                 </Paper>
452             );
453         }
454
455         changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
456             this.props.onPageChange(page);
457         };
458
459         changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = event => {
460             this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
461         };
462
463         loadMore = () => {
464             this.props.onLoadMore(this.props.page + 1);
465         };
466
467         renderContextMenuTrigger = (item: T) => (
468             <Grid
469                 container
470                 justifyContent="center"
471             >
472                 <Tooltip
473                     title="More options"
474                     disableFocusListener
475                 >
476                     <IconButton
477                         className={this.props.classes.moreOptionsButton}
478                         onClick={event => {
479                             event.stopPropagation()
480                             this.props.onContextMenu(event, item)
481                         }}
482                         size="large">
483                         <MoreVerticalIcon />
484                     </IconButton>
485                 </Tooltip>
486             </Grid>
487         );
488
489         contextMenuColumn: DataColumn<any, any> = {
490             name: "Actions",
491             selected: true,
492             configurable: false,
493             filters: createTree(),
494             key: "context-actions",
495             render: this.renderContextMenuTrigger,
496         };
497     }
498 );
499
500 const renderPaginationLabel = (loading: boolean) => ({ from, to, count }) => (
501     loading ?
502         <InlinePulser/>
503         : <>{from}-{to} of {count}</>
504 );
505
506 const getPaginiationButtonProps = (itemsAvailable: number, loading: boolean) => (
507     loading
508         ? { disabled: false } // Always allow paging while loading total
509         : itemsAvailable > 0
510             ? { }
511             : { disabled: true } // Disable next button on empty lists since that's not default behavior
512 );