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