cf14303d5af38da39304aaf701e2459c5d387369
[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         componentDidMount() {
97             if (this.props.onSetColumns) {
98                 this.props.onSetColumns(this.props.columns);
99             }
100         }
101         render() {
102             const {
103                 columns, onContextMenu, onFiltersChange, onSortToggle, working, extractKey,
104                 rowsPerPage, rowsPerPageOptions, onColumnToggle, searchLabel, searchValue, onSearch,
105                 items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
106                 dataTableDefaultView, hideColumnSelector, actions, paperProps, hideSearchInput,
107                 paperKey, fetchMode, currentItemUuid, title,
108                 doHidePanel, doMaximizePanel, panelName, panelMaximized
109             } = this.props;
110             return <Paper className={classes.root} {...paperProps} key={paperKey}>
111                 <Grid container direction="column" wrap="nowrap" className={classes.container}>
112                 {title && <Grid item xs className={classes.title}>{title}</Grid>}
113                 {(!hideColumnSelector || !hideSearchInput) && <Grid item xs><Toolbar className={title ? classes.toolbarUnderTitle : classes.toolbar}>
114                     <Grid container justify="space-between" wrap="nowrap" alignItems="center">
115                         <div className={classes.searchBox}>
116                             {!hideSearchInput && <SearchInput
117                                 label={searchLabel}
118                                 value={searchValue}
119                                 onSearch={onSearch} />}
120                         </div>
121                         {actions}
122                         {!hideColumnSelector && <ColumnSelector
123                             columns={columns}
124                             onColumnToggle={onColumnToggle} />}
125                     </Grid>
126                     { doMaximizePanel && !!!panelMaximized &&
127                         <Tooltip title={`Maximize ${panelName || 'panel'}`} disableFocusListener>
128                             <IconButton onClick={doMaximizePanel}><MaximizeIcon /></IconButton>
129                         </Tooltip> }
130                     { doHidePanel &&
131                         <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
132                             <IconButton onClick={doHidePanel}><CloseIcon /></IconButton>
133                         </Tooltip> }
134                 </Toolbar></Grid>}
135                 <Grid item xs="auto" className={classes.dataTable}><DataTable
136                     columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
137                     items={items}
138                     onRowClick={(_, item: T) => onRowClick(item)}
139                     onContextMenu={onContextMenu}
140                     onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
141                     onFiltersChange={onFiltersChange}
142                     onSortToggle={onSortToggle}
143                     extractKey={extractKey}
144                     working={working}
145                     defaultView={dataTableDefaultView}
146                     currentItemUuid={currentItemUuid}
147                     currentRoute={paperKey} /></Grid>
148                 <Grid item xs><Toolbar className={classes.footer}>
149                     <Grid container justify="flex-end">
150                         {fetchMode === DataTableFetchMode.PAGINATED ? <TablePagination
151                             count={itemsAvailable}
152                             rowsPerPage={rowsPerPage}
153                             rowsPerPageOptions={rowsPerPageOptions}
154                             page={this.props.page}
155                             onChangePage={this.changePage}
156                             onChangeRowsPerPage={this.changeRowsPerPage}
157                             // Disable next button on empty lists since that's not default behavior
158                             nextIconButtonProps={(itemsAvailable > 0) ? {} : {disabled: true}}
159                             component="div" /> : <Button
160                                 variant="text"
161                                 size="medium"
162                                 onClick={this.loadMore}
163                             >Load more</Button>}
164                     </Grid>
165                 </Toolbar></Grid>
166                 </Grid>
167             </Paper>;
168         }
169
170         changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
171             this.props.onChangePage(page);
172         }
173
174         changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = (event) => {
175             this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
176         }
177
178         loadMore = () => {
179             this.props.onLoadMore(this.props.page + 1);
180         }
181
182         renderContextMenuTrigger = (item: T) =>
183             <Grid container justify="center">
184                 <Tooltip title="More options" disableFocusListener>
185                     <IconButton className={this.props.classes.moreOptionsButton} onClick={event => this.props.onContextMenu(event, item)}>
186                         <MoreOptionsIcon />
187                     </IconButton>
188                 </Tooltip>
189             </Grid>
190
191         contextMenuColumn: DataColumn<any> = {
192             name: "Actions",
193             selected: true,
194             configurable: false,
195             filters: createTree(),
196             key: "context-actions",
197             render: this.renderContextMenuTrigger
198         };
199     }
200 );