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