Skip to content

Commit

Permalink
Add log level selector component to StatusBar.
Browse files Browse the repository at this point in the history
  • Loading branch information
junhaoliao committed Sep 26, 2024
1 parent d185624 commit 0e36cd9
Show file tree
Hide file tree
Showing 8 changed files with 239 additions and 0 deletions.
1 change: 1 addition & 0 deletions new-log-viewer/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" crossorigin href="https://fonts.gstatic.com">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto&display=swap">
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap">
</head>
<body>
<div id="root"></div>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.log-level-chip {
/* stylelint-disable-next-line custom-property-pattern */
--Chip-radius: 0;

font-family: "Roboto Mono", monospace !important;
font-weight: 600 !important;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from "react";

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 with colors from JoyUI's default color palette.
*/
const LOG_LEVEL_COLOR_MAP: Record<LOG_LEVEL, DefaultColorPalette> = Object.freeze({
[LOG_LEVEL.NONE]: "neutral",
[LOG_LEVEL.TRACE]: "neutral",
[LOG_LEVEL.DEBUG]: "neutral",
[LOG_LEVEL.INFO]: "primary",
[LOG_LEVEL.WARN]: "warning",
[LOG_LEVEL.ERROR]: "danger",
[LOG_LEVEL.FATAL]: "danger",
});

interface LogLevelChipProps {
name: string,
value: LOG_LEVEL,

onSelectedLogLevelsChange: (func: (value: LOG_LEVEL[]) => LOG_LEVEL[]) => void,
}

/**
* Renders a log level chip.
*
* @param props
* @param props.name
* @param props.value
* @param props.onSelectedLogLevelsChange Callback to handle changes to selected log levels.
* @return
*/
const LogLevelChip = ({name, value, onSelectedLogLevelsChange}: LogLevelChipProps) => {
const handleChipClick = (ev: React.MouseEvent<HTMLButtonElement>) => {
ev.stopPropagation();
onSelectedLogLevelsChange((oldValue) => oldValue.filter((v) => v !== value));
};

return (
<Tooltip
color={LOG_LEVEL_COLOR_MAP[value]}
key={value}
title={name}
>
<Chip
className={"log-level-chip"}
color={LOG_LEVEL_COLOR_MAP[value]}
variant={"outlined"}
onClick={handleChipClick}
>
{name[0]}
</Chip>
</Tooltip>
);
};
export default LogLevelChip;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.log-level-select-render-value-box {
display: flex;
gap: 2px;
}
102 changes: 102 additions & 0 deletions new-log-viewer/src/components/StatusBar/LogLevelSelect/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import React, {
useCallback,
useState,
} from "react";

import {SelectValue} from "@mui/base/useSelect";
import {
Box,
IconButton,
MenuItem,
Option,
Select,
SelectOption,
} from "@mui/joy";

import CloseRoundedIcon from "@mui/icons-material/CloseRounded";
import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";

import {
LOG_LEVEL,
LOG_LEVEL_NAMES,
MAX_LOG_LEVEL,
} from "../../../typings/logs";
import {range} from "../../../utils/data";
import LogLevelChip from "./LogLevelChip";

import "./index.css";


/**
* Renders a dropdown box for selecting log levels.
*
* @return
*/
const LogLevelSelect = () => {
const [selectedLogLevels, setSelectedLogLevels] = useState<LOG_LEVEL[]>([]);

const handleRenderValue = (selected: SelectValue<SelectOption<LOG_LEVEL>, true>) => (
<Box className={"log-level-select-render-value-box"}>
{selected.map((selectedOption) => (
<LogLevelChip
key={selectedOption.value}
name={selectedOption.label as string}
value={selectedOption.value}
onSelectedLogLevelsChange={setSelectedLogLevels}/>
))}
</Box>
);

const handleSelectChange = useCallback((
_: React.MouseEvent | React.KeyboardEvent | React.FocusEvent | null,
newValue: SelectValue<LOG_LEVEL, true>
) => {
if (0 === selectedLogLevels.length) {
const [singleSelectValue] = newValue;
setSelectedLogLevels(range(singleSelectValue as number, 1 + MAX_LOG_LEVEL));
} else {
setSelectedLogLevels(newValue.sort());
}
}, [selectedLogLevels]);

const handleSelectClearButtonClick = () => {
handleSelectChange(null, []);
};

const handleSelectClearButtonMouseDown = (ev: React.MouseEvent<HTMLButtonElement>) => {
ev.stopPropagation();
};

return (
<Select
multiple={true}
placeholder={"Log Level"}
renderValue={handleRenderValue}
size={"sm"}
value={selectedLogLevels}
variant={"soft"}
indicator={0 === selectedLogLevels.length ?
<KeyboardArrowUpIcon/> :
<IconButton
variant={"plain"}
onClick={handleSelectClearButtonClick}
onMouseDown={handleSelectClearButtonMouseDown}
>
<CloseRoundedIcon/>
</IconButton>}
onChange={handleSelectChange}
>
{/* Add a dummy MenuItem to avoid the first Option receiving focus. */}
<MenuItem sx={{display: "none"}}/>
{LOG_LEVEL_NAMES.map((logLevelName, index) => (
<Option
key={logLevelName}
value={index}
>
{logLevelName}
</Option>
))}
</Select>
);
};
export default LogLevelSelect;
2 changes: 2 additions & 0 deletions new-log-viewer/src/components/StatusBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
copyPermalinkToClipboard,
UrlContext,
} from "../../contexts/UrlContextProvider";
import LogLevelSelect from "./LogLevelSelect";

import "./index.css";

Expand Down Expand Up @@ -37,6 +38,7 @@ const StatusBar = () => {
>
Status message
</Typography>
<LogLevelSelect/>
<Button
size={"sm"}
onClick={handleCopyLinkButtonClick}
Expand Down
19 changes: 19 additions & 0 deletions new-log-viewer/src/typings/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,28 @@ enum LOG_LEVEL {
FATAL
}

/**
* Key names in enum `LOG_LEVEL`.
*/
const LOG_LEVEL_NAMES = Object.freeze(
Object.values(LOG_LEVEL).filter((value) => "string" === typeof value)
);

/**
* Values in enum `LOG_LEVEL`.
*/
const LOG_LEVEL_VALUES = Object.freeze(
Object.values(LOG_LEVEL).filter((value) => "number" === typeof value)
);

const MAX_LOG_LEVEL = Math.max(...LOG_LEVEL_VALUES);


const INVALID_TIMESTAMP_VALUE = 0;

export {
INVALID_TIMESTAMP_VALUE,
LOG_LEVEL,
LOG_LEVEL_NAMES,
MAX_LOG_LEVEL,
};
38 changes: 38 additions & 0 deletions new-log-viewer/src/utils/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,45 @@ const getMapValueWithNearestLessThanOrEqualKey = <T>(
map.get(lowerBoundKey) as T;
};

/**
* Creates an array of numbers in a specified range [startNum, startNum + steps].
*
* @param start The value of the start parameter (or `0` if the parameter was not supplied).
* @param stop The value of the stop parameter.
* @param step The value of the step parameter (or `1` if the parameter was not supplied).
* - For a positive step, the contents of a range r are determined by the formula `r[i] = start +
* step*i` where `i >= 0` and `r[i] < stop`.
* - For a negative step, the contents of the range are still determined by the formula `r[i] =
* start + step*i`, but the constraints are `i >= 0` and `r[i] > stop`.
* @return An array of numbers from `start` to `stop` (exclusive) with a step of `step`.
* @throws {Error} if `step` is 0.
*/
const range = (start: number, stop: Nullable<number> = null, step: number = 1): number[] => {
const result: number[] = [];

if (0 === step) {
throw new Error("Step cannot be zero.");
}
if (null === stop) {
stop = start;
start = 0;
}

if (0 < step) {
for (let i = start; i < stop; i += step) {
result.push(i);
}
} else {
for (let i = start; i > stop; i += step) {
result.push(i);
}
}

return result;
};

export {
getMapKeyByValue,
getMapValueWithNearestLessThanOrEqualKey,
range,
};

0 comments on commit 0e36cd9

Please sign in to comment.