Skip to content

Commit dc63e77

Browse files
authored
new-log-viewer: Integrate latest clp-ffi-js with support for log-level filtering; Add log-level selector to StatusBar. (#77)
1 parent ed98646 commit dc63e77

File tree

15 files changed

+429
-37
lines changed

15 files changed

+429
-37
lines changed

new-log-viewer/package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

new-log-viewer/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
"scripts": {
77
"build": "webpack --config webpack.prod.js",
88
"start": "webpack serve --open --config webpack.dev.js",
9-
109
"lint": "npm run lint:check",
1110
"lint:check": "npm-run-all --sequential --continue-on-error lint:check:*",
1211
"lint:check:css": "stylelint src/**/*.css",
@@ -30,7 +29,7 @@
3029
"@mui/icons-material": "^6.1.0",
3130
"@mui/joy": "^5.0.0-beta.48",
3231
"axios": "^1.7.2",
33-
"clp-ffi-js": "^0.1.0",
32+
"clp-ffi-js": "^0.2.0",
3433
"dayjs": "^1.11.11",
3534
"monaco-editor": "^0.50.0",
3635
"react": "^18.3.1",
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.log-level-chip {
2+
/* stylelint-disable-next-line custom-property-pattern */
3+
--Chip-radius: 0;
4+
}
5+
6+
.log-level-chip span {
7+
width: 1.4ch;
8+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import {
2+
Chip,
3+
Tooltip,
4+
} from "@mui/joy";
5+
import {DefaultColorPalette} from "@mui/joy/styles/types/colorSystem";
6+
7+
import {LOG_LEVEL} from "../../../typings/logs";
8+
9+
import "./LogLevelChip.css";
10+
11+
12+
/**
13+
* Maps log levels to colors from JoyUI's default color palette.
14+
*/
15+
const LOG_LEVEL_COLOR_MAP: Record<LOG_LEVEL, DefaultColorPalette> = Object.freeze({
16+
[LOG_LEVEL.UNKNOWN]: "neutral",
17+
[LOG_LEVEL.TRACE]: "success",
18+
[LOG_LEVEL.DEBUG]: "success",
19+
[LOG_LEVEL.INFO]: "primary",
20+
[LOG_LEVEL.WARN]: "warning",
21+
[LOG_LEVEL.ERROR]: "danger",
22+
[LOG_LEVEL.FATAL]: "danger",
23+
});
24+
25+
interface LogLevelChipProps {
26+
name: string,
27+
value: LOG_LEVEL,
28+
}
29+
30+
/**
31+
* Renders a log level chip.
32+
*
33+
* @param props
34+
* @param props.name
35+
* @param props.value
36+
* @return
37+
*/
38+
const LogLevelChip = ({name, value}: LogLevelChipProps) => (
39+
<Tooltip
40+
key={value}
41+
title={name}
42+
>
43+
<Chip
44+
className={"log-level-chip"}
45+
color={LOG_LEVEL_COLOR_MAP[value]}
46+
variant={"outlined"}
47+
>
48+
{name[0]}
49+
</Chip>
50+
</Tooltip>
51+
);
52+
53+
54+
export default LogLevelChip;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
.log-level-select {
2+
/* JoyUI has a rounding issue when calculating the listbox position, causing it to misjudge if
3+
the listbox will fit on the right side. To mitigate this, we shift the select box 1px to the
4+
left. */
5+
margin-right: 1px;
6+
}
7+
8+
.log-level-select-render-value-box {
9+
display: flex;
10+
gap: 2px;
11+
}
12+
13+
.log-level-select-render-value-box-label {
14+
/* Disable `Chip`'s background style. */
15+
background-color: initial !important;
16+
}
17+
18+
.log-level-select-listbox {
19+
/* Disallow width auto-resizing with the `Select` button. */
20+
max-width: 0;
21+
}
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
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;

new-log-viewer/src/components/StatusBar/index.css

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
bottom: 0;
44

55
display: flex;
6+
flex-wrap: wrap;
67
align-items: center;
78

89
width: 100%;

0 commit comments

Comments
 (0)