Skip to content
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

feat(formatter): Display entire log event as JSON by default and remind users to set format string. #129

Merged
merged 9 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
5 changes: 3 additions & 2 deletions src/components/CentralContainer/Sidebar/SidebarTabs/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {
forwardRef,
useState,
useContext,
} from "react";

import {
Expand All @@ -13,6 +13,7 @@ import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
import SearchIcon from "@mui/icons-material/Search";
import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";

import {StateContext} from "../../../../contexts/StateContextProvider";
import {TAB_NAME} from "../../../../typings/tab";
import SettingsModal from "../../../modals/SettingsModal";
import FileInfoTabPanel from "./FileInfoTabPanel";
Expand Down Expand Up @@ -51,7 +52,7 @@ const SidebarTabs = forwardRef<HTMLDivElement, SidebarTabsProps>((
},
tabListRef
) => {
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState<boolean>(false);
const {isSettingsModalOpen, setIsSettingsModalOpen} = useContext(StateContext);

const handleSettingsModalClose = () => {
setIsSettingsModalOpen(false);
Expand Down
26 changes: 23 additions & 3 deletions src/components/PopUps/PopUpMessageBox.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {
import React, {
useContext,
useEffect,
useRef,
Expand All @@ -8,6 +8,7 @@ import {
import {
Alert,
Box,
Button,
CircularProgress,
IconButton,
Typography,
Expand All @@ -31,14 +32,16 @@ interface PopUpMessageProps {
}

/**
* Display a pop-up message in an alert box.
* Displays a pop-up message in an alert box with an optional action button. The pop-up can
* be manually dismissed or will automatically close after the specified `timeoutMillis`.
* If `timeoutMillis` is `0`, the pop-up will remain open until manually closed.
*
* @param props
* @param props.message
* @return
*/
const PopUpMessageBox = ({message}: PopUpMessageProps) => {
const {id, level, message: messageStr, title, timeoutMillis} = message;
const {id, level, primaryAction, message: messageStr, title, timeoutMillis} = message;

const {handlePopUpMessageClose} = useContext(NotificationContext);
const [percentRemaining, setPercentRemaining] = useState<number>(100);
Expand All @@ -48,6 +51,11 @@ const PopUpMessageBox = ({message}: PopUpMessageProps) => {
handlePopUpMessageClose(id);
};

const handlePrimaryActionClick = (ev: React.MouseEvent<HTMLButtonElement>) => {
primaryAction?.onClick?.(ev);
handleCloseButtonClick();
};

useEffect(() => {
if (DO_NOT_TIMEOUT_VALUE === timeoutMillis) {
return () => {};
Expand Down Expand Up @@ -113,6 +121,18 @@ const PopUpMessageBox = ({message}: PopUpMessageProps) => {
<Typography level={"body-sm"}>
{messageStr}
</Typography>
{primaryAction && (
<Box className={"pop-up-message-box-actions-container"}>
<Button
color={color}
variant={"solid"}
{...primaryAction}
onClick={handlePrimaryActionClick}
>
{primaryAction.children}
</Button>
</Box>
)}
</div>
</Alert>
);
Expand Down
10 changes: 9 additions & 1 deletion src/components/PopUps/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,15 @@
}

.pop-up-message-box-alert-layout {
width: 300px;
display: flex;
flex-direction: column;
gap: 10px;
width: 333px;
}

.pop-up-message-box-actions-container {
display: flex;
justify-content: flex-end;
}

.pop-up-message-box-title-container {
Expand Down
3 changes: 2 additions & 1 deletion src/components/modals/SettingsModal/SettingsDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ const CONFIG_FORM_FIELDS = [
\`{<field-name>[:<formatter-name>[:<formatter-options>]]}\`, where \`field-name\` is
required, while \`formatter-name\` and \`formatter-options\` are optional. For example,
the following placeholder would format a timestamp field with name \`@timestamp\`:
\`{@timestamp:timestamp:YYYY-MM-DD HH\\:mm\\:ss.SSS}\`.`,
\`{@timestamp:timestamp:YYYY-MM-DD HH\\:mm\\:ss.SSS}\`. Leave format string blank to
display the entire log event formatted as JSON.`,
initialValue: getConfig(CONFIG_KEY.DECODER_OPTIONS).formatString,
label: "Decoder: Format string",
name: LOCAL_STORAGE_KEY.DECODER_OPTIONS_FORMAT_STRING,
Expand Down
42 changes: 36 additions & 6 deletions src/contexts/StateContextProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,22 @@ import React, {
useState,
} from "react";

import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";

import LogExportManager, {
EXPORT_LOG_PROGRESS_VALUE_MAX,
EXPORT_LOG_PROGRESS_VALUE_MIN,
} from "../services/LogExportManager";
import {Nullable} from "../typings/common";
import {CONFIG_KEY} from "../typings/config";
import {LogLevelFilter} from "../typings/logs";
import {DEFAULT_AUTO_DISMISS_TIMEOUT_MILLIS} from "../typings/notifications";
import {
LOG_LEVEL,
LogLevelFilter,
} from "../typings/logs";
import {
DEFAULT_AUTO_DISMISS_TIMEOUT_MILLIS,
LONG_AUTO_DISMISS_TIMEOUT_MILLIS,
} from "../typings/notifications";
import {UI_STATE} from "../typings/states";
import {SEARCH_PARAM_NAMES} from "../typings/url";
import {
Expand Down Expand Up @@ -56,8 +64,9 @@ import {

interface StateContextType {
beginLineNumToLogEventNum: BeginLineNumToLogEventNumMap,
fileName: string,
exportProgress: Nullable<number>,
fileName: string,
isSettingsModalOpen: boolean,
uiState: UI_STATE,
logData: string,
numEvents: number,
Expand All @@ -70,7 +79,8 @@ interface StateContextType {
exportLogs: () => void,
loadFile: (fileSrc: FileSrcType, cursor: CursorType) => void,
loadPageByAction: (navAction: NavigationAction) => void,
setLogLevelFilter: (newLogLevelFilter: LogLevelFilter) => void,
setIsSettingsModalOpen: (isOpen: boolean) => void,
Copy link
Collaborator

Choose a reason for hiding this comment

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

For consistency, how about

Suggested change
setIsSettingsModalOpen: (isOpen: boolean) => void,
setIsSettingsModalOpen: (newIsOpen: boolean) => void,

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I prefer without new. I think the new for log level filter was just there to solve a namespace issue (i think loglevelfilter taken). Instead, i left as isOpen, and changed newLogLevelFilter to just filter.

setLogLevelFilter: (filter: LogLevelFilter) => void,
startQuery: (queryString: string, isRegex: boolean, isCaseSensitive: boolean) => void,
}
const StateContext = createContext<StateContextType>({} as StateContextType);
Expand All @@ -82,6 +92,7 @@ const STATE_DEFAULT: Readonly<StateContextType> = Object.freeze({
beginLineNumToLogEventNum: new Map<number, number>(),
exportProgress: null,
fileName: "",
isSettingsModalOpen: false,
logData: "No file is open.",
numEvents: 0,
numPages: 0,
Expand All @@ -94,6 +105,7 @@ const STATE_DEFAULT: Readonly<StateContextType> = Object.freeze({
exportLogs: () => null,
loadFile: () => null,
loadPageByAction: () => null,
setIsSettingsModalOpen: () => null,
setLogLevelFilter: () => null,
startQuery: () => null,
});
Expand Down Expand Up @@ -236,6 +248,8 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
// States
const [exportProgress, setExportProgress] =
useState<Nullable<number>>(STATE_DEFAULT.exportProgress);
const [isSettingsModalOpen, setIsSettingsModalOpen] =
useState<boolean>(STATE_DEFAULT.isSettingsModalOpen);
const [fileName, setFileName] = useState<string>(STATE_DEFAULT.fileName);
const [logData, setLogData] = useState<string>(STATE_DEFAULT.logData);
const [numEvents, setNumEvents] = useState<number>(STATE_DEFAULT.numEvents);
Expand Down Expand Up @@ -270,6 +284,20 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
}
}
break;
case WORKER_RESP_CODE.FORMAT_POPUP:
postPopUp({
level: LOG_LEVEL.INFO,
message: "Adding a format string can enhance the readability of your" +
" structured logs by customizing how fields are displayed.",
primaryAction: {
children: "Settings",
startDecorator: <SettingsOutlinedIcon/>,
onClick: () => { setIsSettingsModalOpen(true); },
},
timeoutMillis: LONG_AUTO_DISMISS_TIMEOUT_MILLIS,
title: "A format string has not been configured",
});
break;
Comment on lines +287 to +300
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Fix line length and improve message text.

The message text exceeds the line length limit and could be more concise.

Apply this diff to improve readability:

-                    message: "Adding a format string can enhance the readability of your" +
-                    " structured logs by customizing how fields are displayed.",
+                    message: "Add a format string to customize how log fields are displayed.",
📝 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
case WORKER_RESP_CODE.FORMAT_POPUP:
postPopUp({
level: LOG_LEVEL.INFO,
message: "Adding a format string can enhance the readability of your" +
" structured logs by customizing how fields are displayed.",
primaryAction: {
children: "Settings",
startDecorator: <SettingsOutlinedIcon/>,
onClick: () => { setIsSettingsModalOpen(true); },
},
timeoutMillis: LONG_AUTO_DISMISS_TIMEOUT_MILLIS,
title: "A format string has not been configured",
});
break;
case WORKER_RESP_CODE.FORMAT_POPUP:
postPopUp({
level: LOG_LEVEL.INFO,
message: "Add a format string to customize how log fields are displayed.",
primaryAction: {
children: "Settings",
startDecorator: <SettingsOutlinedIcon/>,
onClick: () => { setIsSettingsModalOpen(true); },
},
timeoutMillis: LONG_AUTO_DISMISS_TIMEOUT_MILLIS,
title: "A format string has not been configured",
});
break;

case WORKER_RESP_CODE.LOG_FILE_INFO:
setFileName(args.fileName);
setNumEvents(args.numEvents);
Expand Down Expand Up @@ -418,14 +446,14 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
loadPageByCursor(mainWorkerRef.current, cursor);
}, []);

const setLogLevelFilter = useCallback((newLogLevelFilter: LogLevelFilter) => {
const setLogLevelFilter = useCallback((filter: LogLevelFilter) => {
if (null === mainWorkerRef.current) {
return;
}
setUiState(UI_STATE.FAST_LOADING);
workerPostReq(mainWorkerRef.current, WORKER_REQ_CODE.SET_FILTER, {
cursor: {code: CURSOR_CODE.EVENT_NUM, args: {eventNum: logEventNumRef.current ?? 1}},
logLevelFilter: newLogLevelFilter,
logLevelFilter: filter,
});
}, []);

Expand Down Expand Up @@ -510,6 +538,7 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
beginLineNumToLogEventNum: beginLineNumToLogEventNumRef.current,
exportProgress: exportProgress,
fileName: fileName,
isSettingsModalOpen: isSettingsModalOpen,
logData: logData,
numEvents: numEvents,
numPages: numPages,
Expand All @@ -522,6 +551,7 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
exportLogs: exportLogs,
loadFile: loadFile,
loadPageByAction: loadPageByAction,
setIsSettingsModalOpen: setIsSettingsModalOpen,
setLogLevelFilter: setLogLevelFilter,
startQuery: startQuery,
}}
Expand Down
10 changes: 10 additions & 0 deletions src/services/MainWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ const onQueryResults = (queryProgress: number, queryResults: QueryResults) => {
postResp(WORKER_RESP_CODE.QUERY_RESULT, {progress: queryProgress, results: queryResults});
};

/**
* Sends a message to the renderer to open a pop-up which prompts user to replace the default
* format string.
*/
const postFormatPopup = () => {
Copy link
Collaborator

Choose a reason for hiding this comment

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

For now this is fine. In a future PR, we can refactor notification & exception handling in the service worker so that every notification at any level can only be sent by either a notification dispatcher (like this postFormatPopup) or the exception catching ~L147. What do you think?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think we can do something like this. I think we will need to add codes to the notifications so front-end knows if its an error or generic notifcation. I originally planned to do something like this and also just send the entire popup object to the front end from the back end. However, adding the settings modal callback in the backend is a bit gross, so decided against it. In future i think tho, we can have a unified notification handler in front-end, and maybe add codes (one could be for constructing format string popup with settings modal, another for error, etc..) Should we add issue?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Sure. Let's add the issue.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@coderabbitai can you add github issue to unify popup handler and notification handler in the front-end per my comment?

Copy link

Choose a reason for hiding this comment

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

@davemarco, I've created the GitHub issue to unify the popup handler and notification handler in the front-end as per your request: Issue #131.

🔗 Analysis chain
🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Create a new GitHub issue as requested by @davemarco
gh issue create \
  --title "Unify popup handler and notification handler in the front-end" \
  --body "As discussed in [PR #129](https://github.com/y-scope/yscope-log-viewer/pull/129) and [this comment](https://github.com/y-scope/yscope-log-viewer/pull/129#discussion_r1860925043), we should unify the popup handler and notification handler in the front-end.

Requester: @davemarco." \
  --assignee davemarco

Length of output: 455

postResp(WORKER_RESP_CODE.FORMAT_POPUP, null);
};

// eslint-disable-next-line no-warning-comments
// TODO: Break this function up into smaller functions.
// eslint-disable-next-line max-lines-per-function,max-statements
Expand Down Expand Up @@ -149,3 +157,5 @@ onmessage = async (ev: MessageEvent<MainWorkerReqMessage>) => {
}
}
};

export {postFormatPopup};
12 changes: 8 additions & 4 deletions src/services/decoders/ClpIrDecoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {Formatter} from "../../typings/formatters";
import {JsonObject} from "../../typings/js";
import {LogLevelFilter} from "../../typings/logs";
import YscopeFormatter from "../formatters/YscopeFormatter";
import {postFormatPopup} from "../MainWorker";
import {
convertToDayjsTimestamp,
isJsonObject,
Expand All @@ -29,7 +30,7 @@ class ClpIrDecoder implements Decoder {

readonly #streamType: CLP_IR_STREAM_TYPE;

#formatter: Nullable<Formatter>;
#formatter: Nullable<Formatter> = null;

constructor (
streamType: CLP_IR_STREAM_TYPE,
Expand All @@ -38,9 +39,12 @@ class ClpIrDecoder implements Decoder {
) {
this.#streamType = streamType;
this.#streamReader = streamReader;
this.#formatter = (streamType === CLP_IR_STREAM_TYPE.STRUCTURED) ?
new YscopeFormatter({formatString: decoderOptions.formatString}) :
null;
if (streamType === CLP_IR_STREAM_TYPE.STRUCTURED) {
this.#formatter = new YscopeFormatter({formatString: decoderOptions.formatString});
if (0 === decoderOptions.formatString.length) {
postFormatPopup();
}
}
Comment on lines +42 to +47
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add validation and optimize formatter creation

The current implementation has potential issues:

  1. No validation of decoderOptions or formatString
  2. Creates formatter even when format string is empty
  3. Potential undefined access on formatString.length

Consider this safer implementation:

 if (streamType === CLP_IR_STREAM_TYPE.STRUCTURED) {
+    if (!decoderOptions?.formatString) {
+        postFormatPopup();
+        // Set empty string as default
+        decoderOptions = { ...decoderOptions, formatString: "" };
+    } else if (decoderOptions.formatString.length === 0) {
+        postFormatPopup();
+    }
     this.#formatter = new YscopeFormatter({formatString: decoderOptions.formatString});
-    if (0 === decoderOptions.formatString.length) {
-        postFormatPopup();
-    }
 }
📝 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
if (streamType === CLP_IR_STREAM_TYPE.STRUCTURED) {
this.#formatter = new YscopeFormatter({formatString: decoderOptions.formatString});
if (0 === decoderOptions.formatString.length) {
postFormatPopup();
}
}
if (streamType === CLP_IR_STREAM_TYPE.STRUCTURED) {
if (!decoderOptions?.formatString) {
postFormatPopup();
// Set empty string as default
decoderOptions = { ...decoderOptions, formatString: "" };
} else if (decoderOptions.formatString.length === 0) {
postFormatPopup();
}
this.#formatter = new YscopeFormatter({formatString: decoderOptions.formatString});
}

}

/**
Expand Down
4 changes: 4 additions & 0 deletions src/services/decoders/JsonlDecoder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
LogLevelFilter,
} from "../../../typings/logs";
import YscopeFormatter from "../../formatters/YscopeFormatter";
import {postFormatPopup} from "../../MainWorker";
import {
convertToDayjsTimestamp,
convertToLogLevelValue,
Expand Down Expand Up @@ -54,6 +55,9 @@ class JsonlDecoder implements Decoder {
this.#logLevelKey = decoderOptions.logLevelKey;
this.#timestampKey = decoderOptions.timestampKey;
this.#formatter = new YscopeFormatter({formatString: decoderOptions.formatString});
if (0 === decoderOptions.formatString.length) {
postFormatPopup();
}
Comment on lines +58 to +60
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add null/undefined check for formatString

While the empty string check is good, we should also handle cases where formatString might be undefined.

Consider this safer implementation:

-        if (0 === decoderOptions.formatString.length) {
+        if (!decoderOptions.formatString?.length) {
             postFormatPopup();
         }
📝 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
if (0 === decoderOptions.formatString.length) {
postFormatPopup();
}
if (!decoderOptions.formatString?.length) {
postFormatPopup();
}

💡 Codebase verification

The review comment's concern is valid but for a different reason

The popup is triggered in a Web Worker context where JsonlDecoder is instantiated only once per file through LogFileManager. However, the current implementation still has two decoders (JsonlDecoder and ClpIrDecoder) that can trigger the same popup, which could lead to duplicate notifications.

  • Move the format popup logic to LogFileManager where it can be triggered once during file initialization, regardless of the decoder type.
  • Consider using a session flag in the worker context to ensure the popup appears only once per session.
🔗 Analysis chain

Consider moving popup trigger to a more appropriate location

Triggering the popup in the constructor might lead to multiple popups if multiple decoder instances are created. Consider:

  1. Moving this logic to a higher-level component
  2. Adding a flag to ensure the popup is shown only once per session

Let's verify if multiple decoders might be instantiated:


Let me gather more context about where and how the decoder is used.


Let me check one more thing to understand the worker communication pattern.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for JsonlDecoder instantiation patterns
ast-grep --pattern 'new JsonlDecoder($$$)'

Length of output: 151


Script:

#!/bin/bash
# Check how LogFileManager is instantiated
ast-grep --pattern 'new LogFileManager($$$)'

# Check the implementation context of postFormatPopup
rg "postFormatPopup" -A 5

Length of output: 2531


Script:

#!/bin/bash
# Check for WORKER_RESP_CODE usage and worker communication
rg "WORKER_RESP_CODE" -A 3

# Check where the format popup is handled
rg "FORMAT_POPUP" -B 2 -A 2

Length of output: 9059

}

getEstimatedNumEvents (): number {
Expand Down
6 changes: 6 additions & 0 deletions src/services/formatters/YscopeFormatter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import {LogEvent} from "../../../typings/logs";
import {
getFormattedField,
jsonValueToString,
removeEscapeCharacters,
replaceDoubleBacklash,
splitFieldPlaceholder,
Expand Down Expand Up @@ -37,6 +38,11 @@ class YscopeFormatter implements Formatter {
}

formatLogEvent (logEvent: LogEvent): string {
// Empty format string is special case where formatter returns all fields as JSON.
if ("" === this.#processedFormatString) {
return jsonValueToString(logEvent.fields);
}

const formattedLogFragments: string[] = [];
let lastIndex = 0;

Expand Down
6 changes: 3 additions & 3 deletions src/typings/formatters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {LogEvent} from "./logs";


/**
* @property formatString A Yscope format string. The format string can include field-placeholders
* @property formatString A YScope format string. The format string can include field-placeholders
* to insert and format any field of a JSON log event. A field-placeholder uses the following
* syntax:
* `{<field-name>[:<formatter-name>[:<formatter-options>]]}`
Expand Down Expand Up @@ -49,14 +49,14 @@ interface YscopeFieldFormatter {
}

/**
* Type for list of currently supported Yscope field formatters.
* Type for list of currently supported YScope field formatters.
*/
type YscopeFieldFormatterMap = {
[key: string]: new (options: Nullable<string>) => YscopeFieldFormatter;
};

/**
* Parsed field placeholder from a Yscope format string.
* Parsed field placeholder from a YScope format string.
*/
type YscopeFieldPlaceholder = {
fieldNameKeys: string[],
Expand Down
9 changes: 9 additions & 0 deletions src/typings/notifications.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import {ButtonProps} from "@mui/joy";

import {LOG_LEVEL} from "./logs";


Expand All @@ -9,6 +11,7 @@ interface PopUpMessage {
message: string,
timeoutMillis: number,
title: string,
primaryAction?: ButtonProps,
}

/**
Expand All @@ -21,9 +24,15 @@ const DO_NOT_TIMEOUT_VALUE = 0;
*/
const DEFAULT_AUTO_DISMISS_TIMEOUT_MILLIS = 10_000;

/**
* A longer duration in milliseconds after which an automatic dismissal will occur.
*/
const LONG_AUTO_DISMISS_TIMEOUT_MILLIS = 20_000;


export type {PopUpMessage};
export {
DEFAULT_AUTO_DISMISS_TIMEOUT_MILLIS,
DO_NOT_TIMEOUT_VALUE,
LONG_AUTO_DISMISS_TIMEOUT_MILLIS,
};
Loading