18203: Added test for multiple properties creation
[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' | 'headerMenu' | "toolbar" | "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     footer: {
29         overflow: 'auto'
30     },
31     root: {
32         height: '100%',
33     },
34     moreOptionsButton: {
35         padding: 0
36     },
37     title: {
38         display: 'inline-block',
39         paddingLeft: theme.spacing.unit * 3,
40         paddingTop: theme.spacing.unit * 3,
41         fontSize: '18px'
42     },
43     dataTable: {
44         height: '100%',
45         overflow: 'auto',
46     },
47     container: {
48         height: '100%',
49     },
50     headerMenu: {
51         float: 'right',
52         display: 'inline-block'
53     }
54 });
55
56 interface DataExplorerDataProps<T> {
57     fetchMode: DataTableFetchMode;
58     items: T[];
59     itemsAvailable: number;
60     columns: DataColumns<T>;
61     searchLabel?: string;
62     searchValue: string;
63     rowsPerPage: number;
64     rowsPerPageOptions: number[];
65     page: number;
66     contextMenuColumn: boolean;
67     dataTableDefaultView?: React.ReactNode;
68     working?: boolean;
69     currentRefresh?: string;
70     currentRoute?: string;
71     hideColumnSelector?: boolean;
72     paperProps?: PaperProps;
73     actions?: React.ReactNode;
74     hideSearchInput?: boolean;
75     title?: React.ReactNode;
76     paperKey?: string;
77     currentItemUuid: string;
78     elementPath?: string;
79 }
80
81 interface DataExplorerActionProps<T> {
82     onSetColumns: (columns: DataColumns<T>) => void;
83     onSearch: (value: string) => void;
84     onRowClick: (item: T) => void;
85     onRowDoubleClick: (item: T) => void;
86     onColumnToggle: (column: DataColumn<T>) => void;
87     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: T) => void;
88     onSortToggle: (column: DataColumn<T>) => void;
89     onFiltersChange: (filters: DataTableFilters, column: DataColumn<T>) => void;
90     onChangePage: (page: number) => void;
91     onChangeRowsPerPage: (rowsPerPage: number) => void;
92     onLoadMore: (page: number) => void;
93     extractKey?: (item: T) => React.Key;
94 }
95
96 type DataExplorerProps<T> = DataExplorerDataProps<T> &
97     DataExplorerActionProps<T> & WithStyles<CssRules> & MPVPanelProps;
98
99 export const DataExplorer = withStyles(styles)(
100     class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
101         state = {
102             showLoading: false,
103             prevRefresh: '',
104             prevRoute: '',
105         };
106
107         componentDidUpdate(prevProps: DataExplorerProps<T>) {
108             const currentRefresh = this.props.currentRefresh || '';
109             const currentRoute = this.props.currentRoute || '';
110
111             if (currentRoute !== this.state.prevRoute) {
112                 // Component already mounted, but the user comes from a route change,
113                 // like browsing through a project hierarchy.
114                 this.setState({
115                     showLoading: this.props.working,
116                     prevRoute: currentRoute,
117                 });
118             }
119
120             if (currentRefresh !== this.state.prevRefresh) {
121                 // Component already mounted, but the user just clicked the
122                 // refresh button.
123                 this.setState({
124                     showLoading: this.props.working,
125                     prevRefresh: currentRefresh,
126                 });
127             }
128             if (this.state.showLoading && !this.props.working) {
129                 this.setState({
130                     showLoading: false,
131                 });
132             }
133         }
134
135         componentDidMount() {
136             if (this.props.onSetColumns) {
137                 this.props.onSetColumns(this.props.columns);
138             }
139             // Component just mounted, so we need to show the loading indicator.
140             this.setState({
141                 showLoading: this.props.working,
142                 prevRefresh: this.props.currentRefresh || '',
143                 prevRoute: this.props.currentRoute || '',
144             });
145         }
146
147         render() {
148             const {
149                 columns, onContextMenu, onFiltersChange, onSortToggle, extractKey,
150                 rowsPerPage, rowsPerPageOptions, onColumnToggle, searchLabel, searchValue, onSearch,
151                 items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
152                 dataTableDefaultView, hideColumnSelector, actions, paperProps, hideSearchInput,
153                 paperKey, fetchMode, currentItemUuid, title,
154                 doHidePanel, doMaximizePanel, panelName, panelMaximized, elementPath
155             } = this.props;
156
157             return <Paper className={classes.root} {...paperProps} key={paperKey} data-cy={this.props["data-cy"]}>
158                 <Grid container direction="column" wrap="nowrap" className={classes.container}>
159                     <div>
160                         {title && <Grid item xs className={classes.title}>{title}</Grid>}
161                         {
162                             (!hideColumnSelector || !hideSearchInput || !!actions) &&
163                             <Grid className={classes.headerMenu} item xs>
164                                 <Toolbar className={classes.toolbar}>
165                                     <Grid container justify="space-between" wrap="nowrap" alignItems="center">
166                                         {!hideSearchInput && <div className={classes.searchBox}>
167                                             {!hideSearchInput && <SearchInput
168                                                 label={searchLabel}
169                                                 value={searchValue}
170                                                 selfClearProp={currentItemUuid}
171                                                 onSearch={onSearch} />}
172                                         </div>}
173                                         {actions}
174                                         {!hideColumnSelector && <ColumnSelector
175                                             columns={columns}
176                                             onColumnToggle={onColumnToggle} />}
177                                     </Grid>
178                                     { doMaximizePanel && !panelMaximized &&
179                                         <Tooltip title={`Maximize ${panelName || 'panel'}`} disableFocusListener>
180                                             <IconButton onClick={doMaximizePanel}><MaximizeIcon /></IconButton>
181                                         </Tooltip> }
182                                     { doHidePanel &&
183                                         <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
184                                             <IconButton onClick={doHidePanel}><CloseIcon /></IconButton>
185                                         </Tooltip> }
186                                 </Toolbar>
187                             </Grid>
188                         }
189                     </div>
190                 <Grid item xs="auto" className={classes.dataTable}><DataTable
191                     columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
192                     items={items}
193                     onRowClick={(_, item: T) => onRowClick(item)}
194                     onContextMenu={onContextMenu}
195                     onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
196                     onFiltersChange={onFiltersChange}
197                     onSortToggle={onSortToggle}
198                     extractKey={extractKey}
199                     working={this.state.showLoading}
200                     defaultView={dataTableDefaultView}
201                     currentItemUuid={currentItemUuid}
202                     currentRoute={paperKey} /></Grid>
203                 <Grid item xs><Toolbar className={classes.footer}>
204                     {
205                         elementPath &&
206                         <Grid container>
207                             <span data-cy="element-path">
208                                 {elementPath}
209                             </span>
210                         </Grid>
211                     }
212                     <Grid container={!elementPath} justify="flex-end">
213                         {fetchMode === DataTableFetchMode.PAGINATED ? <TablePagination
214                             count={itemsAvailable}
215                             rowsPerPage={rowsPerPage}
216                             rowsPerPageOptions={rowsPerPageOptions}
217                             page={this.props.page}
218                             onChangePage={this.changePage}
219                             onChangeRowsPerPage={this.changeRowsPerPage}
220                             // Disable next button on empty lists since that's not default behavior
221                             nextIconButtonProps={(itemsAvailable > 0) ? {} : {disabled: true}}
222                             component="div" /> : <Button
223                                 variant="text"
224                                 size="medium"
225                                 onClick={this.loadMore}
226                             >Load more</Button>}
227                     </Grid>
228                 </Toolbar></Grid>
229                 </Grid>
230             </Paper>;
231         }
232
233         changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
234             this.props.onChangePage(page);
235         }
236
237         changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = (event) => {
238             this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
239         }
240
241         loadMore = () => {
242             this.props.onLoadMore(this.props.page + 1);
243         }
244
245         renderContextMenuTrigger = (item: T) =>
246             <Grid container justify="center">
247                 <Tooltip title="More options" disableFocusListener>
248                     <IconButton className={this.props.classes.moreOptionsButton} onClick={event => this.props.onContextMenu(event, item)}>
249                         <MoreOptionsIcon />
250                     </IconButton>
251                 </Tooltip>
252             </Grid>
253
254         contextMenuColumn: DataColumn<any> = {
255             name: "Actions",
256             selected: true,
257             configurable: false,
258             filters: createTree(),
259             key: "context-actions",
260             render: this.renderContextMenuTrigger
261         };
262     }
263 );