Merge branch '21224-project-details'
[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     currentRoute?: string;
96     hideColumnSelector?: boolean;
97     paperProps?: PaperProps;
98     actions?: React.ReactNode;
99     hideSearchInput?: boolean;
100     title?: React.ReactNode;
101     progressBar?: React.ReactNode;
102     paperKey?: string;
103     currentRouteUuid: string;
104     selectedResourceUuid: string;
105     elementPath?: string;
106     isMSToolbarVisible: boolean;
107     checkedList: TCheckedList;
108     isNotFound: boolean;
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             msToolbarInDetailsCard: true,
135         };
136
137         multiSelectToolbarInTitle = !this.props.title && !this.props.progressBar;
138
139         componentDidMount() {
140             if (this.props.onSetColumns) {
141                 this.props.onSetColumns(this.props.columns);
142             }
143         }
144
145         componentDidUpdate( prevProps: Readonly<DataExplorerProps<T>>, prevState: Readonly<{}>, snapshot?: any ): void {
146             const { selectedResourceUuid, currentRouteUuid } = this.props;
147             if(selectedResourceUuid !== prevProps.selectedResourceUuid || currentRouteUuid !== prevProps.currentRouteUuid) {
148                 this.setState({
149                     msToolbarInDetailsCard: selectedResourceUuid === this.props.currentRouteUuid,
150                 })
151             }
152         }
153
154         render() {
155             const {
156                 columns,
157                 onContextMenu,
158                 onFiltersChange,
159                 onSortToggle,
160                 extractKey,
161                 rowsPerPage,
162                 rowsPerPageOptions,
163                 onColumnToggle,
164                 searchLabel,
165                 searchValue,
166                 onSearch,
167                 items,
168                 itemsAvailable,
169                 onRowClick,
170                 onRowDoubleClick,
171                 classes,
172                 defaultViewIcon,
173                 defaultViewMessages,
174                 hideColumnSelector,
175                 actions,
176                 paperProps,
177                 hideSearchInput,
178                 paperKey,
179                 fetchMode,
180                 selectedResourceUuid,
181                 currentRoute,
182                 title,
183                 progressBar,
184                 doHidePanel,
185                 doMaximizePanel,
186                 doUnMaximizePanel,
187                 panelName,
188                 panelMaximized,
189                 elementPath,
190                 toggleMSToolbar,
191                 setCheckedListOnStore,
192                 checkedList,
193                 working,
194             } = this.props;
195             return (
196                 <Paper
197                     className={classes.root}
198                     {...paperProps}
199                     key={paperKey}
200                     data-cy={this.props["data-cy"]}
201                 >
202                     <Grid
203                         container
204                         direction="column"
205                         wrap="nowrap"
206                         className={classes.container}
207                     >
208                         <div data-cy="title-wrapper" className={classes.titleWrapper} style={currentRoute?.includes('search-results') || !!progressBar ? {marginBottom: '-20px'} : {}}>
209                             {title && (
210                                 <Grid
211                                     item
212                                     xs
213                                     className={!!progressBar ? classes.subProcessTitle : classes.title}
214                                 >
215                                     {title}
216                                 </Grid>
217                             )}
218                             {!!progressBar && progressBar}
219                             {this.multiSelectToolbarInTitle && !this.state.msToolbarInDetailsCard && <MultiselectToolbar injectedStyles={classes.msToolbarStyles} />}
220                             {(!hideColumnSelector || !hideSearchInput || !!actions) && (
221                                 <Grid
222                                     className={classes.headerMenu}
223                                     item
224                                     xs
225                                 >
226                                     <Toolbar className={classes.toolbar}>
227                                         <Grid container justify="space-between" wrap="nowrap" alignItems="center">
228                                             {!hideSearchInput && (
229                                                 <div className={classes.searchBox}>
230                                                     {!hideSearchInput && (
231                                                         <SearchInput
232                                                             label={searchLabel}
233                                                             value={searchValue}
234                                                             selfClearProp={""}
235                                                             onSearch={onSearch}
236                                                         />
237                                                     )}
238                                                 </div>
239                                             )}
240                                             {actions}
241                                             {!hideColumnSelector && (
242                                                 <ColumnSelector
243                                                     columns={columns}
244                                                     onColumnToggle={onColumnToggle}
245                                                 />
246                                             )}
247                                         </Grid>
248                                         {doUnMaximizePanel && panelMaximized && (
249                                             <Tooltip
250                                                 title={`Unmaximize ${panelName || "panel"}`}
251                                                 disableFocusListener
252                                             >
253                                                 <IconButton onClick={doUnMaximizePanel}>
254                                                     <UnMaximizeIcon />
255                                                 </IconButton>
256                                             </Tooltip>
257                                         )}
258                                         {doMaximizePanel && !panelMaximized && (
259                                             <Tooltip
260                                                 title={`Maximize ${panelName || "panel"}`}
261                                                 disableFocusListener
262                                             >
263                                                 <IconButton onClick={doMaximizePanel}>
264                                                     <MaximizeIcon />
265                                                 </IconButton>
266                                             </Tooltip>
267                                         )}
268                                         {doHidePanel && (
269                                             <Tooltip
270                                                 title={`Close ${panelName || "panel"}`}
271                                                 disableFocusListener
272                                             >
273                                                 <IconButton
274                                                     disabled={panelMaximized}
275                                                     onClick={doHidePanel}
276                                                 >
277                                                     <CloseIcon />
278                                                 </IconButton>
279                                             </Tooltip>
280                                         )}
281                                     </Toolbar>
282                                 </Grid>
283                             )}
284                         </div>
285                         {!this.multiSelectToolbarInTitle && <MultiselectToolbar isSubPanel={true} injectedStyles={classes.subpanelToolbarStyles}/>}
286                         <Grid
287                             item
288                             xs="auto"
289                             className={classes.dataTable}
290                             style={currentRoute?.includes('search-results')  || !!progressBar ? {marginTop: '-10px'} : {}}
291                         >
292                             <DataTable
293                                 columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
294                                 items={items}
295                                 onRowClick={(_, item: T) => onRowClick(item)}
296                                 onContextMenu={onContextMenu}
297                                 onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
298                                 onFiltersChange={onFiltersChange}
299                                 onSortToggle={onSortToggle}
300                                 extractKey={extractKey}
301                                 defaultViewIcon={defaultViewIcon}
302                                 defaultViewMessages={defaultViewMessages}
303                                 currentRoute={paperKey}
304                                 toggleMSToolbar={toggleMSToolbar}
305                                 setCheckedListOnStore={setCheckedListOnStore}
306                                 checkedList={checkedList}
307                                 selectedResourceUuid={selectedResourceUuid}
308                                 setSelectedUuid={this.props.setSelectedUuid}
309                                 currentRouteUuid={this.props.currentRouteUuid}
310                                 working={working}
311                                 isNotFound={this.props.isNotFound}
312                             />
313                         </Grid>
314                         <Grid
315                             item
316                             xs
317                         >
318                             <Toolbar className={classes.footer}>
319                                 {elementPath && (
320                                     <Grid container>
321                                         <span data-cy="element-path">{elementPath}</span>
322                                     </Grid>
323                                 )}
324                                 <Grid
325                                     container={!elementPath}
326                                     justify="flex-end"
327                                 >
328                                     {fetchMode === DataTableFetchMode.PAGINATED ? (
329                                         <TablePagination
330                                             count={itemsAvailable}
331                                             rowsPerPage={rowsPerPage}
332                                             rowsPerPageOptions={rowsPerPageOptions}
333                                             page={this.props.page}
334                                             onChangePage={this.changePage}
335                                             onChangeRowsPerPage={this.changeRowsPerPage}
336                                             // Disable next button on empty lists since that's not default behavior
337                                             nextIconButtonProps={itemsAvailable > 0 ? {} : { disabled: true }}
338                                             component="div"
339                                         />
340                                     ) : (
341                                         <Button
342                                             variant="text"
343                                             size="medium"
344                                             onClick={this.loadMore}
345                                         >
346                                             Load more
347                                         </Button>
348                                     )}
349                                 </Grid>
350                             </Toolbar>
351                         </Grid>
352                     </Grid>
353                 </Paper>
354             );
355         }
356
357         changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
358             this.props.onChangePage(page);
359         };
360
361         changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = event => {
362             this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
363         };
364
365         loadMore = () => {
366             this.props.onLoadMore(this.props.page + 1);
367         };
368
369         renderContextMenuTrigger = (item: T) => (
370             <Grid
371                 container
372                 justify="center"
373             >
374                 <Tooltip
375                     title="More options"
376                     disableFocusListener
377                 >
378                     <IconButton
379                         className={this.props.classes.moreOptionsButton}
380                         onClick={event => {
381                             event.stopPropagation()
382                             this.props.onContextMenu(event, item)
383                         }}
384                     >
385                         <MoreVerticalIcon />
386                     </IconButton>
387                 </Tooltip>
388             </Grid>
389         );
390
391         contextMenuColumn: DataColumn<any, any> = {
392             name: "Actions",
393             selected: true,
394             configurable: false,
395             filters: createTree(),
396             key: "context-actions",
397             render: this.renderContextMenuTrigger,
398         };
399     }
400 );