mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
feat(logs): add LogSearchHelp component for enhanced search syntax guidance
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
118
Common/UI/Components/LogsViewer/components/LogSearchHelp.tsx
Normal file
118
Common/UI/Components/LogsViewer/components/LogSearchHelp.tsx
Normal 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 "timeout"
|
||||
</code>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogSearchHelp;
|
||||
Reference in New Issue
Block a user