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