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