Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
c2c4c24
feat: Add KQL filtering support.
hoophalab Aug 19, 2025
5be2cfa
address coderabbit comments
hoophalab Aug 25, 2025
2b33717
rename filterString as query and queryString as subquery
hoophalab Aug 25, 2025
c39c378
address coderabbitai comments
hoophalab Aug 25, 2025
e6acd7b
update docs
hoophalab Aug 25, 2025
14874a7
fix scroll bar
hoophalab Aug 25, 2025
33ffec6
fix styles
hoophalab Aug 25, 2025
580fc57
Merge branch 'main' into kql-pr
hoophalab Aug 25, 2025
46758d4
make filterlogs single action; remove getinitalcursor;
hoophalab Aug 26, 2025
88c2ce4
fix timestamp being ignored on the initial page load
hoophalab Aug 26, 2025
e140670
Update src/utils/url/urlHash.ts
hoophalab Aug 26, 2025
1f43d78
Update src/stores/viewStore/createViewFilterSlice.ts
hoophalab Aug 26, 2025
e6dcdc2
Update src/stores/viewStore/createViewFilterSlice.ts
hoophalab Aug 26, 2025
03baed3
Update src/components/StatusBar/LogLevelSelect/index.tsx
hoophalab Aug 26, 2025
7ed5631
Update src/components/StatusBar/LogLevelSelect/index.tsx
hoophalab Aug 26, 2025
8a5a8a9
Update src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabP…
hoophalab Aug 26, 2025
be68751
lint
hoophalab Aug 26, 2025
ea972ce
Merge branch 'main' into kql-pr
junhaoliao Sep 2, 2025
fded0a5
Merge branch 'main' into kql-pr
junhaoliao Sep 2, 2025
6596f9b
fix: not-yet-applied kql filter changed after clicking log event
hoophalab Sep 2, 2025
ad63515
fix: incorrect page num in the search tab panel when filter is applied.
hoophalab Sep 2, 2025
8a3b968
fix: log level filter isn't cleared in zustand after reloading
hoophalab Sep 2, 2025
a3f82d4
Merge branch 'main' into kql-pr
junhaoliao Sep 2, 2025
889e7c6
shift+enter submits query; address comments; update placeholder
hoophalab Sep 2, 2025
53a9036
add kql docs link; add dynamic place holder for search input box
hoophalab Sep 2, 2025
cd950f5
address comments; fix filter isn't applied after reloading the file
hoophalab Sep 3, 2025
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
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"@mui/icons-material": "^7.1.0",
"@mui/joy": "^5.0.0-beta.52",
"axios": "^1.11.0",
"clp-ffi-js": "^0.6.0",
"clp-ffi-js": "^0.6.1",
"comlink": "^4.4.2",
"dayjs": "^1.11.13",
"js-beautify": "^1.15.4",
Expand Down
55 changes: 6 additions & 49 deletions src/components/AppController.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,9 @@ import useQueryStore from "../stores/queryStore";
import useUiStore from "../stores/uiStore";
import useViewStore from "../stores/viewStore";
import {TAB_NAME} from "../typings/tab";
import {
HASH_PARAM_NAMES,
UrlHashParams,
} from "../typings/url";
import {
CURSOR_CODE,
CursorType,
} from "../typings/worker";
import {
getWindowUrlHashParams,
getWindowUrlSearchParams,
updateWindowUrlHashParams,
URL_HASH_PARAMS_DEFAULT,
URL_SEARCH_PARAMS_DEFAULT,
} from "../utils/url";
import {
Expand All @@ -43,32 +33,6 @@ const handleHashChange = () => {
}
};

/**
* Returns the initial load file cursor based on the URL hash parameters.
*
* @param hashParams
* @return
*/
const getInitialCursor = (hashParams: UrlHashParams) => {
let cursor: CursorType = {code: CURSOR_CODE.LAST_EVENT, args: null};

if (URL_HASH_PARAMS_DEFAULT.timestamp !== hashParams.timestamp) {
cursor = {
code: CURSOR_CODE.TIMESTAMP,
args: {timestamp: hashParams.timestamp},
};
} else if (URL_HASH_PARAMS_DEFAULT.logEventNum !== hashParams.logEventNum) {
const {setLogEventNum} = useViewStore.getState();
setLogEventNum(hashParams.logEventNum);
cursor = {
code: CURSOR_CODE.EVENT_NUM,
args: {eventNum: hashParams.logEventNum},
};
}

return cursor;
};

interface AppControllerProps {
children: React.ReactNode;
}
Expand Down Expand Up @@ -96,26 +60,19 @@ const AppController = ({children}: AppControllerProps) => {

// Handle initial page load and maintain full URL state
const hashParams = getWindowUrlHashParams();
updateWindowUrlHashParams({
isPrettified: hashParams[HASH_PARAM_NAMES.IS_PRETTIFIED],
timestamp: URL_HASH_PARAMS_DEFAULT.timestamp,
});
const {setIsPrettified} = useViewStore.getState();
setIsPrettified(hashParams.isPrettified);

const searchParams = getWindowUrlSearchParams();
if (URL_SEARCH_PARAMS_DEFAULT.filePath !== searchParams.filePath) {
const {loadFile} = useLogFileStore.getState();
(async () => {
Comment on lines 61 to 68
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Apply logEventNum unconditionally at init

setLogEventNum(hashParams.logEventNum) is currently only called when filePath is non-default. If a user lands on the app without a file but with a logEventNum in the hash (e.g., deep link), we silently drop that state. Set it unconditionally during initialisation.

Apply this diff:

-        const hashParams = getWindowUrlHashParams();
-        const {setIsPrettified} = useViewStore.getState();
-        setIsPrettified(hashParams.isPrettified);
+        const hashParams = getWindowUrlHashParams();
+        const {setIsPrettified, setLogEventNum} = useViewStore.getState();
+        setIsPrettified(hashParams.isPrettified);
+        setLogEventNum(hashParams.logEventNum);
@@
-        if (URL_SEARCH_PARAMS_DEFAULT.filePath !== searchParams.filePath) {
-            (async () => {
-                const {setLogEventNum} = useViewStore.getState();
-                setLogEventNum(hashParams.logEventNum);
+        if (URL_SEARCH_PARAMS_DEFAULT.filePath !== searchParams.filePath) {
+            (async () => {
                 const {loadFile} = useLogFileStore.getState();
                 await loadFile(searchParams.filePath);
 
                 handleHashChange();
             })().catch(handleErrorWithNotification);
         }

Also applies to: 69-71

🤖 Prompt for AI Agents
In src/components/AppController.tsx around lines 61-71, the initialization only
applies setLogEventNum when searchParams.filePath differs from the default,
causing hashParams.logEventNum to be dropped for deep links without a file; call
setLogEventNum(hashParams.logEventNum) unconditionally during the initialisation
(move or add the call before the filePath conditional) so the logEventNum from
the URL hash is always applied.

const {setLogEventNum} = useViewStore.getState();
setLogEventNum(hashParams.logEventNum);

const {loadFile} = useLogFileStore.getState();
await loadFile(searchParams.filePath);
const {loadPageByCursor} = useViewStore.getState();
await loadPageByCursor(getInitialCursor(hashParams));
if (updateQueryHashParams()) {
const {setActiveTabName} = useUiStore.getState();
setActiveTabName(TAB_NAME.SEARCH);
const {startQuery} = useQueryStore.getState();
startQuery();
}

handleHashChange();
})().catch(handleErrorWithNotification);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.filter-input-box-textarea {
font-family: monospace !important;
word-break: break-all;
}

.filter-button {
width: 48px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import React, {useCallback} from "react";

import {
Button,
Textarea,
} from "@mui/joy";

import {handleErrorWithNotification} from "../../../../../stores/notificationStore";
import useQueryStore from "../../../../../stores/queryStore";
import useUiStore from "../../../../../stores/uiStore";
import useViewStore from "../../../../../stores/viewStore";
import {UI_ELEMENT} from "../../../../../typings/states";
import {CURSOR_CODE} from "../../../../../typings/worker";
import {isDisabled} from "../../../../../utils/states";

import "./FilterInputBox.css";
import "./InputBox.css";


/**
* Provides a text input to apply a KQL filter.
*
* @return
*/
const FilterInputBox = () => {
const kqlFilterInput = useViewStore((state) => state.kqlFilterInput);
const kqlFilter = useViewStore((state) => state.kqlFilter);
const uiState = useUiStore((state) => state.uiState);

const handleFilterInputChange = useCallback((ev: React.ChangeEvent<HTMLTextAreaElement>) => {
const newFilterString = ev.target.value;
const {setKqlFilterInput} = useViewStore.getState();
setKqlFilterInput(newFilterString);
}, []);
Comment on lines +30 to +34
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Optional: add Cmd/Ctrl+Enter shortcut to apply the filter.

     const handleFilterInputChange = useCallback((ev: React.ChangeEvent<HTMLTextAreaElement>) => {
         const newFilterString = ev.target.value;
         const {setKqlFilterInput} = useViewStore.getState();
         setKqlFilterInput(newFilterString);
     }, []);
+
+    const handleTextareaKeyDown = useCallback((ev: React.KeyboardEvent<HTMLTextAreaElement>) => {
+        const isCmdOrCtrl = ev.metaKey || ev.ctrlKey;
+        if (isCmdOrCtrl && "Enter" === ev.key) {
+            if (false == isFilterInputBoxDisabled && false == (false == isKqlFilterModified)) {
+                ev.preventDefault();
+                void handleFilterButtonClick();
+            }
+        }
+    }, [handleFilterButtonClick, isFilterInputBoxDisabled, isKqlFilterModified]);
@@
         <Textarea
@@
             value={kqlFilterInput}
+            onKeyDown={handleTextareaKeyDown}

Also applies to: 55-61

🤖 Prompt for AI Agents
In
src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/FilterInputBox.tsx
around lines 29-33 and 55-61, add a keyboard shortcut handler so Cmd/Ctrl+Enter
will apply the filter: attach an onKeyDown that detects ev.key === 'Enter' &&
(ev.metaKey || ev.ctrlKey) and then invoke the same apply logic you use for
applying the filter (e.g. call the existing submit/apply handler or call
setKqlFilterInput with the current textarea value and trigger the apply/submit
function). Ensure both places (29-33 and 55-61) include the new onKeyDown
behavior and reuse existing apply/submit code rather than duplicating logic.


const handleFilterButtonClick = useCallback(() => {
const {setKqlFilter, filterLogs, loadPageByCursor, logEventNum} = useViewStore.getState();
setKqlFilter(kqlFilterInput);
filterLogs();

(async () => {
await loadPageByCursor({
code: CURSOR_CODE.EVENT_NUM,
args: {eventNum: logEventNum},
});

const {startQuery} = useQueryStore.getState();
startQuery();
})().catch(handleErrorWithNotification);
}, [kqlFilterInput]);

const handleTextareaKeyDown = useCallback((ev: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (ev.shiftKey && "Enter" === ev.key) {
ev.preventDefault();
handleFilterButtonClick();
}
}, [handleFilterButtonClick]);

const isFilterInputBoxDisabled = isDisabled(uiState, UI_ELEMENT.QUERY_INPUT_BOX);
const isKqlFilterModified = kqlFilter !== kqlFilterInput;

return (
<Textarea
className={"input-box-container input-box"}
maxRows={7}
placeholder={"KQL log filter"}
size={"sm"}
value={kqlFilterInput}
endDecorator={
Comment on lines +62 to +69
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Accessibility + Joy Textarea API + guideline on negation

  • Add aria labels.
  • Pass disabled on Textarea (not via slotProps).
  • Follow guideline: prefer false == expr over !expr.
         <Textarea
-            className={"input-box-container input-box"}
+            className={"input-box-container input-box"}
             maxRows={7}
             placeholder={"KQL log filter"}
+            aria-label={"KQL filter"}
             size={"sm"}
             value={kqlFilterInput}
+            disabled={isFilterInputBoxDisabled}
             endDecorator={
                 <Button
                     className={"filter-button"}
-                    disabled={isFilterInputBoxDisabled || !isKqlFilterModified}
+                    disabled={isFilterInputBoxDisabled || false == isKqlFilterModified}
                     variant={"soft"}
                     onClick={handleFilterButtonClick}
+                    aria-label={"Apply filter"}
                 >
                     Filter
                 </Button>
             }
-            slotProps={{textarea: {
-                disabled: isFilterInputBoxDisabled,
-                className: "filter-input-box-textarea",
-            },
-            endDecorator: {className: "input-box-end-decorator"}}}
+            slotProps={{
+                textarea: {className: "filter-input-box-textarea"},
+                endDecorator: {className: "input-box-end-decorator"}
+            }}

Also applies to: 70-77, 79-85

🤖 Prompt for AI Agents
In
src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/FilterInputBox.tsx
around lines 62-69 (and similarly for 70-77 and 79-85), the Textarea lacks aria
labels, uses disabled via slotProps instead of the Textarea prop, and uses
negation with '!' rather than the preferred 'false == expr'; add appropriate
aria-label and/or aria-describedby attributes to the Textarea, move any disabled
state to the direct disabled prop on the Textarea (remove it from slotProps),
and replace instances of '!expr' with 'false == expr' to follow the guideline.

<Button
className={"filter-button"}
disabled={isFilterInputBoxDisabled || !isKqlFilterModified}
variant={"soft"}
onClick={handleFilterButtonClick}
>
Filter
</Button>
}
slotProps={{textarea: {
disabled: isFilterInputBoxDisabled,
className: "filter-input-box-textarea",
},
endDecorator: {className: "input-box-end-decorator"}}}
onChange={handleFilterInputChange}
onKeyDown={handleTextareaKeyDown}/>
);
};


export default FilterInputBox;
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.input-box-container {
width: 100%;
}
Comment on lines +1 to +3
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Harden width override for JoyUI and allow shrink

Per retrieved learnings, JoyUI often needs !important; also add min-width: 0 and box-sizing.

 .input-box-container {
-    width: 100%;
+    width: 100% !important;
+    min-width: 0;
+    box-sizing: border-box;
 }

Used retrieved learnings about acceptable use of !important to override JoyUI.

📝 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
.input-box-container {
width: 100%;
}
.input-box-container {
width: 100% !important;
min-width: 0;
box-sizing: border-box;
}
🤖 Prompt for AI Agents
In
src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/InputBox.css
lines 1-3, the .input-box-container width override can be ignored by JoyUI and
may prevent flex shrinking; update the rule to force the width and allow
shrinking by using width: 100% !important, add min-width: 0, box-sizing:
border-box, and flex-shrink: 1 so the container respects parent flex constraints
and the override reliably applies.


.input-box {
flex-direction: row !important;
border-radius: 0 !important;
}
Comment on lines +5 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)

Ensure layout intent applies regardless of JoyUI internals

Be explicit so flex-direction reliably takes effect.

 .input-box {
-    flex-direction: row !important;
+    display: flex !important;
+    flex-direction: row !important;
     border-radius: 0 !important;
 }
🤖 Prompt for AI Agents
In
src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/InputBox.css
around lines 5 to 8, the rule sets flex-direction but doesn’t guarantee the
element is a flex container; modify the selector to explicitly set display: flex
!important (and optionally align-items/justify-content as needed) so the
flex-direction: row !important reliably applies regardless of JoyUI internals.


.input-box-end-decorator {
display: block !important;
margin-block-start: 0 !important;
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,3 @@
.query-input-box-with-progress {
/* JoyUI has a rounding issue when calculating the Textarea width, causing it to overflow its
container. */
margin-right: 1px;
}

.query-input-box {
flex-direction: row !important;
border-radius: 0 !important;
}

.query-option-button {
width: 1.5rem !important;
min-width: 0 !important;
Expand All @@ -20,11 +9,7 @@

.query-input-box-textarea {
width: 0;
}

.query-input-box-end-decorator {
display: block !important;
margin-block-start: 0 !important;
font-family: monospace !important;
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Unify monospace stack with filter box

Use a cross‑platform stack for visual consistency.

-    font-family: monospace !important;
+    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
+                 "Courier New", monospace !important;
📝 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
font-family: monospace !important;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
"Courier New", monospace !important;
🤖 Prompt for AI Agents
In
src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/QueryInputBox.css
around line 12, the rule currently forces a generic monospace with !important;
replace it with a cross-platform monospace stack to match the filter box (e.g.
ui-monospace, SFMono-Regular, Menlo, Monaco, "Roboto Mono", "Segoe UI Mono",
"Ubuntu Mono", "Courier New", monospace) and remove the !important (or only keep
it if necessary) so the input uses the unified, consistent font stack across
platforms.

}

.query-input-box-linear-progress {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
Textarea,
} from "@mui/joy";

import useLogFileStore from "../../../../../stores/logFileStore";
import useQueryStore from "../../../../../stores/queryStore";
import useUiStore from "../../../../../stores/uiStore";
import {QUERY_PROGRESS_VALUE_MAX} from "../../../../../typings/query";
Expand All @@ -15,6 +16,7 @@ import {updateWindowUrlHashParams} from "../../../../../utils/url";
import ToggleIconButton from "./ToggleIconButton";

import "./QueryInputBox.css";
import "./InputBox.css";


/**
Expand All @@ -28,10 +30,11 @@ const QueryInputBox = () => {
const querystring = useQueryStore((state) => state.queryString);
const queryProgress = useQueryStore((state) => state.queryProgress);
const uiState = useUiStore((state) => state.uiState);
const fileTypeInfo = useLogFileStore((state) => state.fileTypeInfo);

Comment on lines +33 to 34
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Avoid magic strings for file types

Consider a shared enum/constant to prevent drift (“CLP IR”).

Example (outside range):

export const FILE_TYPES = { CLP_IR: "CLP IR" } as const;
// usage:
"CLP IR" === fileTypeInfo.name // → FILE_TYPES.CLP_IR === fileTypeInfo.name

const handleQueryInputChange = useCallback((ev: React.ChangeEvent<HTMLTextAreaElement>) => {
const newQueryString = ev.target.value;
updateWindowUrlHashParams({queryString: newQueryString});
updateWindowUrlHashParams({subquery: newQueryString});
const {setQueryString, startQuery} = useQueryStore.getState();
setQueryString(newQueryString);
startQuery();
Expand All @@ -55,12 +58,19 @@ const QueryInputBox = () => {

const isQueryInputBoxDisabled = isDisabled(uiState, UI_ELEMENT.QUERY_INPUT_BOX);

const isKqlFilteringEnabled = null !== fileTypeInfo &&
"CLP IR" === fileTypeInfo.name &&
true === fileTypeInfo.isStructured;
const placeholder = isKqlFilteringEnabled ?
"Search (in filtered logs)" :
"Search";

return (
<div className={"query-input-box-with-progress"}>
<div className={"input-box-container"}>
<Textarea
className={"query-input-box"}
className={"input-box"}
maxRows={7}
placeholder={"Search"}
placeholder={placeholder}
size={"sm"}
value={querystring}
Comment on lines +69 to 75
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 an accessible name to the Textarea

Placeholder isn’t a reliable accessible name once text is entered.

-            <Textarea
+            <Textarea
                 className={"input-box"}
                 maxRows={7}
-                placeholder={placeholder}
+                placeholder={placeholder}
+                aria-label={placeholder}
                 size={"sm"}
📝 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
<div className={"input-box-container"}>
<Textarea
className={"query-input-box"}
className={"input-box"}
maxRows={7}
placeholder={"Search"}
placeholder={placeholder}
size={"sm"}
value={querystring}
<div className={"input-box-container"}>
<Textarea
className={"input-box"}
maxRows={7}
placeholder={placeholder}
aria-label={placeholder}
size={"sm"}
value={querystring}
🤖 Prompt for AI Agents
In
src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/QueryInputBox.tsx
around lines 69 to 75, the Textarea relies on placeholder for an accessible name
which is unreliable once the user types; add a proper accessible name by
providing either an aria-label prop with a concise descriptive string (e.g.,
"Search query input") or by associating a visible label with the textarea via id
and aria-labelledby; implement one of these options and keep the existing
placeholder intact.

endDecorator={
Expand Down Expand Up @@ -98,7 +108,7 @@ const QueryInputBox = () => {
className: "query-input-box-textarea",
disabled: isQueryInputBoxDisabled,
},
endDecorator: {className: "query-input-box-end-decorator"},
endDecorator: {className: "input-box-end-decorator"},
}}
onChange={handleQueryInputChange}/>
<LinearProgress
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.search-tab-container {
overflow-y: hidden;
/* We apply overflow-x here because JoyUI has a rounding issue when calculating the Textarea width, causing it to overflow its container. */
overflow: hidden;
display: flex;
flex-direction: column;
height: 100%;
Expand All @@ -9,3 +10,9 @@
overflow-y: auto;
flex-grow: 1;
}

.query-input-boxes-container {
display: flex;
flex-direction: column;
gap: 8px;
}
Loading