Merge branch '18848-upgrade-yarn3'. Closes #18848
[arvados-workbench2.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 { createTree } from 'models/tree';
13 import { DataTableFilters } from 'components/data-table-filters/data-table-filters-tree';
14 import { CloseIcon, MaximizeIcon, MoreOptionsIcon } from 'components/icon/icon';
15 import { PaperProps } from '@material-ui/core/Paper';
16 import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
17
18 type CssRules = 'searchBox' | "toolbar" | "toolbarUnderTitle" | "footer" | "root" | 'moreOptionsButton' | 'title' | 'dataTable' | 'container';
19
20 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
21     searchBox: {
22         paddingBottom: theme.spacing.unit * 2
23     },
24     toolbar: {
25         paddingTop: theme.spacing.unit,
26         paddingRight: theme.spacing.unit * 2,
27     },
28     toolbarUnderTitle: {
29         paddingTop: 0
30     },
31     footer: {
32         overflow: 'auto'
33     },
34     root: {
35         height: '100%',
36     },
37     moreOptionsButton: {
38         padding: 0
39     },
40     title: {
41         paddingLeft: theme.spacing.unit * 3,
42         paddingTop: theme.spacing.unit * 3,
43         fontSize: '18px'
44     },
45     dataTable: {
46         height: '100%',
47         overflow: 'auto',
48     },
49     container: {
50         height: '100%',
51     },
52 });
53
54 interface DataExplorerDataProps<T> {
55     fetchMode: DataTableFetchMode;
56     items: T[];
57     itemsAvailable: number;
58     columns: DataColumns<T>;
59     searchLabel?: string;
60     searchValue: string;
61     rowsPerPage: number;
62     rowsPerPageOptions: number[];
63     page: number;
64     contextMenuColumn: boolean;
65     dataTableDefaultView?: React.ReactNode;
66     working?: boolean;
67     hideColumnSelector?: boolean;
68     paperProps?: PaperProps;
69     actions?: React.ReactNode;
70     hideSearchInput?: boolean;
71     title?: React.ReactNode;
72     paperKey?: string;
73     currentItemUuid: string;
74 }
75
76 interface DataExplorerActionProps<T> {
77     onSetColumns: (columns: DataColumns<T>) => void;
78     onSearch: (value: string) => void;
79     onRowClick: (item: T) => void;
80     onRowDoubleClick: (item: T) => void;
81     onColumnToggle: (column: DataColumn<T>) => void;
82     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: T) => void;
83     onSortToggle: (column: DataColumn<T>) => void;
84     onFiltersChange: (filters: DataTableFilters, column: DataColumn<T>) => void;
85     onChangePage: (page: number) => void;
86     onChangeRowsPerPage: (rowsPerPage: number) => void;
87     onLoadMore: (page: number) => void;
88     extractKey?: (item: T) => React.Key;
89 }
90
91 type DataExplorerProps<T> = DataExplorerDataProps<T> &
92     DataExplorerActionProps<T> & WithStyles<CssRules> & MPVPanelProps;
93
94 export const DataExplorer = withStyles(styles)(
95     class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
96
97         componentDidMount() {
98             if (this.props.onSetColumns) {
99                 this.props.onSetColumns(this.props.columns);
100             }
101         }
102
103         render() {
104             const {
105                 columns, onContextMenu, onFiltersChange, onSortToggle, working, extractKey,
106                 rowsPerPage, rowsPerPageOptions, onColumnToggle, searchLabel, searchValue, onSearch,
107                 items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
108                 dataTableDefaultView, hideColumnSelector, actions, paperProps, hideSearchInput,
109                 paperKey, fetchMode, currentItemUuid, title,
110                 doHidePanel, doMaximizePanel, panelName, panelMaximized
111             } = this.props;
112
113             const dataCy = this.props["data-cy"];
114             return <Paper className={classes.root} {...paperProps} key={paperKey} data-cy={dataCy}>
115                 <Grid container direction="column" wrap="nowrap" className={classes.container}>
116                 {title && <Grid item xs className={classes.title}>{title}</Grid>}
117                 {(!hideColumnSelector || !hideSearchInput || !!actions) && <Grid item xs><Toolbar className={title ? classes.toolbarUnderTitle : classes.toolbar}>
118                     <Grid container justify="space-between" wrap="nowrap" alignItems="center">
119                         {!hideSearchInput && <div className={classes.searchBox}>
120                             {!hideSearchInput && <SearchInput
121                                 label={searchLabel}
122                                 value={searchValue}
123                                 selfClearProp={currentItemUuid}
124                                 onSearch={onSearch} />}
125                         </div>}
126                         {actions}
127                         {!hideColumnSelector && <ColumnSelector
128                             columns={columns}
129                             onColumnToggle={onColumnToggle} />}
130                     </Grid>
131                     { doMaximizePanel && !panelMaximized &&
132                         <Tooltip title={`Maximize ${panelName || 'panel'}`} disableFocusListener>
133                             <IconButton onClick={doMaximizePanel}><MaximizeIcon /></IconButton>
134                         </Tooltip> }
135                     { doHidePanel &&
136                         <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
137                             <IconButton onClick={doHidePanel}><CloseIcon /></IconButton>
138                         </Tooltip> }
139                 </Toolbar></Grid>}
140                 <Grid item xs="auto" className={classes.dataTable}><DataTable
141                     columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
142                     items={items}
143                     onRowClick={(_, item: T) => onRowClick(item)}
144                     onContextMenu={onContextMenu}
145                     onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
146                     onFiltersChange={onFiltersChange}
147                     onSortToggle={onSortToggle}
148                     extractKey={extractKey}
149                     working={working}
150                     defaultView={dataTableDefaultView}
151                     currentItemUuid={currentItemUuid}
152                     currentRoute={paperKey} /></Grid>
153                 <Grid item xs><Toolbar className={classes.footer}>
154                     <Grid container justify="flex-end">
155                         {fetchMode === DataTableFetchMode.PAGINATED ? <TablePagination
156                             count={itemsAvailable}
157                             rowsPerPage={rowsPerPage}
158                             rowsPerPageOptions={rowsPerPageOptions}
159                             page={this.props.page}
160                             onChangePage={this.changePage}
161                             onChangeRowsPerPage={this.changeRowsPerPage}
162                             // Disable next button on empty lists since that's not default behavior
163                             nextIconButtonProps={(itemsAvailable > 0) ? {} : {disabled: true}}
164                             component="div" /> : <Button
165                                 variant="text"
166                                 size="medium"
167                                 onClick={this.loadMore}
168                             >Load more</Button>}
169                     </Grid>
170                 </Toolbar></Grid>
171                 </Grid>
172             </Paper>;
173         }
174
175         changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
176             this.props.onChangePage(page);
177         }
178
179         changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = (event) => {
180             this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
181         }
182
183         loadMore = () => {
184             this.props.onLoadMore(this.props.page + 1);
185         }
186
187         renderContextMenuTrigger = (item: T) =>
188             <Grid container justify="center">
189                 <Tooltip title="More options" disableFocusListener>
190                     <IconButton className={this.props.classes.moreOptionsButton} onClick={event => this.props.onContextMenu(event, item)}>
191                         <MoreOptionsIcon />
192                     </IconButton>
193                 </Tooltip>
194             </Grid>
195
196         contextMenuColumn: DataColumn<any> = {
197             name: "Actions",
198             selected: true,
199             configurable: false,
200             filters: createTree(),
201             key: "context-actions",
202             render: this.renderContextMenuTrigger
203         };
204     }
205 );