-
Notifications
You must be signed in to change notification settings - Fork 18
feat: Add KQL query support for filtering structured IR log events (fixes #277). #371
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
Changes from all commits
c2c4c24
5be2cfa
2b33717
c39c378
e6acd7b
14874a7
33ffec6
580fc57
46758d4
88c2ce4
e140670
1f43d78
e6dcdc2
03baed3
7ed5631
8a5a8a9
be68751
ea972ce
fded0a5
6596f9b
ad63515
8a3b968
a3f82d4
889e7c6
53a9036
cd950f5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
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; | ||
} | ||
hoophalab marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
.filter-button { | ||
width: 48px; | ||
} | ||
hoophalab marked this conversation as resolved.
Show resolved
Hide resolved
|
hoophalab marked this conversation as resolved.
Show resolved
Hide resolved
|
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||
|
||
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); | ||
hoophalab marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Accessibility + Joy Textarea API + guideline on negation
<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
|
||
<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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||
|
||||||||||||||||||
.input-box { | ||||||||||||||||||
flex-direction: row !important; | ||||||||||||||||||
border-radius: 0 !important; | ||||||||||||||||||
} | ||||||||||||||||||
Comment on lines
+5
to
+8
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||||||||||||||||
|
||||||||||||||||||
.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; | ||||||||
|
@@ -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; | ||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents
|
||||||||
} | ||||||||
|
||||||||
.query-input-box-linear-progress { | ||||||||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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"; | ||||||||||||||||||||||||||||||||||||
|
@@ -15,6 +16,7 @@ import {updateWindowUrlHashParams} from "../../../../../utils/url"; | |||||||||||||||||||||||||||||||||||
import ToggleIconButton from "./ToggleIconButton"; | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
import "./QueryInputBox.css"; | ||||||||||||||||||||||||||||||||||||
import "./InputBox.css"; | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
/** | ||||||||||||||||||||||||||||||||||||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||||||||||||||||||||||||||||||||||||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🤖 Prompt for AI Agents
|
||||||||||||||||||||||||||||||||||||
endDecorator={ | ||||||||||||||||||||||||||||||||||||
|
@@ -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 | ||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment.
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 initsetLogEventNum(hashParams.logEventNum)
is currently only called whenfilePath
is non-default. If a user lands on the app without a file but with alogEventNum
in the hash (e.g., deep link), we silently drop that state. Set it unconditionally during initialisation.Apply this diff:
Also applies to: 69-71
🤖 Prompt for AI Agents