19300: Adds un-maximize button to every maximizable 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: theme.spacing.unit * 2
29     },
30     toolbar: {
31         paddingTop: theme.spacing.unit,
32         paddingRight: theme.spacing.unit * 2,
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 * 3,
46         paddingTop: theme.spacing.unit * 3,
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                                     <Grid container justify="space-between" wrap="nowrap" alignItems="center">
173                                         {!hideSearchInput && <div className={classes.searchBox}>
174                                             {!hideSearchInput && <SearchInput
175                                                 label={searchLabel}
176                                                 value={searchValue}
177                                                 selfClearProp={currentItemUuid}
178                                                 onSearch={onSearch} />}
179                                         </div>}
180                                         {actions}
181                                         {!hideColumnSelector && <ColumnSelector
182                                             columns={columns}
183                                             onColumnToggle={onColumnToggle} />}
184                                     </Grid>
185                                     { doUnMaximizePanel && panelMaximized &&
186                                     <Tooltip title={`Unmaximize ${panelName || 'panel'}`} disableFocusListener>
187                                         <IconButton onClick={doUnMaximizePanel}><UnMaximizeIcon /></IconButton>
188                                     </Tooltip> }
189                                     { doMaximizePanel && !panelMaximized &&
190                                         <Tooltip title={`Maximize ${panelName || 'panel'}`} disableFocusListener>
191                                             <IconButton onClick={doMaximizePanel}><MaximizeIcon /></IconButton>
192                                         </Tooltip> }
193                                     { doHidePanel &&
194                                         <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
195                                             <IconButton onClick={doHidePanel}><CloseIcon /></IconButton>
196                                         </Tooltip> }
197                                 </Toolbar>
198                             </Grid>
199                         }
200                     </div>
201                 <Grid item xs="auto" className={classes.dataTable}><DataTable
202                     columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
203                     items={items}
204                     onRowClick={(_, item: T) => onRowClick(item)}
205                     onContextMenu={onContextMenu}
206                     onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
207                     onFiltersChange={onFiltersChange}
208                     onSortToggle={onSortToggle}
209                     extractKey={extractKey}
210                     working={this.state.showLoading}
211                     defaultViewIcon={defaultViewIcon}
212                     defaultViewMessages={defaultViewMessages}
213                     currentItemUuid={currentItemUuid}
214                     currentRoute={paperKey} /></Grid>
215                 <Grid item xs><Toolbar className={classes.footer}>
216                     {
217                         elementPath &&
218                         <Grid container>
219                             <span data-cy="element-path">
220                                 {elementPath}
221                             </span>
222                         </Grid>
223                     }
224                     <Grid container={!elementPath} justify="flex-end">
225                         {fetchMode === DataTableFetchMode.PAGINATED ? <TablePagination
226                             count={itemsAvailable}
227                             rowsPerPage={rowsPerPage}
228                             rowsPerPageOptions={rowsPerPageOptions}
229                             page={this.props.page}
230                             onChangePage={this.changePage}
231                             onChangeRowsPerPage={this.changeRowsPerPage}
232                             // Disable next button on empty lists since that's not default behavior
233                             nextIconButtonProps={(itemsAvailable > 0) ? {} : {disabled: true}}
234                             component="div" /> : <Button
235                                 variant="text"
236                                 size="medium"
237                                 onClick={this.loadMore}
238                             >Load more</Button>}
239                     </Grid>
240                 </Toolbar></Grid>
241                 </Grid>
242             </Paper>;
243         }
244
245         changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
246             this.props.onChangePage(page);
247         }
248
249         changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = (event) => {
250             this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
251         }
252
253         loadMore = () => {
254             this.props.onLoadMore(this.props.page + 1);
255         }
256
257         renderContextMenuTrigger = (item: T) =>
258             <Grid container justify="center">
259                 <Tooltip title="More options" disableFocusListener>
260                     <IconButton className={this.props.classes.moreOptionsButton} onClick={event => this.props.onContextMenu(event, item)}>
261                         <MoreOptionsIcon />
262                     </IconButton>
263                 </Tooltip>
264             </Grid>
265
266         contextMenuColumn: DataColumn<any> = {
267             name: "Actions",
268             selected: true,
269             configurable: false,
270             filters: createTree(),
271             key: "context-actions",
272             render: this.renderContextMenuTrigger
273         };
274     }
275 );