Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ function Terminal({

const handleLineClick = useCallback(
(event: React.MouseEvent<HTMLSpanElement>) => {
const selection = window.getSelection();
if (selection && selection.toString().length > 0) {
return;
}

const lineElement = (event.target as HTMLElement).closest('.log-line');
if (lineElement) {
lineElement.classList.toggle('expanded');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AutocompleteSuggestion, LOG_LEVELS } from './types';
import { AutocompleteSuggestion, LOG_LEVELS, LOG_LEVEL_COLORS } from './types';

const LEVEL_DESCRIPTIONS: Record<string, string> = {
error: 'Error messages',
Expand Down Expand Up @@ -27,6 +27,7 @@ export function getAutocomplete(query: string): AutocompleteState {
label: level,
value: level,
description: LEVEL_DESCRIPTIONS[level],
color: LOG_LEVEL_COLORS[level],
}));
return { mode: 'value', suggestions };
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { getAutocomplete, applySuggestion } from './autocomplete';
import { useLogsContext } from './use-logs';
import {
Command,
CommandEmpty,
CommandGroup,
CommandItem,
CommandList,
Expand All @@ -28,7 +27,7 @@ export function LogSearchInput({
const inputRef = useRef<HTMLInputElement>(null);
const blurTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const [isOpen, setIsOpen] = useState(false);
const [selectedIndex, setSelectedIndex] = useState(0);
const [selectedIndex, setSelectedIndex] = useState<number>();
const [localValue, setLocalValue] = useState(queryString);

useEffect(() => {
Expand All @@ -41,6 +40,23 @@ export function LogSearchInput({
setQueryString(localValue);
}, [localValue, setQueryString]);

const handleFilterChipClick = useCallback(
(filterKey: string) => {
const newValue = localValue ? `${localValue} ${filterKey}` : filterKey;
setLocalValue(newValue);
setIsOpen(true);
setSelectedIndex(undefined);
setTimeout(() => {
const input = inputRef.current;
if (input) {
input.focus();
input.setSelectionRange(newValue.length, newValue.length);
}
}, 0);
},
[localValue],
);

const handleSelect = useCallback(
(index: number) => {
const suggestion = suggestions[index];
Expand All @@ -64,13 +80,13 @@ export function LogSearchInput({
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
if (isOpen && suggestions.length > 0) {
e.preventDefault();
e.preventDefault();
if (isOpen && suggestions.length > 0 && selectedIndex !== undefined) {
handleSelect(selectedIndex);
} else {
e.preventDefault();
submitSearch();
}
setIsOpen(false);
return;
}

Expand All @@ -80,23 +96,36 @@ export function LogSearchInput({

if (e.key === 'Escape') {
setIsOpen(false);
setSelectedIndex(undefined);
} else if (e.key === 'ArrowDown') {
e.preventDefault();
setSelectedIndex((i) => (i < suggestions.length - 1 ? i + 1 : 0));
setSelectedIndex((i) => {
if (i === undefined) {
return 0;
}
return i < suggestions.length - 1 ? i + 1 : 0;
});
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setSelectedIndex((i) => (i > 0 ? i - 1 : suggestions.length - 1));
setSelectedIndex((i) => {
if (i === undefined) {
return suggestions.length - 1;
}
return i > 0 ? i - 1 : suggestions.length - 1;
});
} else if (e.key === 'Tab') {
e.preventDefault();
handleSelect(selectedIndex);
if (selectedIndex !== undefined) {
e.preventDefault();
handleSelect(selectedIndex);
}
}
},
[isOpen, suggestions.length, selectedIndex, handleSelect, submitSearch],
);

return (
<div className={cn('space-y-2', className)}>
<Popover open={isOpen && suggestions.length > 0} modal={false}>
<Popover open={isOpen} modal={false}>
<PopoverTrigger asChild>
<div className="relative">
<MagnifyingGlassIcon className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
Expand All @@ -107,17 +136,16 @@ export function LogSearchInput({
onChange={(e) => {
setLocalValue(e.target.value);
setIsOpen(true);
setSelectedIndex(0);
setSelectedIndex(undefined);
}}
onKeyDown={handleKeyDown}
onFocus={() => {
if (blurTimeoutRef.current) {
clearTimeout(blurTimeoutRef.current);
blurTimeoutRef.current = null;
}
if (suggestions.length > 0) {
setIsOpen(true);
}
setIsOpen(true);
setSelectedIndex(undefined);
}}
onBlur={() => {
blurTimeoutRef.current = setTimeout(() => {
Expand Down Expand Up @@ -149,42 +177,74 @@ export function LogSearchInput({
onOpenAutoFocus={(e) => e.preventDefault()}
onCloseAutoFocus={(e) => e.preventDefault()}
>
<Command>
<CommandList className="max-h-[300px]">
<CommandEmpty>No suggestions</CommandEmpty>
<CommandGroup>
{suggestions.map((suggestion, index) => (
<CommandItem
key={suggestion.value}
onSelect={() => handleSelect(index)}
className={cn(
'flex items-center justify-between',
index === selectedIndex &&
'bg-accent text-accent-foreground',
)}
onMouseEnter={() => setSelectedIndex(index)}
>
<div className="flex items-center gap-2">
{suggestion.type === 'key' ? (
<code className="px-1.5 py-0.5 bg-muted rounded text-xs font-mono">
{suggestion.label}:
</code>
) : (
<span className="font-mono text-sm">
{suggestion.label}
{suggestions.length > 0 && (
<Command
value={
selectedIndex !== undefined
? suggestions[selectedIndex]?.value
: ''
}
>
<CommandList className="max-h-[300px]">
<CommandGroup>
{suggestions.map((suggestion, index) => (
<CommandItem
key={suggestion.value}
value={suggestion.value}
onSelect={() => handleSelect(index)}
className={cn(
'flex items-center justify-between',
selectedIndex !== undefined &&
index === selectedIndex &&
'bg-accent text-accent-foreground',
)}
onMouseEnter={() => setSelectedIndex(index)}
>
<div className="flex items-center gap-2">
{suggestion.color && (
<div
className={cn(
suggestion.color,
'h-[6px] w-[6px] rounded-full',
)}
/>
)}
{suggestion.type === 'key' ? (
<code className="px-1.5 py-0.5 bg-muted rounded text-xs font-mono">
{suggestion.label}:
</code>
) : (
<span className="font-mono text-sm">
{suggestion.label}
</span>
)}
</div>
{suggestion.description && (
<span className="text-xs text-muted-foreground truncate ml-2">
{suggestion.description}
</span>
)}
</div>
{suggestion.description && (
<span className="text-xs text-muted-foreground truncate ml-2">
{suggestion.description}
</span>
)}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
)}
<div
className={cn(
'flex items-center gap-2 px-3 py-2 text-xs',
suggestions.length > 0 && 'border-t',
)}
>
<span className="text-muted-foreground">Available filters:</span>
<button
type="button"
onClick={() => handleFilterChipClick('level:')}
className="inline-flex items-center rounded-md border border-input px-2 py-0.5 text-xs font-medium transition-colors hover:bg-accent hover:text-accent-foreground"
>
Level
</button>
</div>
</PopoverContent>
</Popover>
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
export const LOG_LEVELS = ['error', 'warn', 'info', 'debug'] as const;
export type LogLevel = (typeof LOG_LEVELS)[number];

export const LOG_LEVEL_COLORS: Record<LogLevel, string> = {
error: 'bg-red-500',
warn: 'bg-yellow-500',
info: 'bg-green-500',
debug: 'bg-slate-500',
};

export interface ParsedLogQuery {
search?: string;
level?: LogLevel;
Expand All @@ -14,6 +21,7 @@ export interface AutocompleteSuggestion {
label: string;
value: string;
description?: string;
color?: string;
}

export interface LogSearchInputProps {
Expand Down
Loading