Skip to content
Draft
Show file tree
Hide file tree
Changes from 6 commits
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
1 change: 1 addition & 0 deletions docs/src/dev-guide/contributing-validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,6 @@ still be tested manually:
* Toggling tabbed panels in the sidebar
* Using keyboard shortcuts
* Toggling "Prettify" in both the status bar and the address bar
* Select different timezone in the "Timezone" dropdown menu in the status bar or URL

[gh-workflow-test]: https://github.com/y-scope/yscope-log-viewer/blob/main/.github/workflows/test.yaml
31 changes: 31 additions & 0 deletions src/components/StatusBar/TimezoneSelect/index.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/* Since timezone is not multi select, and multi select has a different style in Joy components, so we have to manually
tune the style
*/
.timezone-select {
margin-right: 1px;
}

.timezone-select-render-value-box {
display: flex;
gap: 2px;
}

.timezone-select-render-value-box-label {
padding: 0 !important;

font-size: 0.95rem !important;
font-weight: 500 !important;
line-height: 1.5 !important;

background-color: initial !important;
border: none !important;
box-shadow: none !important;
}

.timezone-select-render-value-box-label-disabled {
color: #686f76 !important;
}

.timezone-select-listbox {
max-height: calc(100vh - var(--ylv-menu-bar-height) - var(--ylv-status-bar-height)) !important;
}
229 changes: 229 additions & 0 deletions src/components/StatusBar/TimezoneSelect/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import React, {
useCallback,
useContext,
useEffect,
useRef,
useState,
} from "react";

import {SelectValue} from "@mui/base/useSelect";
import {
Box,
Chip,
ListDivider,
ListItemContent,
Option,
Select,
SelectOption,
} from "@mui/joy";

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

import {StateContext} from "../../../contexts/StateContextProvider";
import {
updateWindowUrlHashParams,
UrlContext,
} from "../../../contexts/UrlContextProvider";
import {UI_ELEMENT} from "../../../typings/states";
import {HASH_PARAM_NAMES} from "../../../typings/url";
import {isDisabled} from "../../../utils/states";

import "./index.css";


const LOGGER_TIMEZONE = "Logger Timezone";
const COMMON_TIMEZONES = [
"America/New_York",
"Europe/London",
"Asia/Shanghai",
"Asia/Tokyo",
"Australia/Sydney",
"Pacific/Honolulu",
"America/Los_Angeles",
"America/Chicago",
"America/Denver",
"Asia/Kolkata",
"Europe/Berlin",
"Europe/Moscow",
"Asia/Dubai",
"Asia/Singapore",
"Asia/Seoul",
"Pacific/Auckland",
];

/**
* Convert the timezone string to GMT +/- minutes
*
* @param tz
* @return The GMT +/- minutes shown before the timezone string
*/
const getLongOffsetOfTimezone = (tz: string): string => {
return new Intl.DateTimeFormat("default", {
timeZone: tz,
timeZoneName: "longOffset",
}).formatToParts()
.find((p) => "timeZoneName" === p.type)?.value ?? "Unknown timezone";
};
Comment on lines +55 to +61
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling for invalid timezone strings.

The function doesn't handle invalid timezone strings gracefully. While there's a fallback to "Unknown timezone", it would be better to validate the timezone before attempting to use it.

const getLongOffsetOfTimezone = (tz: string): string => {
+    // Validate timezone before using it
+    try {
+        // Check if timezone is valid by attempting to use it
+        Intl.DateTimeFormat("default", { timeZone: tz });
+        
        return new Intl.DateTimeFormat("default", {
            timeZone: tz,
            timeZoneName: "longOffset",
        }).formatToParts()
            .find((p) => "timeZoneName" === p.type)?.value ?? "Unknown timezone";
+    } catch (error) {
+        console.warn(`Invalid timezone: ${tz}`);
+        return "Invalid timezone";
+    }
};
📝 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
const getLongOffsetOfTimezone = (tz: string): string => {
return new Intl.DateTimeFormat("default", {
timeZone: tz,
timeZoneName: "longOffset",
}).formatToParts()
.find((p) => "timeZoneName" === p.type)?.value ?? "Unknown timezone";
};
const getLongOffsetOfTimezone = (tz: string): string => {
// Validate timezone before using it
try {
// Check if timezone is valid by attempting to use it
Intl.DateTimeFormat("default", { timeZone: tz });
return new Intl.DateTimeFormat("default", {
timeZone: tz,
timeZoneName: "longOffset",
}).formatToParts()
.find((p) => "timeZoneName" === p.type)?.value ?? "Unknown timezone";
} catch (error) {
console.warn(`Invalid timezone: ${tz}`);
return "Invalid timezone";
}
};


/**
* Render the selected timezone option in the status bar
*
* @param selected
* @return The selected timezone shown in the status bar
*/
const handleRenderValue = (selected: SelectValue<SelectOption<string>, false>) => (
<Box className={"timezone-select-render-value-box"}>
<Chip className={"timezone-select-render-value-box-label"}>Timezone</Chip>
<Chip>
{selected?.label}
</Chip>
</Box>
);

/**
* Render the timezone options in the dropdown menu
*
* @param value
* @param label
* @param onClick
* @param suffix
* @return An option box in the dropdown menu
*/
const renderTimezoneOption = (
value: string,
label: string,
onClick: React.MouseEventHandler,
suffix?: string
) => (
<Option
data-value={value}
key={value}
value={value}
onClick={onClick}
>
{LOGGER_TIMEZONE !== value &&
<ListItemContent>
(
{getLongOffsetOfTimezone(value)}
)
{" "}
{label}
{" "}
{suffix ?? ""}
</ListItemContent>}

{LOGGER_TIMEZONE === value &&
<ListItemContent>
{LOGGER_TIMEZONE}
</ListItemContent>}
</Option>
);

/**
* 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} = useContext(StateContext);
const {logTimezone} = useContext(UrlContext);

const [browserTimezone, setBrowserTimezone] = useState<string | null>(null);
const [selectedTimezone, setSelectedTimezone] = useState<string | null>(null);

const logTimezoneRef = useRef<string | null>(logTimezone);

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

useEffect(() => {
const tz = new Intl.DateTimeFormat().resolvedOptions().timeZone;
setBrowserTimezone(tz);
}, []);

useEffect(() => {
logTimezoneRef.current = logTimezone;
if (!disabled) {
setSelectedTimezone(logTimezone ?? LOGGER_TIMEZONE);
}
}, [disabled,
logTimezone]);

useEffect(() => {
if (selectedTimezone !== logTimezoneRef.current) {
const updatedTimezone = (LOGGER_TIMEZONE === selectedTimezone) ?
null :
selectedTimezone;

updateWindowUrlHashParams({
[HASH_PARAM_NAMES.LOG_TIMEZONE]: updatedTimezone,
});
}
}, [selectedTimezone]);

const handleOptionClick = useCallback((ev: React.MouseEvent) => {
const currentTarget = ev.currentTarget as HTMLElement;
const value = currentTarget.dataset.value ?? LOGGER_TIMEZONE;
setSelectedTimezone(value);
}, []);

return (
<Select
className={"timezone-select"}
disabled={disabled}
indicator={<KeyboardArrowUpIcon/>}
renderValue={handleRenderValue}
size={"sm"}
value={selectedTimezone}
variant={"soft"}
placeholder={
<Box className={"timezone-select-render-value-box"}>
<Chip
className={`timezone-select-render-value-box-label ${disabled ?
"timezone-select-render-value-box-label-disabled" :
""}`}
>
Timezone
</Chip>
</Box>
}
slotProps={{
listbox: {
className: "timezone-select-listbox",
placement: "top-end",
modifiers: [
{name: "equalWidth", enabled: false},
{name: "offset", enabled: false},
],
},
}}
onChange={(_, value) => {
if (value) {
setSelectedTimezone(value);
}
}}
>
{renderTimezoneOption(LOGGER_TIMEZONE, LOGGER_TIMEZONE, handleOptionClick)}

{browserTimezone &&
renderTimezoneOption(
browserTimezone,
browserTimezone,
handleOptionClick,
"(Browser Timezone)"
)}

<ListDivider
inset={"gutter"}
role={"separator"}/>

{COMMON_TIMEZONES.map(
(label) => renderTimezoneOption(label, label, handleOptionClick)
)}
</Select>
);
};

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 @@ -22,6 +22,7 @@ import {ACTION_NAME} from "../../utils/actions";
import {isDisabled} from "../../utils/states";
import LogLevelSelect from "./LogLevelSelect";
import StatusBarToggleButton from "./StatusBarToggleButton";
import TimezoneSelect from "./TimezoneSelect";

import "./index.css";

Expand Down Expand Up @@ -65,6 +66,7 @@ const StatusBar = () => {
{/* This is left blank intentionally until status messages are implemented. */}
</Typography>

<TimezoneSelect/>
<Tooltip title={"Copy link to clipboard"}>
<span>
<Button
Expand Down
Loading