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