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