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