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