Skip to content

Commit 82e677e

Browse files
feat(formatter): Display entire log event as JSON by default and remind users to set format string. (#129)
Co-authored-by: Junhao Liao <junhao@junhao.ca>
1 parent 72e40e4 commit 82e677e

File tree

13 files changed

+120
-24
lines changed

13 files changed

+120
-24
lines changed

src/components/CentralContainer/Sidebar/SidebarTabs/index.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {
22
forwardRef,
3-
useState,
3+
useContext,
44
} from "react";
55

66
import {
@@ -13,6 +13,7 @@ import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
1313
import SearchIcon from "@mui/icons-material/Search";
1414
import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
1515

16+
import {StateContext} from "../../../../contexts/StateContextProvider";
1617
import {TAB_NAME} from "../../../../typings/tab";
1718
import SettingsModal from "../../../modals/SettingsModal";
1819
import FileInfoTabPanel from "./FileInfoTabPanel";
@@ -51,7 +52,7 @@ const SidebarTabs = forwardRef<HTMLDivElement, SidebarTabsProps>((
5152
},
5253
tabListRef
5354
) => {
54-
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState<boolean>(false);
55+
const {isSettingsModalOpen, setIsSettingsModalOpen} = useContext(StateContext);
5556

5657
const handleSettingsModalClose = () => {
5758
setIsSettingsModalOpen(false);

src/components/PopUps/PopUpMessageBox.tsx

+23-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {
1+
import React, {
22
useContext,
33
useEffect,
44
useRef,
@@ -8,6 +8,7 @@ import {
88
import {
99
Alert,
1010
Box,
11+
Button,
1112
CircularProgress,
1213
IconButton,
1314
Typography,
@@ -31,14 +32,16 @@ interface PopUpMessageProps {
3132
}
3233

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

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

54+
const handlePrimaryActionClick = (ev: React.MouseEvent<HTMLButtonElement>) => {
55+
primaryAction?.onClick?.(ev);
56+
handleCloseButtonClick();
57+
};
58+
5159
useEffect(() => {
5260
if (DO_NOT_TIMEOUT_VALUE === timeoutMillis) {
5361
return () => {};
@@ -113,6 +121,18 @@ const PopUpMessageBox = ({message}: PopUpMessageProps) => {
113121
<Typography level={"body-sm"}>
114122
{messageStr}
115123
</Typography>
124+
{primaryAction && (
125+
<Box className={"pop-up-message-box-actions-container"}>
126+
<Button
127+
color={color}
128+
variant={"solid"}
129+
{...primaryAction}
130+
onClick={handlePrimaryActionClick}
131+
>
132+
{primaryAction.children}
133+
</Button>
134+
</Box>
135+
)}
116136
</div>
117137
</Alert>
118138
);

src/components/PopUps/index.css

+9-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,15 @@
2727
}
2828

2929
.pop-up-message-box-alert-layout {
30-
width: 300px;
30+
display: flex;
31+
flex-direction: column;
32+
gap: 10px;
33+
width: 333px;
34+
}
35+
36+
.pop-up-message-box-actions-container {
37+
display: flex;
38+
justify-content: flex-end;
3139
}
3240

3341
.pop-up-message-box-title-container {

src/components/modals/SettingsModal/SettingsDialog.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ const CONFIG_FORM_FIELDS = [
3838
\`{<field-name>[:<formatter-name>[:<formatter-options>]]}\`, where \`field-name\` is
3939
required, while \`formatter-name\` and \`formatter-options\` are optional. For example,
4040
the following placeholder would format a timestamp field with name \`@timestamp\`:
41-
\`{@timestamp:timestamp:YYYY-MM-DD HH\\:mm\\:ss.SSS}\`.`,
41+
\`{@timestamp:timestamp:YYYY-MM-DD HH\\:mm\\:ss.SSS}\`. Leave format string blank to
42+
display the entire log event formatted as JSON.`,
4243
initialValue: getConfig(CONFIG_KEY.DECODER_OPTIONS).formatString,
4344
label: "Decoder: Format string",
4445
name: LOCAL_STORAGE_KEY.DECODER_OPTIONS_FORMAT_STRING,

src/contexts/StateContextProvider.tsx

+36-6
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,22 @@ import React, {
88
useState,
99
} from "react";
1010

11+
import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
12+
1113
import LogExportManager, {
1214
EXPORT_LOG_PROGRESS_VALUE_MAX,
1315
EXPORT_LOG_PROGRESS_VALUE_MIN,
1416
} from "../services/LogExportManager";
1517
import {Nullable} from "../typings/common";
1618
import {CONFIG_KEY} from "../typings/config";
17-
import {LogLevelFilter} from "../typings/logs";
18-
import {DEFAULT_AUTO_DISMISS_TIMEOUT_MILLIS} from "../typings/notifications";
19+
import {
20+
LOG_LEVEL,
21+
LogLevelFilter,
22+
} from "../typings/logs";
23+
import {
24+
DEFAULT_AUTO_DISMISS_TIMEOUT_MILLIS,
25+
LONG_AUTO_DISMISS_TIMEOUT_MILLIS,
26+
} from "../typings/notifications";
1927
import {UI_STATE} from "../typings/states";
2028
import {SEARCH_PARAM_NAMES} from "../typings/url";
2129
import {
@@ -56,8 +64,9 @@ import {
5664

5765
interface StateContextType {
5866
beginLineNumToLogEventNum: BeginLineNumToLogEventNumMap,
59-
fileName: string,
6067
exportProgress: Nullable<number>,
68+
fileName: string,
69+
isSettingsModalOpen: boolean,
6170
uiState: UI_STATE,
6271
logData: string,
6372
numEvents: number,
@@ -70,7 +79,8 @@ interface StateContextType {
7079
exportLogs: () => void,
7180
loadFile: (fileSrc: FileSrcType, cursor: CursorType) => void,
7281
loadPageByAction: (navAction: NavigationAction) => void,
73-
setLogLevelFilter: (newLogLevelFilter: LogLevelFilter) => void,
82+
setIsSettingsModalOpen: (isOpen: boolean) => void,
83+
setLogLevelFilter: (filter: LogLevelFilter) => void,
7484
startQuery: (queryString: string, isRegex: boolean, isCaseSensitive: boolean) => void,
7585
}
7686
const StateContext = createContext<StateContextType>({} as StateContextType);
@@ -82,6 +92,7 @@ const STATE_DEFAULT: Readonly<StateContextType> = Object.freeze({
8292
beginLineNumToLogEventNum: new Map<number, number>(),
8393
exportProgress: null,
8494
fileName: "",
95+
isSettingsModalOpen: false,
8596
logData: "No file is open.",
8697
numEvents: 0,
8798
numPages: 0,
@@ -94,6 +105,7 @@ const STATE_DEFAULT: Readonly<StateContextType> = Object.freeze({
94105
exportLogs: () => null,
95106
loadFile: () => null,
96107
loadPageByAction: () => null,
108+
setIsSettingsModalOpen: () => null,
97109
setLogLevelFilter: () => null,
98110
startQuery: () => null,
99111
});
@@ -236,6 +248,8 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
236248
// States
237249
const [exportProgress, setExportProgress] =
238250
useState<Nullable<number>>(STATE_DEFAULT.exportProgress);
251+
const [isSettingsModalOpen, setIsSettingsModalOpen] =
252+
useState<boolean>(STATE_DEFAULT.isSettingsModalOpen);
239253
const [fileName, setFileName] = useState<string>(STATE_DEFAULT.fileName);
240254
const [logData, setLogData] = useState<string>(STATE_DEFAULT.logData);
241255
const [numEvents, setNumEvents] = useState<number>(STATE_DEFAULT.numEvents);
@@ -270,6 +284,20 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
270284
}
271285
}
272286
break;
287+
case WORKER_RESP_CODE.FORMAT_POPUP:
288+
postPopUp({
289+
level: LOG_LEVEL.INFO,
290+
message: "Adding a format string can enhance the readability of your" +
291+
" structured logs by customizing how fields are displayed.",
292+
primaryAction: {
293+
children: "Settings",
294+
startDecorator: <SettingsOutlinedIcon/>,
295+
onClick: () => { setIsSettingsModalOpen(true); },
296+
},
297+
timeoutMillis: LONG_AUTO_DISMISS_TIMEOUT_MILLIS,
298+
title: "A format string has not been configured",
299+
});
300+
break;
273301
case WORKER_RESP_CODE.LOG_FILE_INFO:
274302
setFileName(args.fileName);
275303
setNumEvents(args.numEvents);
@@ -418,14 +446,14 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
418446
loadPageByCursor(mainWorkerRef.current, cursor);
419447
}, []);
420448

421-
const setLogLevelFilter = useCallback((newLogLevelFilter: LogLevelFilter) => {
449+
const setLogLevelFilter = useCallback((filter: LogLevelFilter) => {
422450
if (null === mainWorkerRef.current) {
423451
return;
424452
}
425453
setUiState(UI_STATE.FAST_LOADING);
426454
workerPostReq(mainWorkerRef.current, WORKER_REQ_CODE.SET_FILTER, {
427455
cursor: {code: CURSOR_CODE.EVENT_NUM, args: {eventNum: logEventNumRef.current ?? 1}},
428-
logLevelFilter: newLogLevelFilter,
456+
logLevelFilter: filter,
429457
});
430458
}, []);
431459

@@ -510,6 +538,7 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
510538
beginLineNumToLogEventNum: beginLineNumToLogEventNumRef.current,
511539
exportProgress: exportProgress,
512540
fileName: fileName,
541+
isSettingsModalOpen: isSettingsModalOpen,
513542
logData: logData,
514543
numEvents: numEvents,
515544
numPages: numPages,
@@ -522,6 +551,7 @@ const StateContextProvider = ({children}: StateContextProviderProps) => {
522551
exportLogs: exportLogs,
523552
loadFile: loadFile,
524553
loadPageByAction: loadPageByAction,
554+
setIsSettingsModalOpen: setIsSettingsModalOpen,
525555
setLogLevelFilter: setLogLevelFilter,
526556
startQuery: startQuery,
527557
}}

src/services/MainWorker.ts

+10
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ const onQueryResults = (queryProgress: number, queryResults: QueryResults) => {
5050
postResp(WORKER_RESP_CODE.QUERY_RESULT, {progress: queryProgress, results: queryResults});
5151
};
5252

53+
/**
54+
* Sends a message to the renderer to open a pop-up which prompts user to replace the default
55+
* format string.
56+
*/
57+
const postFormatPopup = () => {
58+
postResp(WORKER_RESP_CODE.FORMAT_POPUP, null);
59+
};
60+
5361
// eslint-disable-next-line no-warning-comments
5462
// TODO: Break this function up into smaller functions.
5563
// eslint-disable-next-line max-lines-per-function,max-statements
@@ -149,3 +157,5 @@ onmessage = async (ev: MessageEvent<MainWorkerReqMessage>) => {
149157
}
150158
}
151159
};
160+
161+
export {postFormatPopup};

src/services/decoders/ClpIrDecoder.ts

+8-4
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {Formatter} from "../../typings/formatters";
1313
import {JsonObject} from "../../typings/js";
1414
import {LogLevelFilter} from "../../typings/logs";
1515
import YscopeFormatter from "../formatters/YscopeFormatter";
16+
import {postFormatPopup} from "../MainWorker";
1617
import {
1718
convertToDayjsTimestamp,
1819
isJsonObject,
@@ -29,7 +30,7 @@ class ClpIrDecoder implements Decoder {
2930

3031
readonly #streamType: CLP_IR_STREAM_TYPE;
3132

32-
#formatter: Nullable<Formatter>;
33+
#formatter: Nullable<Formatter> = null;
3334

3435
constructor (
3536
streamType: CLP_IR_STREAM_TYPE,
@@ -38,9 +39,12 @@ class ClpIrDecoder implements Decoder {
3839
) {
3940
this.#streamType = streamType;
4041
this.#streamReader = streamReader;
41-
this.#formatter = (streamType === CLP_IR_STREAM_TYPE.STRUCTURED) ?
42-
new YscopeFormatter({formatString: decoderOptions.formatString}) :
43-
null;
42+
if (streamType === CLP_IR_STREAM_TYPE.STRUCTURED) {
43+
this.#formatter = new YscopeFormatter({formatString: decoderOptions.formatString});
44+
if (0 === decoderOptions.formatString.length) {
45+
postFormatPopup();
46+
}
47+
}
4448
}
4549

4650
/**

src/services/decoders/JsonlDecoder/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
LogLevelFilter,
1818
} from "../../../typings/logs";
1919
import YscopeFormatter from "../../formatters/YscopeFormatter";
20+
import {postFormatPopup} from "../../MainWorker";
2021
import {
2122
convertToDayjsTimestamp,
2223
convertToLogLevelValue,
@@ -54,6 +55,9 @@ class JsonlDecoder implements Decoder {
5455
this.#logLevelKey = decoderOptions.logLevelKey;
5556
this.#timestampKey = decoderOptions.timestampKey;
5657
this.#formatter = new YscopeFormatter({formatString: decoderOptions.formatString});
58+
if (0 === decoderOptions.formatString.length) {
59+
postFormatPopup();
60+
}
5761
}
5862

5963
getEstimatedNumEvents (): number {

src/services/formatters/YscopeFormatter/index.ts

+6
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
import {LogEvent} from "../../../typings/logs";
1111
import {
1212
getFormattedField,
13+
jsonValueToString,
1314
removeEscapeCharacters,
1415
replaceDoubleBacklash,
1516
splitFieldPlaceholder,
@@ -37,6 +38,11 @@ class YscopeFormatter implements Formatter {
3738
}
3839

3940
formatLogEvent (logEvent: LogEvent): string {
41+
// Empty format string is special case where formatter returns all fields as JSON.
42+
if ("" === this.#processedFormatString) {
43+
return jsonValueToString(logEvent.fields);
44+
}
45+
4046
const formattedLogFragments: string[] = [];
4147
let lastIndex = 0;
4248

src/typings/formatters.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import {LogEvent} from "./logs";
44

55

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

5151
/**
52-
* Type for list of currently supported Yscope field formatters.
52+
* Type for list of currently supported YScope field formatters.
5353
*/
5454
type YscopeFieldFormatterMap = {
5555
[key: string]: new (options: Nullable<string>) => YscopeFieldFormatter;
5656
};
5757

5858
/**
59-
* Parsed field placeholder from a Yscope format string.
59+
* Parsed field placeholder from a YScope format string.
6060
*/
6161
type YscopeFieldPlaceholder = {
6262
fieldNameKeys: string[],

src/typings/notifications.ts

+9
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import {ButtonProps} from "@mui/joy";
2+
13
import {LOG_LEVEL} from "./logs";
24

35

@@ -9,6 +11,7 @@ interface PopUpMessage {
911
message: string,
1012
timeoutMillis: number,
1113
title: string,
14+
primaryAction?: ButtonProps,
1215
}
1316

1417
/**
@@ -21,9 +24,15 @@ const DO_NOT_TIMEOUT_VALUE = 0;
2124
*/
2225
const DEFAULT_AUTO_DISMISS_TIMEOUT_MILLIS = 10_000;
2326

27+
/**
28+
* A longer duration in milliseconds after which an automatic dismissal will occur.
29+
*/
30+
const LONG_AUTO_DISMISS_TIMEOUT_MILLIS = 20_000;
31+
2432

2533
export type {PopUpMessage};
2634
export {
2735
DEFAULT_AUTO_DISMISS_TIMEOUT_MILLIS,
2836
DO_NOT_TIMEOUT_VALUE,
37+
LONG_AUTO_DISMISS_TIMEOUT_MILLIS,
2938
};

0 commit comments

Comments
 (0)