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