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