feat(logs): add LogSearchHelp component for enhanced search syntax guidance

This commit is contained in:
Nawaz Dhandala
2026-03-07 12:39:22 +00:00
parent fe883919c3
commit bf2c26169c
2 changed files with 147 additions and 0 deletions

View File

@@ -10,6 +10,7 @@ import React, {
import Icon from "../../Icon/Icon";
import IconProp from "../../../../Types/Icon/IconProp";
import LogSearchSuggestions from "./LogSearchSuggestions";
import LogSearchHelp from "./LogSearchHelp";
export interface LogSearchBarProps {
value: string;
@@ -24,6 +25,7 @@ const LogSearchBar: FunctionComponent<LogSearchBarProps> = (
): ReactElement => {
const [isFocused, setIsFocused] = useState<boolean>(false);
const [showSuggestions, setShowSuggestions] = useState<boolean>(false);
const [showHelp, setShowHelp] = useState<boolean>(false);
const [selectedSuggestionIndex, setSelectedSuggestionIndex] =
useState<number>(-1);
const inputRef: React.RefObject<HTMLInputElement> =
@@ -48,6 +50,10 @@ const LogSearchBar: FunctionComponent<LogSearchBarProps> = (
filteredSuggestions.length > 0 &&
currentWord.length > 0;
// Show help when focused, input is empty, and no suggestions visible
const shouldShowHelp: boolean =
showHelp && isFocused && props.value.length === 0 && !shouldShowSuggestions;
useEffect(() => {
setSelectedSuggestionIndex(-1);
}, [currentWord]);
@@ -68,11 +74,13 @@ const LogSearchBar: FunctionComponent<LogSearchBarProps> = (
props.onSubmit();
setShowSuggestions(false);
setShowHelp(false);
return;
}
if (e.key === "Escape") {
setShowSuggestions(false);
setShowHelp(false);
return;
}
@@ -114,6 +122,16 @@ const LogSearchBar: FunctionComponent<LogSearchBarProps> = (
props.onChange(parts.join(" "));
setShowSuggestions(false);
setShowHelp(false);
inputRef.current?.focus();
},
[props],
);
const handleExampleClick: (example: string) => void = useCallback(
(example: string): void => {
props.onChange(example);
setShowHelp(false);
inputRef.current?.focus();
},
[props],
@@ -128,6 +146,7 @@ const LogSearchBar: FunctionComponent<LogSearchBarProps> = (
!containerRef.current.contains(e.target as Node)
) {
setShowSuggestions(false);
setShowHelp(false);
}
};
@@ -157,10 +176,14 @@ const LogSearchBar: FunctionComponent<LogSearchBarProps> = (
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
props.onChange(e.target.value);
setShowSuggestions(true);
setShowHelp(false);
}}
onFocus={() => {
setIsFocused(true);
setShowSuggestions(true);
if (props.value.length === 0) {
setShowHelp(true);
}
}}
onBlur={() => {
setIsFocused(false);
@@ -180,6 +203,8 @@ const LogSearchBar: FunctionComponent<LogSearchBarProps> = (
className="flex-none rounded-full p-1 text-gray-400 hover:bg-gray-100"
onClick={() => {
props.onChange("");
setShowHelp(true);
setShowSuggestions(false);
inputRef.current?.focus();
}}
title="Clear search"
@@ -196,6 +221,10 @@ const LogSearchBar: FunctionComponent<LogSearchBarProps> = (
onSelect={applySuggestion}
/>
)}
{shouldShowHelp && (
<LogSearchHelp onExampleClick={handleExampleClick} />
)}
</div>
);
};

View File

@@ -0,0 +1,118 @@
import React, { FunctionComponent, ReactElement } from "react";
export interface LogSearchHelpProps {
onExampleClick?: ((example: string) => void) | undefined;
}
interface HelpRow {
syntax: string;
description: string;
example: string;
}
const HELP_ROWS: Array<HelpRow> = [
{
syntax: "free text",
description: "Search log messages",
example: "connection refused",
},
{
syntax: '"quoted phrase"',
description: "Exact phrase match",
example: '"out of memory"',
},
{
syntax: "severity:<level>",
description: "Filter by log level",
example: "severity:error",
},
{
syntax: "service:<name>",
description: "Filter by service",
example: "service:api",
},
{
syntax: "@<attr>:<value>",
description: "Filter by attribute",
example: "@http.status_code:500",
},
{
syntax: "-field:value",
description: "Exclude matching logs",
example: "-severity:debug",
},
{
syntax: "field:value*",
description: "Wildcard match",
example: "service:api-*",
},
{
syntax: "@attr:>N",
description: "Numeric comparison",
example: "@duration:>1000",
},
];
const LogSearchHelp: FunctionComponent<LogSearchHelpProps> = (
props: LogSearchHelpProps,
): ReactElement => {
return (
<div className="absolute left-0 top-full z-50 mt-1 w-[36rem] overflow-hidden rounded-lg border border-gray-200 bg-white shadow-lg">
<div className="border-b border-gray-100 px-3 py-2">
<span className="text-[11px] font-semibold uppercase tracking-wider text-gray-400">
Search syntax
</span>
</div>
<table className="w-full">
<tbody>
{HELP_ROWS.map((row: HelpRow) => {
return (
<tr
key={row.syntax}
className="cursor-pointer transition-colors hover:bg-gray-50"
onMouseDown={(e: React.MouseEvent) => {
e.preventDefault();
if (props.onExampleClick) {
props.onExampleClick(row.example);
}
}}
>
<td className="whitespace-nowrap py-1.5 pl-3 pr-2">
<code className="font-mono text-xs text-indigo-600">
{row.syntax}
</code>
</td>
<td className="py-1.5 px-2">
<span className="text-xs text-gray-500">
{row.description}
</span>
</td>
<td className="whitespace-nowrap py-1.5 pl-2 pr-3 text-right">
<code className="font-mono text-[11px] text-gray-400">
{row.example}
</code>
</td>
</tr>
);
})}
</tbody>
</table>
<div className="border-t border-gray-100 px-3 py-1.5">
<span className="text-[10px] text-gray-400">
Press{" "}
<kbd className="rounded border border-gray-200 bg-gray-50 px-1 py-0.5 font-mono text-[10px]">
Enter
</kbd>{" "}
to search · Combine filters:{" "}
<code className="font-mono text-[10px] text-gray-500">
severity:error service:api &quot;timeout&quot;
</code>
</span>
</div>
</div>
);
};
export default LogSearchHelp;