Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/components/AppController.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ import {
* NOTE: this may modify the URL parameters.
*/
const updateViewHashParams = () => {
const {isPrettified, logEventNum} = getWindowUrlHashParams();
const {updateIsPrettified, updateLogEventNum} = useViewStore.getState();
const {isPrettified, logEventNum, timezone} = getWindowUrlHashParams();
const {updateIsPrettified, updateTimezoneName, updateLogEventNum} = useViewStore.getState();

updateIsPrettified(isPrettified);
updateTimezoneName(timezone);
updateLogEventNum(logEventNum);
};

Expand Down
2 changes: 1 addition & 1 deletion src/components/StatusBar/LogLevelSelect/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@
}

.log-level-select-listbox {
max-height: calc(100vh - var(--ylv-menu-bar-height) - var(--ylv-status-bar-height)) !important;
max-height: var(--ylv-list-box-max-height) !important;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.timezone-category-chip {
cursor: pointer;
}

.timezone-category-chip-default-timezone {
/* Disable `Chip`'s background style. */
background-color: initial !important;
}
Comment on lines +1 to +8
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Nit: make the chip truly non-interactive when disabled

If a category ever needs a disabled state, consider adding pointer-events: none; alongside the existing background override to prevent misleading hover cues.

No action required for this PR.

🤖 Prompt for AI Agents
In src/components/StatusBar/TimezoneSelect/TimezoneCategoryChip.css lines 1 to
8, to make the chip truly non-interactive when disabled, add pointer-events:
none; to the .timezone-category-chip-default-timezone class alongside the
background-color override. This will prevent hover and click interactions,
ensuring the disabled state is clear and not misleading.

Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {Chip} from "@mui/joy";
import {DefaultColorPalette} from "@mui/joy/styles/types/colorSystem";

import {TIMEZONE_CATEGORY} from "../../../typings/date";

import "./TimezoneCategoryChip.css";


interface TimezoneTypeMetadata {
label: string;
color: DefaultColorPalette;
}

const TIMEZONE_CATEGORY_METADATA: Record<TIMEZONE_CATEGORY, TimezoneTypeMetadata> = {
[TIMEZONE_CATEGORY.DEFAULT]: {
label: "Default",
color: "neutral",
},
[TIMEZONE_CATEGORY.BROWSER]: {
label: "Browser",
color: "warning",
},
[TIMEZONE_CATEGORY.LOGGER]: {
label: "Logger",
color: "primary",
},
[TIMEZONE_CATEGORY.MANUAL]: {
label: "Manual",
color: "success",
},
};

interface TimezoneCategoryChipProps {
category: TIMEZONE_CATEGORY;
disabled: boolean;
}

/**
* Render a chip that represents the category of the timezone.
*
* @param props
* @param props.category
* @param props.disabled
* @return
*/
const TimezoneCategoryChip = ({
category,
disabled,
}: TimezoneCategoryChipProps) => {
const isDefault = category === TIMEZONE_CATEGORY.DEFAULT;

return (
<Chip
color={TIMEZONE_CATEGORY_METADATA[category].color}
disabled={disabled}
sx={{borderRadius: "xs"}}
className={`timezone-category-chip ${isDefault ?
"timezone-category-chip-default-timezone" :
""}`}
>
{"Timezone"}
{false === isDefault && " | "}
{false === isDefault && TIMEZONE_CATEGORY_METADATA[category].label}
</Chip>
);
};


export default TimezoneCategoryChip;
14 changes: 14 additions & 0 deletions src/components/StatusBar/TimezoneSelect/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.timezone-select {
/* stylelint-disable-next-line custom-property-pattern */
--Input-focusedThickness: 0 !important;
}

.timezone-select-listbox {
min-width: fit-content;
max-height: var(--ylv-list-box-max-height) !important;
}

.timezone-select-pop-up-indicator {
/* stylelint-disable-next-line custom-property-pattern */
--Icon-fontSize: 1.1rem !important;
}
115 changes: 115 additions & 0 deletions src/components/StatusBar/TimezoneSelect/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import {
useCallback,
useEffect,
useMemo,
useState,
} from "react";

import {SelectValue} from "@mui/base/useSelect";
import {Autocomplete} from "@mui/joy";

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

import useUiStore from "../../../stores/uiStore";
import useViewStore from "../../../stores/viewStore";
import {
BROWSER_TIMEZONE_NAME,
DEFAULT_TIMEZONE_NAME,
getTimezoneCategory,
INTL_SUPPORTED_TIMEZONE_NAMES,
LOGGER_TIMEZONE_NAME,
UTC_TIMEZONE_OFFSET_NAMES,
} from "../../../typings/date";
import {UI_ELEMENT} from "../../../typings/states";
import {HASH_PARAM_NAMES} from "../../../typings/url";
import {isDisabled} from "../../../utils/states";
import {updateWindowUrlHashParams} from "../../../utils/url";
import TimezoneCategoryChip from "./TimezoneCategoryChip.tsx";

import "./index.css";


/**
* The timezone select dropdown menu, the selectable options can be classified as three types:
* - Default (use the origin timezone of the log events)
* - Browser Timezone (use the timezone that the browser is currently using)
* - Frequently-used Timezone
*
* @return A timezone select dropdown menu
*/
const TimezoneSelect = () => {
const uiState = useUiStore((state) => state.uiState);
const timezoneName = useViewStore((state) => state.timezoneName);

const [inputWidth, setInputWidth] = useState<string>(`${timezoneName.length}ch`);
const timezoneNameOptions = useMemo(() => [
DEFAULT_TIMEZONE_NAME,
BROWSER_TIMEZONE_NAME,
LOGGER_TIMEZONE_NAME,
...UTC_TIMEZONE_OFFSET_NAMES,
...INTL_SUPPORTED_TIMEZONE_NAMES.filter(
(tzName) => tzName !== BROWSER_TIMEZONE_NAME
),
], []);

const handleTimezoneSelectChange =
useCallback((_: unknown, value: SelectValue<string, false>) => {
if (null === value) {
throw new Error("Unexpected null value in non-clearable timezone select.");
}

const {updateTimezoneName} = useViewStore.getState();
updateTimezoneName(value);
updateWindowUrlHashParams({
[HASH_PARAM_NAMES.TIMEZONE]: value,
});
}, []);

useEffect(() => {
// Update the input width based on the selected timezone name.
setInputWidth(`${timezoneName.length}ch`);
}, [timezoneName]);


const disabled = isDisabled(uiState, UI_ELEMENT.TIMEZONE_SETTER);

return (
<Autocomplete
className={"timezone-select"}
componentName={"button"}
disableClearable={true}
disabled={disabled}
groupBy={getTimezoneCategory}
openOnFocus={true}
options={timezoneNameOptions}
popupIcon={<KeyboardArrowUpIcon/>}
size={"sm"}
value={timezoneName}
variant={"soft"}
slotProps={{
popupIndicator: {
className: "timezone-select-pop-up-indicator",
},
input: {
sx: {
width: inputWidth,
},
},
listbox: {
className: "timezone-select-listbox",
placement: "top-end",
modifiers: [
// Remove gap between the listbox and the `Select` button.
{name: "offset", enabled: false},
],
},
}}
startDecorator={<TimezoneCategoryChip
category={getTimezoneCategory(timezoneName)}
disabled={disabled}/>}
onChange={handleTimezoneSelectChange}/>
);
};


export default TimezoneSelect;
2 changes: 2 additions & 0 deletions src/components/StatusBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from "../../utils/url";
import LogLevelSelect from "./LogLevelSelect";
import StatusBarToggleButton from "./StatusBarToggleButton";
import TimezoneSelect from "./TimezoneSelect";

Comment on lines +27 to 28
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Consider lazy-loading the new TimezoneSelect import

TimezoneSelect is only used inside the status bar and most users may never open its listbox. Wrapping the import with React.lazy() + Suspense would shave a few KB from the initial bundle without UX impact.

-import TimezoneSelect from "./TimezoneSelect";
+const TimezoneSelect = React.lazy(() => import("./TimezoneSelect"));

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/components/StatusBar/index.tsx around lines 27 to 28, the TimezoneSelect
component is imported normally but is only used inside the status bar and may
not be needed immediately. To reduce the initial bundle size, change the import
to use React.lazy() for lazy loading and wrap the usage of TimezoneSelect in a
Suspense component with a fallback UI. This will defer loading TimezoneSelect
until it is actually rendered, improving performance without affecting user
experience.

import "./index.css";

Expand Down Expand Up @@ -89,6 +90,7 @@ const StatusBar = () => {
</Tooltip>
<Divider orientation={"vertical"}/>

<TimezoneSelect/>
<LogLevelSelect/>
<Divider orientation={"vertical"}/>
Comment on lines 91 to 95
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Add a divider before LogLevelSelect for visual symmetry

The status-bar controls are currently separated by vertical dividers, except between the newly-inserted TimezoneSelect and the existing LogLevelSelect. Adding one keeps the rhythm consistent.

-            <TimezoneSelect/>
-            <LogLevelSelect/>
+            <TimezoneSelect/>
+            <Divider orientation={"vertical"}/>
+            <LogLevelSelect/>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Divider orientation={"vertical"}/>
<TimezoneSelect/>
<LogLevelSelect/>
<Divider orientation={"vertical"}/>
<Divider orientation={"vertical"}/>
<TimezoneSelect/>
<Divider orientation={"vertical"}/>
<LogLevelSelect/>
<Divider orientation={"vertical"}/>
🤖 Prompt for AI Agents
In src/components/StatusBar/index.tsx between lines 91 and 95, add a vertical
Divider component before the LogLevelSelect component to maintain consistent
visual separation between controls. This will ensure the spacing and rhythm of
the status-bar controls remain uniform by placing a Divider both before and
after LogLevelSelect.


Expand Down
3 changes: 3 additions & 0 deletions src/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ html {
--ylv-status-bar-height: 32px;
--ylv-menu-bar-height: 32px;
--ylv-panel-resize-handle-width: 4px;
--ylv-list-box-max-height: calc(
100vh - var(--ylv-menu-bar-height) - var(--ylv-status-bar-height)
);

/* z-index globals
*
Expand Down
45 changes: 45 additions & 0 deletions src/stores/viewStore/createViewFormattingSlice.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import {StateCreator} from "zustand";

import {DEFAULT_TIMEZONE_NAME} from "../../typings/date";
import {UI_STATE} from "../../typings/states";
import {
CURSOR_CODE,
CursorType,
} from "../../typings/worker";
import useLogFileManagerStore from "../logFileManagerProxyStore";
import useLogFileStore from "../logFileStore";
import {handleErrorWithNotification} from "../notificationStore";
import useUiStore from "../uiStore";
import {VIEW_EVENT_DEFAULT} from "./createViewEventSlice";
Expand All @@ -18,6 +20,7 @@ import {

const VIEW_FORMATTING_DEFAULT: ViewFormattingValues = {
isPrettified: false,
timezoneName: DEFAULT_TIMEZONE_NAME,
};

/**
Expand Down Expand Up @@ -59,6 +62,48 @@ const createViewFormattingSlice: StateCreator<
updatePageData(pageData);
})().catch(handleErrorWithNotification);
},
updateTimezoneName: (newTimezoneName: string) => {
if ("" === newTimezoneName) {
newTimezoneName = DEFAULT_TIMEZONE_NAME;
}

const {numEvents} = useLogFileStore.getState();
if (0 === numEvents) {
return;
}

const {timezoneName} = get();
if (newTimezoneName === timezoneName) {
return;
}
Comment on lines +65 to +78
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Use a local variable instead of reassigning the parameter.

The method reassigns the newTimezoneName parameter, which can be confusing. Use a local variable for better clarity.

 updateTimezoneName: (newTimezoneName: string) => {
-    if ("" === newTimezoneName) {
-        newTimezoneName = DEFAULT_TIMEZONE_NAME;
-    }
+    const timezoneName = "" === newTimezoneName ? DEFAULT_TIMEZONE_NAME : newTimezoneName;

     const {numEvents} = useLogFileStore.getState();
     if (0 === numEvents) {
         return;
     }

-    const {timezoneName} = get();
-    if (newTimezoneName === timezoneName) {
+    const {timezoneName: currentTimezoneName} = get();
+    if (timezoneName === currentTimezoneName) {
         return;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
updateTimezoneName: (newTimezoneName: string) => {
if ("" === newTimezoneName) {
newTimezoneName = DEFAULT_TIMEZONE_NAME;
}
const {numEvents} = useLogFileStore.getState();
if (0 === numEvents) {
return;
}
const {timezoneName} = get();
if (newTimezoneName === timezoneName) {
return;
}
updateTimezoneName: (newTimezoneName: string) => {
const timezoneName = "" === newTimezoneName ? DEFAULT_TIMEZONE_NAME : newTimezoneName;
const {numEvents} = useLogFileStore.getState();
if (0 === numEvents) {
return;
}
const {timezoneName: currentTimezoneName} = get();
if (timezoneName === currentTimezoneName) {
return;
}
// …rest of method…
}
🧰 Tools
🪛 Biome (1.9.4)

[error] 67-67: Reassigning a function parameter is confusing.

The parameter is declared here:

Use a local variable instead.

(lint/style/noParameterAssign)

🤖 Prompt for AI Agents
In src/stores/viewStore/createViewFormattingSlice.ts around lines 65 to 78, the
updateTimezoneName method reassigns the parameter newTimezoneName, which reduces
code clarity. To fix this, declare a new local variable initialized with
newTimezoneName and use that variable for any modifications instead of
reassigning the parameter directly.


const {setUiState} = useUiStore.getState();
setUiState(UI_STATE.FAST_LOADING);

set({timezoneName: newTimezoneName});

const {logEventNum} = get();
let cursor: CursorType = {code: CURSOR_CODE.LAST_EVENT, args: null};
if (VIEW_EVENT_DEFAULT.logEventNum !== logEventNum) {
cursor = {
code: CURSOR_CODE.EVENT_NUM,
args: {eventNum: logEventNum},
};
}

(async () => {
const {logFileManagerProxy} = useLogFileManagerStore.getState();
const {isPrettified, updatePageData} = get();

// await logFileManagerProxy.setTimezone(newTimezoneName);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shall we add a TODO?

const pageData = await logFileManagerProxy.loadPage(
cursor,
isPrettified,
);

updatePageData(pageData);
})().catch(handleErrorWithNotification);
},
});

export default createViewFormattingSlice;
2 changes: 2 additions & 0 deletions src/stores/viewStore/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@ type ViewEventSlice = ViewEventValues & ViewEventActions;

interface ViewFormattingValues {
isPrettified: boolean;
timezoneName: string;
}

interface ViewFormattingActions {
updateIsPrettified: (newIsPrettified: boolean) => void;
updateTimezoneName: (newTimezoneName: string) => void;
}

type ViewFormattingSlice = ViewFormattingValues & ViewFormattingActions;
Expand Down
Loading