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