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