19231: Add smaller page sizes (10 and 20 items) to load faster
[arvados-workbench2.git] / src / components / data-table-filters / data-table-filters-popover.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import React, { useEffect } from "react";
6 import {
7     WithStyles,
8     withStyles,
9     ButtonBase,
10     StyleRulesCallback,
11     Theme,
12     Popover,
13     Button,
14     Card,
15     CardActions,
16     Typography,
17     CardContent,
18     Tooltip,
19     IconButton
20 } from "@material-ui/core";
21 import classnames from "classnames";
22 import { DefaultTransformOrigin } from "components/popover/helpers";
23 import { createTree } from 'models/tree';
24 import { DataTableFilters, DataTableFiltersTree } from "./data-table-filters-tree";
25 import { getNodeDescendants } from 'models/tree';
26 import debounce from "lodash/debounce";
27
28 export type CssRules = "root" | "icon" | "iconButton" | "active" | "checkbox";
29
30 const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
31     root: {
32         cursor: "pointer",
33         display: "inline-flex",
34         justifyContent: "flex-start",
35         flexDirection: "inherit",
36         alignItems: "center",
37         "&:hover": {
38             color: theme.palette.text.primary,
39         },
40         "&:focus": {
41             color: theme.palette.text.primary,
42         },
43     },
44     active: {
45         color: theme.palette.text.primary,
46         '& $iconButton': {
47             opacity: 1,
48         },
49     },
50     icon: {
51         fontSize: 12,
52         userSelect: 'none',
53         width: 16,
54         height: 15,
55         marginTop: 1
56     },
57     iconButton: {
58         color: theme.palette.text.primary,
59         opacity: 0.7,
60     },
61     checkbox: {
62         width: 24,
63         height: 24
64     }
65 });
66
67 enum SelectionMode {
68     ALL = 'all',
69     NONE = 'none'
70 }
71
72 export interface DataTableFilterProps {
73     name: string;
74     filters: DataTableFilters;
75     onChange?: (filters: DataTableFilters) => void;
76
77     /**
78      * When set to true, only one filter can be selected at a time.
79      */
80     mutuallyExclusive?: boolean;
81
82     /**
83      * By default `all` filters selection means that label should be grayed out.
84      * Use `none` when label is supposed to be grayed out when no filter is selected.
85      */
86     defaultSelection?: SelectionMode;
87 }
88
89 interface DataTableFilterState {
90     anchorEl?: HTMLElement;
91     filters: DataTableFilters;
92     prevFilters: DataTableFilters;
93 }
94
95 export const DataTableFiltersPopover = withStyles(styles)(
96     class extends React.Component<DataTableFilterProps & WithStyles<CssRules>, DataTableFilterState> {
97         state: DataTableFilterState = {
98             anchorEl: undefined,
99             filters: createTree(),
100             prevFilters: createTree(),
101         };
102         icon = React.createRef<HTMLElement>();
103
104         render() {
105             const { name, classes, defaultSelection = SelectionMode.ALL, children } = this.props;
106             const isActive = getNodeDescendants('')(this.state.filters)
107                 .some(f => defaultSelection === SelectionMode.ALL
108                     ? !f.selected
109                     : f.selected
110                 );
111             return <>
112                 <Tooltip disableFocusListener title='Filters'>
113                     <ButtonBase
114                         className={classnames([classes.root, { [classes.active]: isActive }])}
115                         component="span"
116                         onClick={this.open}
117                         disableRipple>
118                         {children}
119                         <IconButton component='span' classes={{ root: classes.iconButton }} tabIndex={-1}>
120                             <i className={classnames(["fas fa-filter", classes.icon])}
121                                 data-fa-transform="shrink-3"
122                                 ref={this.icon} />
123                         </IconButton>
124                     </ButtonBase>
125                 </Tooltip>
126                 <Popover
127                     anchorEl={this.state.anchorEl}
128                     open={!!this.state.anchorEl}
129                     anchorOrigin={DefaultTransformOrigin}
130                     transformOrigin={DefaultTransformOrigin}
131                     onClose={this.close}>
132                     <Card>
133                         <CardContent>
134                             <Typography variant="caption">
135                                 {name}
136                             </Typography>
137                         </CardContent>
138                         <DataTableFiltersTree
139                             filters={this.state.filters}
140                             mutuallyExclusive={this.props.mutuallyExclusive}
141                             onChange={this.onChange} />
142                         {this.props.mutuallyExclusive ||
143                         <CardActions>
144                             <Button
145                                 color="primary"
146                                 variant="outlined"
147                                 size="small"
148                                 onClick={this.close}>
149                                 Close
150                             </Button>
151                         </CardActions >
152                         }
153                     </Card>
154                 </Popover>
155                 <this.MountHandler />
156             </>;
157         }
158
159         static getDerivedStateFromProps(props: DataTableFilterProps, state: DataTableFilterState): DataTableFilterState {
160             return props.filters !== state.prevFilters
161                 ? { ...state, filters: props.filters, prevFilters: props.filters }
162                 : state;
163         }
164
165         open = () => {
166             this.setState({ anchorEl: this.icon.current || undefined });
167         }
168
169         onChange = (filters) => {
170             this.setState({ filters });
171             if (this.props.mutuallyExclusive) {
172                 // Mutually exclusive filters apply immediately
173                 const { onChange } = this.props;
174                 if (onChange) {
175                     onChange(filters);
176                 }
177                 this.close();
178             } else {
179                 // Non-mutually exclusive filters are debounced
180                 this.submit();
181             }
182         }
183
184         submit = debounce (() => {
185             const { onChange } = this.props;
186             if (onChange) {
187                 onChange(this.state.filters);
188             }
189         }, 1000);
190
191         MountHandler = () => {
192             useEffect(() => {
193                 return () => {
194                     this.submit.cancel();
195                 }
196             },[]);
197             return null;
198         };
199
200         close = () => {
201             this.setState(prev => ({
202                 ...prev,
203                 anchorEl: undefined
204             }));
205         }
206
207     }
208 );