|
| 1 | +import React, { |
| 2 | + useCallback, |
| 3 | + useContext, |
| 4 | + useEffect, |
| 5 | + useState, |
| 6 | +} from "react"; |
| 7 | + |
| 8 | +import {SelectValue} from "@mui/base/useSelect"; |
| 9 | +import { |
| 10 | + Box, |
| 11 | + Checkbox, |
| 12 | + Chip, |
| 13 | + IconButton, |
| 14 | + ListItemContent, |
| 15 | + ListItemDecorator, |
| 16 | + Option, |
| 17 | + Select, |
| 18 | + SelectOption, |
| 19 | + Stack, |
| 20 | + Tooltip, |
| 21 | +} from "@mui/joy"; |
| 22 | + |
| 23 | +import AddIcon from "@mui/icons-material/Add"; |
| 24 | +import CloseIcon from "@mui/icons-material/Close"; |
| 25 | +import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; |
| 26 | +import RemoveIcon from "@mui/icons-material/Remove"; |
| 27 | + |
| 28 | +import {StateContext} from "../../../contexts/StateContextProvider"; |
| 29 | +import { |
| 30 | + INVALID_LOG_LEVEL_VALUE, |
| 31 | + LOG_LEVEL, |
| 32 | + LOG_LEVEL_NAMES, |
| 33 | + MAX_LOG_LEVEL, |
| 34 | +} from "../../../typings/logs"; |
| 35 | +import {range} from "../../../utils/data"; |
| 36 | +import LogLevelChip from "./LogLevelChip"; |
| 37 | + |
| 38 | +import "./index.css"; |
| 39 | + |
| 40 | + |
| 41 | +interface LogSelectOptionProps { |
| 42 | + isChecked: boolean, |
| 43 | + logLevelName: string, |
| 44 | + logLevelValue: LOG_LEVEL, |
| 45 | + onCheckboxClick: React.MouseEventHandler |
| 46 | + onOptionClick: React.MouseEventHandler |
| 47 | +} |
| 48 | + |
| 49 | +/** |
| 50 | + * Renders an <Option/> in the <LogLevelSelect/> for selecting some log level and/or the levels |
| 51 | + * above it. |
| 52 | + * |
| 53 | + * @param props |
| 54 | + * @param props.isChecked |
| 55 | + * @param props.logLevelName |
| 56 | + * @param props.logLevelValue |
| 57 | + * @param props.onCheckboxClick |
| 58 | + * @param props.onOptionClick |
| 59 | + * @return |
| 60 | + */ |
| 61 | +const LogSelectOption = ({ |
| 62 | + isChecked, |
| 63 | + logLevelName, |
| 64 | + logLevelValue, |
| 65 | + onCheckboxClick, |
| 66 | + onOptionClick, |
| 67 | +}: LogSelectOptionProps) => { |
| 68 | + return ( |
| 69 | + <Option |
| 70 | + data-value={logLevelValue} |
| 71 | + key={logLevelName} |
| 72 | + value={logLevelValue} |
| 73 | + onClick={onOptionClick} |
| 74 | + > |
| 75 | + <ListItemDecorator> |
| 76 | + <Tooltip |
| 77 | + placement={"left"} |
| 78 | + title={ |
| 79 | + <Stack |
| 80 | + alignItems={"center"} |
| 81 | + direction={"row"} |
| 82 | + > |
| 83 | + {isChecked ? |
| 84 | + <RemoveIcon/> : |
| 85 | + <AddIcon/>} |
| 86 | + {logLevelName} |
| 87 | + </Stack> |
| 88 | + } |
| 89 | + > |
| 90 | + <Checkbox |
| 91 | + checked={isChecked} |
| 92 | + size={"sm"} |
| 93 | + value={logLevelValue} |
| 94 | + onClick={onCheckboxClick}/> |
| 95 | + </Tooltip> |
| 96 | + </ListItemDecorator> |
| 97 | + <Tooltip |
| 98 | + placement={"left"} |
| 99 | + title={ |
| 100 | + <Stack |
| 101 | + alignItems={"center"} |
| 102 | + direction={"row"} |
| 103 | + > |
| 104 | + {logLevelName} |
| 105 | + {" and below"} |
| 106 | + </Stack> |
| 107 | + } |
| 108 | + > |
| 109 | + <ListItemContent> |
| 110 | + {logLevelName} |
| 111 | + </ListItemContent> |
| 112 | + </Tooltip> |
| 113 | + </Option> |
| 114 | + ); |
| 115 | +}; |
| 116 | + |
| 117 | +interface ClearFiltersOptionProps { |
| 118 | + onClick: () => void |
| 119 | +} |
| 120 | + |
| 121 | +/** |
| 122 | + * Renders an <Option/> to clear all filters in the <LogLevelSelect/>. |
| 123 | + * |
| 124 | + * @param props |
| 125 | + * @param props.onClick |
| 126 | + * @return |
| 127 | + */ |
| 128 | +const ClearFiltersOption = ({onClick}: ClearFiltersOptionProps) => ( |
| 129 | + <Option |
| 130 | + value={INVALID_LOG_LEVEL_VALUE} |
| 131 | + onClick={onClick} |
| 132 | + > |
| 133 | + <ListItemDecorator> |
| 134 | + <CloseIcon/> |
| 135 | + </ListItemDecorator> |
| 136 | + Clear filters |
| 137 | + </Option> |
| 138 | +); |
| 139 | + |
| 140 | +/** |
| 141 | + * Renders a dropdown box for selecting log levels. |
| 142 | + * |
| 143 | + * @return |
| 144 | + */ |
| 145 | +const LogLevelSelect = () => { |
| 146 | + const [selectedLogLevels, setSelectedLogLevels] = useState<LOG_LEVEL[]>([]); |
| 147 | + const {setLogLevelFilter} = useContext(StateContext); |
| 148 | + |
| 149 | + const handleRenderValue = (selected: SelectValue<SelectOption<LOG_LEVEL>, true>) => ( |
| 150 | + <Box className={"log-level-select-render-value-box"}> |
| 151 | + <Chip className={"log-level-select-render-value-box-label"}> |
| 152 | + Log Level |
| 153 | + </Chip> |
| 154 | + {selected.map((selectedOption) => ( |
| 155 | + <LogLevelChip |
| 156 | + key={selectedOption.value} |
| 157 | + name={selectedOption.label as string} |
| 158 | + value={selectedOption.value}/> |
| 159 | + ))} |
| 160 | + </Box> |
| 161 | + ); |
| 162 | + |
| 163 | + const handleCheckboxClick = useCallback((ev: React.MouseEvent<HTMLInputElement>) => { |
| 164 | + ev.preventDefault(); |
| 165 | + ev.stopPropagation(); |
| 166 | + |
| 167 | + const target = ev.target as HTMLInputElement; |
| 168 | + const value = Number(target.value) as LOG_LEVEL; |
| 169 | + let newSelectedLogLevels: LOG_LEVEL[]; |
| 170 | + if (selectedLogLevels.includes(value)) { |
| 171 | + newSelectedLogLevels = selectedLogLevels.filter((logLevel) => logLevel !== value); |
| 172 | + } else { |
| 173 | + newSelectedLogLevels = [ |
| 174 | + ...selectedLogLevels, |
| 175 | + value, |
| 176 | + ]; |
| 177 | + } |
| 178 | + setSelectedLogLevels(newSelectedLogLevels.sort((a, b) => a - b)); |
| 179 | + }, [selectedLogLevels]); |
| 180 | + |
| 181 | + const handleOptionClick = useCallback((ev: React.MouseEvent) => { |
| 182 | + const currentTarget = ev.currentTarget as HTMLElement; |
| 183 | + if ("undefined" === typeof currentTarget.dataset.value) { |
| 184 | + console.error("Unexpected undefined value for \"data-value\" attribute"); |
| 185 | + |
| 186 | + return; |
| 187 | + } |
| 188 | + |
| 189 | + const selectedValue = Number(currentTarget.dataset.value); |
| 190 | + setSelectedLogLevels(range({begin: selectedValue, end: 1 + MAX_LOG_LEVEL})); |
| 191 | + }, []); |
| 192 | + |
| 193 | + const handleSelectClearButtonClick = () => { |
| 194 | + setSelectedLogLevels([]); |
| 195 | + }; |
| 196 | + |
| 197 | + useEffect(() => { |
| 198 | + setLogLevelFilter((0 === selectedLogLevels.length ? |
| 199 | + null : |
| 200 | + selectedLogLevels)); |
| 201 | + }, [ |
| 202 | + setLogLevelFilter, |
| 203 | + selectedLogLevels, |
| 204 | + ]); |
| 205 | + |
| 206 | + return ( |
| 207 | + <Select |
| 208 | + className={"log-level-select"} |
| 209 | + multiple={true} |
| 210 | + renderValue={handleRenderValue} |
| 211 | + size={"sm"} |
| 212 | + value={selectedLogLevels} |
| 213 | + variant={"soft"} |
| 214 | + indicator={0 === selectedLogLevels.length ? |
| 215 | + <KeyboardArrowUpIcon/> : |
| 216 | + <Tooltip title={"Clear filters"}> |
| 217 | + <IconButton |
| 218 | + variant={"plain"} |
| 219 | + onClick={handleSelectClearButtonClick} |
| 220 | + > |
| 221 | + <CloseIcon/> |
| 222 | + </IconButton> |
| 223 | + </Tooltip>} |
| 224 | + placeholder={ |
| 225 | + <Chip className={"log-level-select-render-value-box-label"}> |
| 226 | + Log Level |
| 227 | + </Chip> |
| 228 | + } |
| 229 | + slotProps={{ |
| 230 | + listbox: { |
| 231 | + className: "log-level-select-listbox", |
| 232 | + placement: "top-end", |
| 233 | + }, |
| 234 | + }} |
| 235 | + > |
| 236 | + <ClearFiltersOption onClick={handleSelectClearButtonClick}/> |
| 237 | + {LOG_LEVEL_NAMES.map((logLevelName, logLevelValue) => { |
| 238 | + const checked = selectedLogLevels.includes(logLevelValue); |
| 239 | + return ( |
| 240 | + <LogSelectOption |
| 241 | + isChecked={checked} |
| 242 | + key={logLevelName} |
| 243 | + logLevelName={logLevelName} |
| 244 | + logLevelValue={logLevelValue} |
| 245 | + onCheckboxClick={handleCheckboxClick} |
| 246 | + onOptionClick={handleOptionClick}/> |
| 247 | + ); |
| 248 | + })} |
| 249 | + </Select> |
| 250 | + ); |
| 251 | +}; |
| 252 | +export default LogLevelSelect; |
0 commit comments