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