aboutsummaryrefslogtreecommitdiffstats
path: root/webui/src/pages/list/Filter.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'webui/src/pages/list/Filter.tsx')
-rw-r--r--webui/src/pages/list/Filter.tsx116
1 files changed, 102 insertions, 14 deletions
diff --git a/webui/src/pages/list/Filter.tsx b/webui/src/pages/list/Filter.tsx
index 66702078..3559b3ce 100644
--- a/webui/src/pages/list/Filter.tsx
+++ b/webui/src/pages/list/Filter.tsx
@@ -1,13 +1,36 @@
import clsx from 'clsx';
import { LocationDescriptor } from 'history';
-import React, { useState, useRef } from 'react';
+import React, { useRef, useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem';
import { SvgIconProps } from '@material-ui/core/SvgIcon';
-import { makeStyles } from '@material-ui/core/styles';
+import TextField from '@material-ui/core/TextField';
+import { makeStyles, withStyles } from '@material-ui/core/styles';
+import { darken } from '@material-ui/core/styles/colorManipulator';
import ArrowDropDown from '@material-ui/icons/ArrowDropDown';
+import CheckIcon from '@material-ui/icons/Check';
+
+import { Color } from '../../gqlTypes';
+
+const CustomTextField = withStyles((theme) => ({
+ root: {
+ margin: '0 8px 12px 8px',
+ '& label.Mui-focused': {
+ margin: '0 2px',
+ color: theme.palette.text.secondary,
+ },
+ '& .MuiInput-underline::before': {
+ borderBottomColor: theme.palette.divider,
+ },
+ '& .MuiInput-underline::after': {
+ borderBottomColor: theme.palette.divider,
+ },
+ },
+}))(TextField);
+
+const ITEM_HEIGHT = 48;
export type Query = { [key: string]: string[] };
@@ -80,9 +103,36 @@ const useStyles = makeStyles((theme) => ({
icon: {
paddingRight: theme.spacing(0.5),
},
+ labelMenu: {
+ '& .MuiMenu-paper': {
+ //somehow using "width" won't override the default width...
+ minWidth: '35ch',
+ },
+ },
+ labelMenuItem: {
+ whiteSpace: 'normal',
+ wordBreak: 'break-word',
+ display: 'flex',
+ alignItems: 'initial',
+ },
+ labelcolor: {
+ minWidth: '0.5rem',
+ display: 'flex',
+ borderRadius: '0.25rem',
+ marginRight: '5px',
+ marginLeft: '3px',
+ },
}));
+const _rgb = (color: Color) =>
+ 'rgb(' + color.R + ',' + color.G + ',' + color.B + ')';
+
+// Create a style object from the label RGB colors
+const createStyle = (color: Color) => ({
+ backgroundColor: _rgb(color),
+ borderBottomColor: darken(_rgb(color), 0.2),
+});
-type DropdownTuple = [string, string];
+type DropdownTuple = [string, string, Color?];
type FilterDropdownProps = {
children: React.ReactNode;
@@ -90,6 +140,7 @@ type FilterDropdownProps = {
itemActive: (key: string) => boolean;
icon?: React.ComponentType<SvgIconProps>;
to: (key: string) => LocationDescriptor;
+ hasFilter?: boolean;
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
function FilterDropdown({
@@ -98,12 +149,19 @@ function FilterDropdown({
itemActive,
icon: Icon,
to,
+ hasFilter,
...props
}: FilterDropdownProps) {
const [open, setOpen] = useState(false);
+ const [filter, setFilter] = useState<string>('');
const buttonRef = useRef<HTMLButtonElement>(null);
+ const searchRef = useRef<HTMLButtonElement>(null);
const classes = useStyles({ active: false });
+ useEffect(() => {
+ searchRef && searchRef.current && searchRef.current.focus();
+ }, [filter]);
+
const content = (
<>
{Icon && <Icon fontSize="small" classes={{ root: classes.icon }} />}
@@ -123,7 +181,9 @@ function FilterDropdown({
<ArrowDropDown fontSize="small" />
</button>
<Menu
+ className={classes.labelMenu}
getContentAnchorEl={null}
+ ref={searchRef}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
@@ -135,18 +195,45 @@ function FilterDropdown({
open={open}
onClose={() => setOpen(false)}
anchorEl={buttonRef.current}
+ PaperProps={{
+ style: {
+ maxHeight: ITEM_HEIGHT * 4.5,
+ width: '25ch',
+ },
+ }}
>
- {dropdown.map(([key, value]) => (
- <MenuItem
- component={Link}
- to={to(key)}
- className={itemActive(key) ? classes.itemActive : undefined}
- onClick={() => setOpen(false)}
- key={key}
- >
- {value}
- </MenuItem>
- ))}
+ {hasFilter && (
+ <CustomTextField
+ onChange={(e) => {
+ const { value } = e.target;
+ setFilter(value);
+ }}
+ onKeyDown={(e) => e.stopPropagation()}
+ value={filter}
+ label={`Filter ${children}`}
+ />
+ )}
+ {dropdown
+ .filter((d) => d[1].toLowerCase().includes(filter.toLowerCase()))
+ .map(([key, value, color]) => (
+ <MenuItem
+ component={Link}
+ to={to(key)}
+ className={classes.labelMenuItem}
+ selected={itemActive(key)}
+ onClick={() => setOpen(false)}
+ key={key}
+ >
+ {itemActive(key) && <CheckIcon />}
+ {color && (
+ <div
+ className={classes.labelcolor}
+ style={createStyle(color)}
+ />
+ )}
+ {value}
+ </MenuItem>
+ ))}
</Menu>
</>
);
@@ -158,6 +245,7 @@ export type FilterProps = {
icon?: React.ComponentType<SvgIconProps>;
children: React.ReactNode;
};
+
function Filter({ active, to, children, icon: Icon }: FilterProps) {
const classes = useStyles();