15768: cleanup Arvados-DCO-1.1-Signed-off-by: Lisa Knox <lisa.knox@curii.com>
[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 { createTree } from "models/tree";
13 import { DataTableFilters } from "components/data-table-filters/data-table-filters-tree";
14 import { CloseIcon, IconType, MaximizeIcon, UnMaximizeIcon, MoreOptionsIcon } from "components/icon/icon";
15 import { PaperProps } from "@material-ui/core/Paper";
16 import { MPVPanelProps } from "components/multi-panel-view/multi-panel-view";
17 import { MultiselectToolbar } from "components/multiselect-toolbar/MultiselectToolbar";
18 import { TCheckedList } from "components/data-table/data-table";
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                     <Grid
201                         container
202                         direction="column"
203                         wrap="nowrap"
204                         className={classes.container}>
205                         <div>
206                             {title && (
207                                 <Grid
208                                     item
209                                     xs
210                                     className={classes.title}>
211                                     {title}
212                                 </Grid>
213                             )}
214                             {(!hideColumnSelector || !hideSearchInput || !!actions) && (
215                                 <Grid
216                                     className={classes.headerMenu}
217                                     item
218                                     xs>
219                                     <Toolbar className={classes.toolbar}>
220                                         {!hideSearchInput && (
221                                             <div className={classes.searchBox}>
222                                                 {!hideSearchInput && (
223                                                     <SearchInput
224                                                         label={searchLabel}
225                                                         value={searchValue}
226                                                         selfClearProp={""}
227                                                         onSearch={onSearch}
228                                                     />
229                                                 )}
230                                             </div>
231                                         )}
232                                         {actions}
233                                         {!hideColumnSelector && (
234                                             <ColumnSelector
235                                                 columns={columns}
236                                                 onColumnToggle={onColumnToggle}
237                                             />
238                                         )}
239                                         {doUnMaximizePanel && panelMaximized && (
240                                             <Tooltip
241                                                 title={`Unmaximize ${panelName || "panel"}`}
242                                                 disableFocusListener>
243                                                 <IconButton onClick={doUnMaximizePanel}>
244                                                     <UnMaximizeIcon />
245                                                 </IconButton>
246                                             </Tooltip>
247                                         )}
248                                         {doMaximizePanel && !panelMaximized && (
249                                             <Tooltip
250                                                 title={`Maximize ${panelName || "panel"}`}
251                                                 disableFocusListener>
252                                                 <IconButton onClick={doMaximizePanel}>
253                                                     <MaximizeIcon />
254                                                 </IconButton>
255                                             </Tooltip>
256                                         )}
257                                         {doHidePanel && (
258                                             <Tooltip
259                                                 title={`Close ${panelName || "panel"}`}
260                                                 disableFocusListener>
261                                                 <IconButton
262                                                     disabled={panelMaximized}
263                                                     onClick={doHidePanel}>
264                                                     <CloseIcon />
265                                                 </IconButton>
266                                             </Tooltip>
267                                         )}
268                                     </Toolbar>
269                                     <MultiselectToolbar />
270                                 </Grid>
271                             )}
272                         </div>
273                         <Grid
274                             item
275                             xs="auto"
276                             className={classes.dataTable}>
277                             <DataTable
278                                 columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
279                                 items={items}
280                                 onRowClick={(_, item: T) => onRowClick(item)}
281                                 onContextMenu={onContextMenu}
282                                 onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
283                                 onFiltersChange={onFiltersChange}
284                                 onSortToggle={onSortToggle}
285                                 extractKey={extractKey}
286                                 working={this.state.showLoading}
287                                 defaultViewIcon={defaultViewIcon}
288                                 defaultViewMessages={defaultViewMessages}
289                                 currentItemUuid={currentItemUuid}
290                                 currentRoute={paperKey}
291                                 toggleMSToolbar={toggleMSToolbar}
292                                 setCheckedListOnStore={setCheckedListOnStore}
293                                 checkedList={checkedList}
294                             />
295                         </Grid>
296                         <Grid
297                             item
298                             xs>
299                             <Toolbar className={classes.footer}>
300                                 {elementPath && (
301                                     <Grid container>
302                                         <span data-cy="element-path">{elementPath}</span>
303                                     </Grid>
304                                 )}
305                                 <Grid
306                                     container={!elementPath}
307                                     justify="flex-end">
308                                     {fetchMode === DataTableFetchMode.PAGINATED ? (
309                                         <TablePagination
310                                             count={itemsAvailable}
311                                             rowsPerPage={rowsPerPage}
312                                             rowsPerPageOptions={rowsPerPageOptions}
313                                             page={this.props.page}
314                                             onChangePage={this.changePage}
315                                             onChangeRowsPerPage={this.changeRowsPerPage}
316                                             // Disable next button on empty lists since that's not default behavior
317                                             nextIconButtonProps={itemsAvailable > 0 ? {} : { disabled: true }}
318                                             component="div"
319                                         />
320                                     ) : (
321                                         <Button
322                                             variant="text"
323                                             size="medium"
324                                             onClick={this.loadMore}>
325                                             Load more
326                                         </Button>
327                                     )}
328                                 </Grid>
329                             </Toolbar>
330                         </Grid>
331                     </Grid>
332                 </Paper>
333             );
334         }
335
336         changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
337             this.props.onChangePage(page);
338         };
339
340         changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = event => {
341             this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
342         };
343
344         loadMore = () => {
345             this.props.onLoadMore(this.props.page + 1);
346         };
347
348         renderContextMenuTrigger = (item: T) => (
349             <Grid
350                 container
351                 justify="center">
352                 <Tooltip
353                     title="More options"
354                     disableFocusListener>
355                     <IconButton
356                         className={this.props.classes.moreOptionsButton}
357                         onClick={event => this.props.onContextMenu(event, item)}>
358                         <MoreOptionsIcon />
359                     </IconButton>
360                 </Tooltip>
361             </Grid>
362         );
363
364         contextMenuColumn: DataColumn<any, any> = {
365             name: "Actions",
366             selected: true,
367             configurable: false,
368             filters: createTree(),
369             key: "context-actions",
370             render: this.renderContextMenuTrigger,
371         };
372     }
373 );