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