-
Notifications
You must be signed in to change notification settings - Fork 16
new-log-viewer: Integrate latest clp-ffi-js with support for log-level filtering; Add log-level selector to StatusBar
.
#77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0e36cd9
c25499f
c7666e1
578f42a
62b67d5
12ecbc6
e199006
d8b68d3
6eb3ed8
d1400f4
9149712
a55c600
f2a4096
4d0f7c8
a590f2b
e35d7cc
da65862
e9e9afb
682b01d
12ef8ad
67dae77
22061bb
a60ba66
a8972f5
31c538f
288a2e8
ce6b038
5309671
1f7071d
7259067
da171dd
bfe6c0e
fae7441
91ae520
3b34d52
d7e1352
a8e9147
b8b0682
2d64c6c
4ad92f4
6d8e98a
fe1c3b3
dffcce2
03b40c0
c634ea2
b76ebef
22ab6da
7a0989f
aa064c1
ec194cd
4b412fd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
.log-level-chip { | ||
/* stylelint-disable-next-line custom-property-pattern */ | ||
--Chip-radius: 0; | ||
} | ||
|
||
.log-level-chip span { | ||
width: 1.4ch; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import { | ||
Chip, | ||
Tooltip, | ||
} from "@mui/joy"; | ||
import {DefaultColorPalette} from "@mui/joy/styles/types/colorSystem"; | ||
|
||
import {LOG_LEVEL} from "../../../typings/logs"; | ||
|
||
import "./LogLevelChip.css"; | ||
|
||
|
||
/** | ||
* Maps log levels to colors from JoyUI's default color palette. | ||
*/ | ||
const LOG_LEVEL_COLOR_MAP: Record<LOG_LEVEL, DefaultColorPalette> = Object.freeze({ | ||
[LOG_LEVEL.UNKNOWN]: "neutral", | ||
[LOG_LEVEL.TRACE]: "success", | ||
[LOG_LEVEL.DEBUG]: "success", | ||
[LOG_LEVEL.INFO]: "primary", | ||
[LOG_LEVEL.WARN]: "warning", | ||
[LOG_LEVEL.ERROR]: "danger", | ||
[LOG_LEVEL.FATAL]: "danger", | ||
}); | ||
|
||
interface LogLevelChipProps { | ||
name: string, | ||
value: LOG_LEVEL, | ||
} | ||
|
||
/** | ||
* Renders a log level chip. | ||
* | ||
* @param props | ||
* @param props.name | ||
* @param props.value | ||
* @return | ||
*/ | ||
const LogLevelChip = ({name, value}: LogLevelChipProps) => ( | ||
<Tooltip | ||
key={value} | ||
title={name} | ||
> | ||
<Chip | ||
className={"log-level-chip"} | ||
color={LOG_LEVEL_COLOR_MAP[value]} | ||
variant={"outlined"} | ||
> | ||
{name[0]} | ||
</Chip> | ||
</Tooltip> | ||
); | ||
|
||
|
||
export default LogLevelChip; | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
.log-level-select { | ||
/* JoyUI has a rounding issue when calculating the listbox position, causing it to misjudge if | ||
the listbox will fit on the right side. To mitigate this, we shift the select box 1px to the | ||
left. */ | ||
margin-right: 1px; | ||
} | ||
|
||
.log-level-select-render-value-box { | ||
display: flex; | ||
gap: 2px; | ||
} | ||
|
||
.log-level-select-render-value-box-label { | ||
/* Disable `Chip`'s background style. */ | ||
background-color: initial !important; | ||
junhaoliao marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
.log-level-select-listbox { | ||
/* Disallow width auto-resizing with the `Select` button. */ | ||
max-width: 0; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,252 @@ | ||
import React, { | ||
useCallback, | ||
useContext, | ||
useEffect, | ||
useState, | ||
} from "react"; | ||
|
||
import {SelectValue} from "@mui/base/useSelect"; | ||
import { | ||
Box, | ||
Checkbox, | ||
Chip, | ||
IconButton, | ||
ListItemContent, | ||
ListItemDecorator, | ||
Option, | ||
Select, | ||
SelectOption, | ||
Stack, | ||
Tooltip, | ||
} from "@mui/joy"; | ||
|
||
import AddIcon from "@mui/icons-material/Add"; | ||
import CloseIcon from "@mui/icons-material/Close"; | ||
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; | ||
import RemoveIcon from "@mui/icons-material/Remove"; | ||
|
||
import {StateContext} from "../../../contexts/StateContextProvider"; | ||
import { | ||
INVALID_LOG_LEVEL_VALUE, | ||
LOG_LEVEL, | ||
LOG_LEVEL_NAMES, | ||
MAX_LOG_LEVEL, | ||
} from "../../../typings/logs"; | ||
import {range} from "../../../utils/data"; | ||
import LogLevelChip from "./LogLevelChip"; | ||
|
||
import "./index.css"; | ||
|
||
|
||
interface LogSelectOptionProps { | ||
isChecked: boolean, | ||
logLevelName: string, | ||
logLevelValue: LOG_LEVEL, | ||
onCheckboxClick: React.MouseEventHandler | ||
onOptionClick: React.MouseEventHandler | ||
} | ||
|
||
/** | ||
* Renders an <Option/> in the <LogLevelSelect/> for selecting some log level and/or the levels | ||
* above it. | ||
* | ||
* @param props | ||
* @param props.isChecked | ||
* @param props.logLevelName | ||
* @param props.logLevelValue | ||
* @param props.onCheckboxClick | ||
* @param props.onOptionClick | ||
* @return | ||
*/ | ||
const LogSelectOption = ({ | ||
isChecked, | ||
logLevelName, | ||
logLevelValue, | ||
onCheckboxClick, | ||
onOptionClick, | ||
}: LogSelectOptionProps) => { | ||
return ( | ||
<Option | ||
data-value={logLevelValue} | ||
key={logLevelName} | ||
value={logLevelValue} | ||
onClick={onOptionClick} | ||
> | ||
<ListItemDecorator> | ||
<Tooltip | ||
placement={"left"} | ||
title={ | ||
<Stack | ||
alignItems={"center"} | ||
direction={"row"} | ||
> | ||
{isChecked ? | ||
<RemoveIcon/> : | ||
<AddIcon/>} | ||
{logLevelName} | ||
</Stack> | ||
} | ||
> | ||
<Checkbox | ||
checked={isChecked} | ||
size={"sm"} | ||
value={logLevelValue} | ||
onClick={onCheckboxClick}/> | ||
</Tooltip> | ||
</ListItemDecorator> | ||
<Tooltip | ||
placement={"left"} | ||
title={ | ||
<Stack | ||
alignItems={"center"} | ||
direction={"row"} | ||
> | ||
{logLevelName} | ||
{" and below"} | ||
</Stack> | ||
} | ||
> | ||
<ListItemContent> | ||
{logLevelName} | ||
</ListItemContent> | ||
</Tooltip> | ||
</Option> | ||
); | ||
}; | ||
Comment on lines
+41
to
+115
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Enhance accessibility and optimize performance of LogSelectOption The LogSelectOption component is well-structured, but consider the following improvements:
Here's a suggested implementation: import React, { memo } from 'react';
const LogSelectOption = memo(({
isChecked,
logLevelName,
logLevelValue,
onCheckboxClick,
onOptionClick,
}: LogSelectOptionProps) => {
return (
<Option
data-value={logLevelValue}
key={logLevelName}
value={logLevelValue}
onClick={onOptionClick}
>
<ListItemDecorator>
<Tooltip
placement="left"
title={
<Stack alignItems="center" direction="row">
{isChecked ? <RemoveIcon /> : <AddIcon />}
{logLevelName}
</Stack>
}
>
<Checkbox
checked={isChecked}
size="sm"
value={logLevelValue}
onClick={onCheckboxClick}
aria-label={`Select ${logLevelName} log level`}
/>
</Tooltip>
</ListItemDecorator>
<Tooltip
placement="left"
title={
<Stack alignItems="center" direction="row">
{logLevelName} and below
</Stack>
}
>
<ListItemContent>{logLevelName}</ListItemContent>
</Tooltip>
</Option>
);
});
LogSelectOption.displayName = 'LogSelectOption'; These changes will improve accessibility for screen reader users and potentially boost performance by reducing unnecessary re-renders. |
||
|
||
interface ClearFiltersOptionProps { | ||
onClick: () => void | ||
} | ||
|
||
/** | ||
* Renders an <Option/> to clear all filters in the <LogLevelSelect/>. | ||
* | ||
* @param props | ||
* @param props.onClick | ||
* @return | ||
*/ | ||
const ClearFiltersOption = ({onClick}: ClearFiltersOptionProps) => ( | ||
<Option | ||
value={INVALID_LOG_LEVEL_VALUE} | ||
onClick={onClick} | ||
> | ||
<ListItemDecorator> | ||
<CloseIcon/> | ||
</ListItemDecorator> | ||
Clear filters | ||
</Option> | ||
); | ||
|
||
/** | ||
* Renders a dropdown box for selecting log levels. | ||
* | ||
* @return | ||
*/ | ||
const LogLevelSelect = () => { | ||
const [selectedLogLevels, setSelectedLogLevels] = useState<LOG_LEVEL[]>([]); | ||
const {setLogLevelFilter} = useContext(StateContext); | ||
|
||
const handleRenderValue = (selected: SelectValue<SelectOption<LOG_LEVEL>, true>) => ( | ||
<Box className={"log-level-select-render-value-box"}> | ||
<Chip className={"log-level-select-render-value-box-label"}> | ||
Log Level | ||
</Chip> | ||
{selected.map((selectedOption) => ( | ||
<LogLevelChip | ||
key={selectedOption.value} | ||
name={selectedOption.label as string} | ||
value={selectedOption.value}/> | ||
))} | ||
</Box> | ||
); | ||
|
||
const handleCheckboxClick = useCallback((ev: React.MouseEvent<HTMLInputElement>) => { | ||
ev.preventDefault(); | ||
ev.stopPropagation(); | ||
|
||
const target = ev.target as HTMLInputElement; | ||
const value = Number(target.value) as LOG_LEVEL; | ||
let newSelectedLogLevels: LOG_LEVEL[]; | ||
if (selectedLogLevels.includes(value)) { | ||
newSelectedLogLevels = selectedLogLevels.filter((logLevel) => logLevel !== value); | ||
} else { | ||
newSelectedLogLevels = [ | ||
...selectedLogLevels, | ||
value, | ||
]; | ||
} | ||
setSelectedLogLevels(newSelectedLogLevels.sort((a, b) => a - b)); | ||
}, [selectedLogLevels]); | ||
Comment on lines
+163
to
+179
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Optimize state updates in handleCheckboxClick Consider using the functional update form of const handleCheckboxClick = useCallback((ev: React.MouseEvent<HTMLInputElement>) => {
ev.preventDefault();
ev.stopPropagation();
const target = ev.target as HTMLInputElement;
const value = Number(target.value) as LOG_LEVEL;
setSelectedLogLevels(prevSelected => {
const newSelected = prevSelected.includes(value)
? prevSelected.filter(logLevel => logLevel !== value)
: [...prevSelected, value];
return newSelected.sort((a, b) => a - b);
});
}, []); This approach ensures that we're always working with the most up-to-date state and reduces the risk of stale state issues. |
||
|
||
const handleOptionClick = useCallback((ev: React.MouseEvent) => { | ||
const currentTarget = ev.currentTarget as HTMLElement; | ||
if ("undefined" === typeof currentTarget.dataset.value) { | ||
console.error("Unexpected undefined value for \"data-value\" attribute"); | ||
|
||
return; | ||
} | ||
|
||
const selectedValue = Number(currentTarget.dataset.value); | ||
setSelectedLogLevels(range({begin: selectedValue, end: 1 + MAX_LOG_LEVEL})); | ||
}, []); | ||
Comment on lines
+181
to
+191
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Improve error handling in handleOptionClick The current error handling in const handleOptionClick = useCallback((ev: React.MouseEvent) => {
const currentTarget = ev.currentTarget as HTMLElement;
const dataValue = currentTarget.dataset.value;
if (typeof dataValue === 'undefined') {
throw new Error("Unexpected undefined value for 'data-value' attribute");
}
const selectedValue = Number(dataValue);
setSelectedLogLevels(range({begin: selectedValue, end: 1 + MAX_LOG_LEVEL}));
}, []); This change ensures that errors are caught and can be handled appropriately by an error boundary or other error handling mechanism in your application. |
||
|
||
const handleSelectClearButtonClick = () => { | ||
setSelectedLogLevels([]); | ||
}; | ||
|
||
useEffect(() => { | ||
setLogLevelFilter((0 === selectedLogLevels.length ? | ||
null : | ||
selectedLogLevels)); | ||
}, [ | ||
setLogLevelFilter, | ||
selectedLogLevels, | ||
]); | ||
|
||
return ( | ||
<Select | ||
className={"log-level-select"} | ||
multiple={true} | ||
renderValue={handleRenderValue} | ||
size={"sm"} | ||
value={selectedLogLevels} | ||
variant={"soft"} | ||
indicator={0 === selectedLogLevels.length ? | ||
<KeyboardArrowUpIcon/> : | ||
<Tooltip title={"Clear filters"}> | ||
<IconButton | ||
variant={"plain"} | ||
onClick={handleSelectClearButtonClick} | ||
> | ||
<CloseIcon/> | ||
</IconButton> | ||
</Tooltip>} | ||
placeholder={ | ||
<Chip className={"log-level-select-render-value-box-label"}> | ||
Log Level | ||
</Chip> | ||
} | ||
slotProps={{ | ||
listbox: { | ||
className: "log-level-select-listbox", | ||
placement: "top-end", | ||
}, | ||
}} | ||
> | ||
<ClearFiltersOption onClick={handleSelectClearButtonClick}/> | ||
{LOG_LEVEL_NAMES.map((logLevelName, logLevelValue) => { | ||
const checked = selectedLogLevels.includes(logLevelValue); | ||
return ( | ||
<LogSelectOption | ||
isChecked={checked} | ||
key={logLevelName} | ||
logLevelName={logLevelName} | ||
logLevelValue={logLevelValue} | ||
onCheckboxClick={handleCheckboxClick} | ||
onOptionClick={handleOptionClick}/> | ||
); | ||
})} | ||
</Select> | ||
); | ||
Comment on lines
+206
to
+250
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Consider performance optimization for Select component The Select component is re-rendering on every state change. To optimize performance, consider memoizing the options: const memoizedOptions = useMemo(() => (
LOG_LEVEL_NAMES.map((logLevelName, logLevelValue) => {
const checked = selectedLogLevels.includes(logLevelValue);
return (
<LogSelectOption
isChecked={checked}
key={logLevelName}
logLevelName={logLevelName}
logLevelValue={logLevelValue}
onCheckboxClick={handleCheckboxClick}
onOptionClick={handleOptionClick}
/>
);
})
), [selectedLogLevels, handleCheckboxClick, handleOptionClick]);
// In the return statement
{memoizedOptions} This optimization prevents unnecessary re-renders of the options when the selected log levels haven't changed. |
||
}; | ||
export default LogLevelSelect; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
bottom: 0; | ||
|
||
display: flex; | ||
flex-wrap: wrap; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is to avoid the individual components in the status bar squeezing themselves when the browser window is too narrow. One may not like that the components are wrapped outside of the viewport so that a scroll-down becomes necessary to access the wrapped components. If so, an issue can be submitted to keep track of the behaviour and we can look for a better solution in the future. |
||
align-items: center; | ||
|
||
width: 100%; | ||
|
Uh oh!
There was an error while loading. Please reload this page.