21386: added default case for isNotFound Arvados-DCO-1.1-Signed-off-by: Lisa Knox...
[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     },
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         paddingRight: "10px",
50     },
51     subProcessTitle: {
52         display: "inline-block",
53         paddingLeft: theme.spacing.unit * 2,
54         paddingTop: theme.spacing.unit * 2,
55         fontSize: "18px",
56         flexGrow: 0,
57         paddingRight: "10px",
58     },
59     dataTable: {
60         height: "100%",
61         overflow: "auto",
62     },
63     container: {
64         height: "100%",
65     },
66     headerMenu: {
67         marginLeft: "auto",
68         flexBasis: "initial",
69         flexGrow: 0,
70     },
71 });
72
73 interface DataExplorerDataProps<T> {
74     fetchMode: DataTableFetchMode;
75     items: T[];
76     itemsAvailable: number;
77     columns: DataColumns<T, any>;
78     searchLabel?: string;
79     searchValue: string;
80     rowsPerPage: number;
81     rowsPerPageOptions: number[];
82     page: number;
83     contextMenuColumn: boolean;
84     defaultViewIcon?: IconType;
85     defaultViewMessages?: string[];
86     working?: boolean;
87     currentRefresh?: string;
88     currentRoute?: string;
89     hideColumnSelector?: boolean;
90     paperProps?: PaperProps;
91     actions?: React.ReactNode;
92     hideSearchInput?: boolean;
93     title?: React.ReactNode;
94     progressBar?: React.ReactNode;
95     paperKey?: string;
96     currentItemUuid: string;
97     elementPath?: string;
98     isMSToolbarVisible: boolean;
99     checkedList: TCheckedList;
100     isNotFound?: boolean;
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 type DataExplorerState = {
123     prevRefresh: string;
124     prevRoute: string;
125 };
126
127 export const DataExplorer = withStyles(styles)(
128     class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
129         state: DataExplorerState = {
130             prevRefresh: "",
131             prevRoute: "",
132         };
133
134         multiSelectToolbarInTitle = !this.props.title && !this.props.progressBar;
135
136         componentDidUpdate(prevProps: DataExplorerProps<T>) {
137             const currentRefresh = this.props.currentRefresh || "";
138             const currentRoute = this.props.currentRoute || "";
139
140             if (currentRoute !== this.state.prevRoute) {
141                 // Component already mounted, but the user comes from a route change,
142                 // like browsing through a project hierarchy.
143                 this.setState({
144                     prevRoute: currentRoute,
145                 });
146             }
147
148             if (currentRefresh !== this.state.prevRefresh) {
149                 // Component already mounted, but the user just clicked the
150                 // refresh button.
151                 this.setState({
152                     prevRefresh: currentRefresh,
153                 });
154             }
155         }
156
157         componentDidMount() {
158             if (this.props.onSetColumns) {
159                 this.props.onSetColumns(this.props.columns);
160             }
161             // Component just mounted, so we need to show the loading indicator.
162             this.setState({
163                 prevRefresh: this.props.currentRefresh || "",
164                 prevRoute: this.props.currentRoute || "",
165             });
166         }
167
168         render() {
169             const {
170                 columns,
171                 onContextMenu,
172                 onFiltersChange,
173                 onSortToggle,
174                 extractKey,
175                 rowsPerPage,
176                 rowsPerPageOptions,
177                 onColumnToggle,
178                 searchLabel,
179                 searchValue,
180                 onSearch,
181                 items,
182                 itemsAvailable,
183                 onRowClick,
184                 onRowDoubleClick,
185                 classes,
186                 defaultViewIcon,
187                 defaultViewMessages,
188                 hideColumnSelector,
189                 actions,
190                 paperProps,
191                 hideSearchInput,
192                 paperKey,
193                 fetchMode,
194                 currentItemUuid,
195                 currentRoute,
196                 title,
197                 progressBar,
198                 doHidePanel,
199                 doMaximizePanel,
200                 doUnMaximizePanel,
201                 panelName,
202                 panelMaximized,
203                 elementPath,
204                 toggleMSToolbar,
205                 setCheckedListOnStore,
206                 checkedList,
207                 working,
208             } = this.props;
209             return (
210                 <Paper
211                     className={classes.root}
212                     {...paperProps}
213                     key={paperKey}
214                     data-cy={this.props["data-cy"]}
215                 >
216                     <Grid
217                         container
218                         direction="column"
219                         wrap="nowrap"
220                         className={classes.container}
221                     >
222                         <div className={classes.titleWrapper} style={currentRoute?.includes('search-results') || !!progressBar ? {marginBottom: '-20px'} : {}}>
223                             {title && (
224                                 <Grid
225                                     item
226                                     xs
227                                     className={!!progressBar ? classes.subProcessTitle : classes.title}
228                                 >
229                                     {title}
230                                 </Grid>
231                             )}
232                             {!!progressBar && progressBar}
233                             {this.multiSelectToolbarInTitle && <MultiselectToolbar />}
234                             {(!hideColumnSelector || !hideSearchInput || !!actions) && (
235                                 <Grid
236                                     className={classes.headerMenu}
237                                     item
238                                     xs
239                                 >
240                                     <Toolbar className={classes.toolbar}>
241                                         <Grid container justify="space-between" wrap="nowrap" alignItems="center">
242                                             {!hideSearchInput && (
243                                                 <div className={classes.searchBox}>
244                                                     {!hideSearchInput && (
245                                                         <SearchInput
246                                                             label={searchLabel}
247                                                             value={searchValue}
248                                                             selfClearProp={""}
249                                                             onSearch={onSearch}
250                                                         />
251                                                     )}
252                                                 </div>
253                                             )}
254                                             {actions}
255                                             {!hideColumnSelector && (
256                                                 <ColumnSelector
257                                                     columns={columns}
258                                                     onColumnToggle={onColumnToggle}
259                                                 />
260                                             )}
261                                         </Grid>
262                                         {doUnMaximizePanel && panelMaximized && (
263                                             <Tooltip
264                                                 title={`Unmaximize ${panelName || "panel"}`}
265                                                 disableFocusListener
266                                             >
267                                                 <IconButton onClick={doUnMaximizePanel}>
268                                                     <UnMaximizeIcon />
269                                                 </IconButton>
270                                             </Tooltip>
271                                         )}
272                                         {doMaximizePanel && !panelMaximized && (
273                                             <Tooltip
274                                                 title={`Maximize ${panelName || "panel"}`}
275                                                 disableFocusListener
276                                             >
277                                                 <IconButton onClick={doMaximizePanel}>
278                                                     <MaximizeIcon />
279                                                 </IconButton>
280                                             </Tooltip>
281                                         )}
282                                         {doHidePanel && (
283                                             <Tooltip
284                                                 title={`Close ${panelName || "panel"}`}
285                                                 disableFocusListener
286                                             >
287                                                 <IconButton
288                                                     disabled={panelMaximized}
289                                                     onClick={doHidePanel}
290                                                 >
291                                                     <CloseIcon />
292                                                 </IconButton>
293                                             </Tooltip>
294                                         )}
295                                     </Toolbar>
296                                 </Grid>
297                             )}
298                         </div>
299                         {!this.multiSelectToolbarInTitle && <MultiselectToolbar />}
300                         <Grid
301                             item
302                             xs="auto"
303                             className={classes.dataTable}
304                             style={currentRoute?.includes('search-results')  || !!progressBar ? {marginTop: '-10px'} : {}}
305                         >
306                             <DataTable
307                                 columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
308                                 items={items}
309                                 onRowClick={(_, item: T) => onRowClick(item)}
310                                 onContextMenu={onContextMenu}
311                                 onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
312                                 onFiltersChange={onFiltersChange}
313                                 onSortToggle={onSortToggle}
314                                 extractKey={extractKey}
315                                 defaultViewIcon={defaultViewIcon}
316                                 defaultViewMessages={defaultViewMessages}
317                                 currentItemUuid={currentItemUuid}
318                                 currentRoute={paperKey}
319                                 toggleMSToolbar={toggleMSToolbar}
320                                 setCheckedListOnStore={setCheckedListOnStore}
321                                 checkedList={checkedList}
322                                 working={working}
323                                 isNotFound={this.props.isNotFound || false}
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 => {
393                             event.stopPropagation()
394                             this.props.onContextMenu(event, item)
395                         }}
396                     >
397                         <MoreVerticalIcon />
398                     </IconButton>
399                 </Tooltip>
400             </Grid>
401         );
402
403         contextMenuColumn: DataColumn<any, any> = {
404             name: "Actions",
405             selected: true,
406             configurable: false,
407             filters: createTree(),
408             key: "context-actions",
409             render: this.renderContextMenuTrigger,
410         };
411     }
412 );