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