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