18549: Layout fixed, tests updated
[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     hideColumnSelector?: boolean;
70     paperProps?: PaperProps;
71     actions?: React.ReactNode;
72     hideSearchInput?: boolean;
73     title?: React.ReactNode;
74     paperKey?: string;
75     currentItemUuid: string;
76     elementPath?: string;
77 }
78
79 interface DataExplorerActionProps<T> {
80     onSetColumns: (columns: DataColumns<T>) => void;
81     onSearch: (value: string) => void;
82     onRowClick: (item: T) => void;
83     onRowDoubleClick: (item: T) => void;
84     onColumnToggle: (column: DataColumn<T>) => void;
85     onContextMenu: (event: React.MouseEvent<HTMLElement>, item: T) => void;
86     onSortToggle: (column: DataColumn<T>) => void;
87     onFiltersChange: (filters: DataTableFilters, column: DataColumn<T>) => void;
88     onChangePage: (page: number) => void;
89     onChangeRowsPerPage: (rowsPerPage: number) => void;
90     onLoadMore: (page: number) => void;
91     extractKey?: (item: T) => React.Key;
92 }
93
94 type DataExplorerProps<T> = DataExplorerDataProps<T> &
95     DataExplorerActionProps<T> & WithStyles<CssRules> & MPVPanelProps;
96
97 export const DataExplorer = withStyles(styles)(
98     class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
99
100         componentDidMount() {
101             if (this.props.onSetColumns) {
102                 this.props.onSetColumns(this.props.columns);
103             }
104         }
105
106         render() {
107             const {
108                 columns, onContextMenu, onFiltersChange, onSortToggle, working, extractKey,
109                 rowsPerPage, rowsPerPageOptions, onColumnToggle, searchLabel, searchValue, onSearch,
110                 items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
111                 dataTableDefaultView, hideColumnSelector, actions, paperProps, hideSearchInput,
112                 paperKey, fetchMode, currentItemUuid, title,
113                 doHidePanel, doMaximizePanel, panelName, panelMaximized, elementPath
114             } = this.props;
115
116             const dataCy = this.props["data-cy"];
117             return <Paper className={classes.root} {...paperProps} key={paperKey} data-cy={dataCy}>
118                 <Grid container direction="column" wrap="nowrap" className={classes.container}>
119                     <div>
120                         {title && <Grid item xs className={classes.title}>{title}</Grid>}
121                         {
122                             (!hideColumnSelector || !hideSearchInput || !!actions) &&
123                             <Grid className={classes.headerMenu} item xs>
124                                 <Toolbar className={classes.toolbar}>
125                                     <Grid container justify="space-between" wrap="nowrap" alignItems="center">
126                                         {!hideSearchInput && <div className={classes.searchBox}>
127                                             {!hideSearchInput && <SearchInput
128                                                 label={searchLabel}
129                                                 value={searchValue}
130                                                 selfClearProp={currentItemUuid}
131                                                 onSearch={onSearch} />}
132                                         </div>}
133                                         {actions}
134                                         {!hideColumnSelector && <ColumnSelector
135                                             columns={columns}
136                                             onColumnToggle={onColumnToggle} />}
137                                     </Grid>
138                                     { doMaximizePanel && !panelMaximized &&
139                                         <Tooltip title={`Maximize ${panelName || 'panel'}`} disableFocusListener>
140                                             <IconButton onClick={doMaximizePanel}><MaximizeIcon /></IconButton>
141                                         </Tooltip> }
142                                     { doHidePanel &&
143                                         <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
144                                             <IconButton onClick={doHidePanel}><CloseIcon /></IconButton>
145                                         </Tooltip> }
146                                 </Toolbar>
147                             </Grid>
148                         }
149                     </div>
150                 <Grid item xs="auto" className={classes.dataTable}><DataTable
151                     columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
152                     items={items}
153                     onRowClick={(_, item: T) => onRowClick(item)}
154                     onContextMenu={onContextMenu}
155                     onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
156                     onFiltersChange={onFiltersChange}
157                     onSortToggle={onSortToggle}
158                     extractKey={extractKey}
159                     working={working}
160                     defaultView={dataTableDefaultView}
161                     currentItemUuid={currentItemUuid}
162                     currentRoute={paperKey} /></Grid>
163                 <Grid item xs><Toolbar className={classes.footer}>
164                     {
165                         elementPath &&
166                         <Grid container>
167                             <span data-cy="element-path">
168                                 {elementPath}
169                             </span>
170                         </Grid>
171                     }
172                     <Grid container={!elementPath} justify="flex-end">
173                         {fetchMode === DataTableFetchMode.PAGINATED ? <TablePagination
174                             count={itemsAvailable}
175                             rowsPerPage={rowsPerPage}
176                             rowsPerPageOptions={rowsPerPageOptions}
177                             page={this.props.page}
178                             onChangePage={this.changePage}
179                             onChangeRowsPerPage={this.changeRowsPerPage}
180                             // Disable next button on empty lists since that's not default behavior
181                             nextIconButtonProps={(itemsAvailable > 0) ? {} : {disabled: true}}
182                             component="div" /> : <Button
183                                 variant="text"
184                                 size="medium"
185                                 onClick={this.loadMore}
186                             >Load more</Button>}
187                     </Grid>
188                 </Toolbar></Grid>
189                 </Grid>
190             </Paper>;
191         }
192
193         changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
194             this.props.onChangePage(page);
195         }
196
197         changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = (event) => {
198             this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
199         }
200
201         loadMore = () => {
202             this.props.onLoadMore(this.props.page + 1);
203         }
204
205         renderContextMenuTrigger = (item: T) =>
206             <Grid container justify="center">
207                 <Tooltip title="More options" disableFocusListener>
208                     <IconButton className={this.props.classes.moreOptionsButton} onClick={event => this.props.onContextMenu(event, item)}>
209                         <MoreOptionsIcon />
210                     </IconButton>
211                 </Tooltip>
212             </Grid>
213
214         contextMenuColumn: DataColumn<any> = {
215             name: "Actions",
216             selected: true,
217             configurable: false,
218             filters: createTree(),
219             key: "context-actions",
220             render: this.renderContextMenuTrigger
221         };
222     }
223 );