-
Notifications
You must be signed in to change notification settings - Fork 298
Feat: Log Search Frontend, Part I #2830
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
Merged
Merged
Changes from all commits
Commits
Show all changes
51 commits
Select commit
Hold shift + click to select a range
12ff6e1
feat: initial work on new logs component
mrkaye97 6d93538
feat: improve autocomplete, refactor a bit
mrkaye97 3cebb09
refactor: remove some unused stuff
mrkaye97 083dc9f
refactor: more cleanup
mrkaye97 8b7c8ac
fix: on click handler
mrkaye97 d35b598
refactor: remove duped data, do some cleanup
mrkaye97 077bd0f
chore: simplify a bunch
mrkaye97 df56520
fix: pass level through
mrkaye97 6ae5da8
chore: cleanup
mrkaye97 871f0c4
fix: tsc
mrkaye97 a7f2945
chore: remove a bunch more stuff
mrkaye97 3116508
chore: comments
mrkaye97 c730261
fix: revert existing log component, add new filterable one
mrkaye97 3fef744
refactor: create use logs hook, simplify step-run-logs.tsx
mrkaye97 e21d176
chore: rename
mrkaye97 5743f69
chore: rename
mrkaye97 0d5c5a1
chore: fix tsc
mrkaye97 6c0cc17
feat: add a flag to enable the new experience
mrkaye97 dd4a16d
chore: lint
mrkaye97 3bd1dac
feat: provider
mrkaye97 ce6d73c
chore: lint
mrkaye97 8efd75e
fix: cursor jumping
mrkaye97 109305c
feat: try to use ghostty-web for rendering log lines instead of ansiT…
abelanger5 8efa791
fix: start getting the log viewer scrolling properly again
mrkaye97 47ce67f
fix: more scroll improvements
mrkaye97 c1fbf9e
feat: replace ghostty
mrkaye97 97d9424
chore: cleanup
mrkaye97 3655d9c
fix: dynamic height and width
mrkaye97 0d11e92
chore: lint
mrkaye97 42d0728
chore: format css
mrkaye97 0b22da8
chore: remove unused ref and styles
mrkaye97 0258c39
chore: cleanup
mrkaye97 81e6baa
fix: terminal styling
mrkaye97 40648f3
fix: scroll behavior / polling behavior while tasks are running still
mrkaye97 ff93c81
fix: jump to top on search change
mrkaye97 dc04df7
fix: improve loading / empty states
mrkaye97 9de8f90
chore: lint
mrkaye97 95f5262
Merge branch 'main' into mk/logs-fe-part-i
mrkaye97 8f92c1b
fix: status
mrkaye97 6256e5b
Merge branch 'mk/logs-fe-part-i' of https://github.com/hatchet-dev/ha…
mrkaye97 1102226
fix: enter to submit search
mrkaye97 1ccf925
fix: remove color var to fix ansi highlighting
mrkaye97 429347e
feat: expand / collapse on click
mrkaye97 7d089f2
chore: rm unused code
mrkaye97 c0376ac
Add edge detection for scroll callbacks to prevent repeated API calls…
Copilot 7de68a2
Fix polling loop to prevent overlapping requests in log search (#2845)
Copilot 4263513
chore: lint
mrkaye97 e85be47
Merge branch 'main' into mk/logs-fe-part-i
mrkaye97 8206a8c
fix: dedupe log lines
mrkaye97 cf174ea
fix: remove duped component
mrkaye97 3605cc8
fix: pass order by direction
mrkaye97 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
117 changes: 117 additions & 0 deletions
117
frontend/app/src/components/v1/cloud/logging/components/Terminal.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| import { cn } from '@/lib/utils'; | ||
| import { LazyLog, ScrollFollow } from '@melloware/react-logviewer'; | ||
| import { useCallback, useRef } from 'react'; | ||
|
|
||
| interface TerminalProps { | ||
| logs: string; | ||
| autoScroll?: boolean; | ||
| onScrollToTop?: () => void; | ||
| onScrollToBottom?: () => void; | ||
| onAtTopChange?: (atTop: boolean) => void; | ||
| className?: string; | ||
| } | ||
|
|
||
| function Terminal({ | ||
| logs, | ||
| autoScroll = false, | ||
| onScrollToTop, | ||
| onScrollToBottom, | ||
| onAtTopChange, | ||
| className, | ||
| }: TerminalProps) { | ||
| const lastScrollTopRef = useRef(0); | ||
| const wasAtTopRef = useRef(true); | ||
| const wasInTopRegionRef = useRef(false); | ||
| const wasInBottomRegionRef = useRef(false); | ||
|
|
||
| const handleLineClick = useCallback( | ||
| (event: React.MouseEvent<HTMLSpanElement>) => { | ||
| const lineElement = (event.target as HTMLElement).closest('.log-line'); | ||
| if (lineElement) { | ||
| lineElement.classList.toggle('expanded'); | ||
| } | ||
| }, | ||
| [], | ||
| ); | ||
|
|
||
| const handleScroll = useCallback( | ||
| ({ | ||
| scrollTop, | ||
| scrollHeight, | ||
| clientHeight, | ||
| }: { | ||
| scrollTop: number; | ||
| scrollHeight: number; | ||
| clientHeight: number; | ||
| }) => { | ||
| const scrollableHeight = scrollHeight - clientHeight; | ||
| if (scrollableHeight <= 0) { | ||
| return; | ||
| } | ||
|
|
||
| const scrollPercentage = scrollTop / scrollableHeight; | ||
| const isScrollingUp = scrollTop < lastScrollTopRef.current; | ||
| const isScrollingDown = scrollTop > lastScrollTopRef.current; | ||
|
|
||
| const isAtTop = scrollPercentage < 0.05; | ||
| if (onAtTopChange && isAtTop !== wasAtTopRef.current) { | ||
| wasAtTopRef.current = isAtTop; | ||
| onAtTopChange(isAtTop); | ||
| } | ||
|
|
||
| // Near top (newest logs with newest-first) - for running tasks | ||
| // Only fire when entering the region (edge detection) | ||
| // The region is defined by both scroll direction AND position, so changing | ||
| // direction automatically resets the region state | ||
| const isInTopRegion = isScrollingUp && scrollPercentage < 0.3; | ||
| if (isInTopRegion && !wasInTopRegionRef.current && onScrollToTop) { | ||
| onScrollToTop(); | ||
| } | ||
| wasInTopRegionRef.current = isInTopRegion; | ||
|
|
||
| // Near bottom (older logs with newest-first) - for infinite scroll | ||
| // Only fire when entering the region (edge detection) | ||
| // The region is defined by both scroll direction AND position, so changing | ||
| // direction automatically resets the region state | ||
| const isInBottomRegion = isScrollingDown && scrollPercentage > 0.7; | ||
| if ( | ||
| isInBottomRegion && | ||
| !wasInBottomRegionRef.current && | ||
| onScrollToBottom | ||
| ) { | ||
| onScrollToBottom(); | ||
| } | ||
| wasInBottomRegionRef.current = isInBottomRegion; | ||
|
|
||
| lastScrollTopRef.current = scrollTop; | ||
| }, | ||
| [onScrollToTop, onScrollToBottom, onAtTopChange], | ||
| ); | ||
|
|
||
| return ( | ||
| <div | ||
| className={cn( | ||
| 'terminal-root h-[500px] md:h-[600px] rounded-md w-full overflow-hidden', | ||
| className, | ||
| )} | ||
| > | ||
| <ScrollFollow | ||
| startFollowing={autoScroll} | ||
| render={({ follow, onScroll }) => ( | ||
| <LazyLog | ||
| text={logs} | ||
| follow={follow} | ||
| onScroll={(args) => { | ||
| onScroll(args); | ||
| handleScroll(args); | ||
| }} | ||
| onLineContentClick={handleLineClick} | ||
| selectableLines | ||
| /> | ||
| )} | ||
| /> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export default Terminal; | ||
85 changes: 85 additions & 0 deletions
85
frontend/app/src/components/v1/cloud/logging/log-search/autocomplete.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| import { AutocompleteSuggestion, LOG_LEVELS } from './types'; | ||
|
|
||
| const LEVEL_DESCRIPTIONS: Record<string, string> = { | ||
| error: 'Error messages', | ||
| warn: 'Warning messages', | ||
| info: 'Informational messages', | ||
| debug: 'Debug messages', | ||
| }; | ||
|
|
||
| export type AutocompleteMode = 'key' | 'value' | 'none'; | ||
|
|
||
| export interface AutocompleteState { | ||
| mode: AutocompleteMode; | ||
| suggestions: AutocompleteSuggestion[]; | ||
| } | ||
|
|
||
| export function getAutocomplete(query: string): AutocompleteState { | ||
| const trimmed = query.trimEnd(); | ||
| const lastWord = trimmed.split(' ').pop() || ''; | ||
|
|
||
| if (lastWord.startsWith('level:')) { | ||
| const partial = lastWord.slice(6).toLowerCase(); | ||
| const suggestions = LOG_LEVELS.filter((level) => | ||
| level.startsWith(partial), | ||
| ).map((level) => ({ | ||
| type: 'value' as const, | ||
| label: level, | ||
| value: level, | ||
| description: LEVEL_DESCRIPTIONS[level], | ||
| })); | ||
| return { mode: 'value', suggestions }; | ||
| } | ||
|
|
||
| if ('level:'.startsWith(lastWord.toLowerCase()) && lastWord.length > 0) { | ||
| return { | ||
| mode: 'key', | ||
| suggestions: [ | ||
| { | ||
| type: 'key', | ||
| label: 'level', | ||
| value: 'level:', | ||
| description: 'Filter by log level', | ||
| }, | ||
| ], | ||
| }; | ||
| } | ||
|
|
||
| if (trimmed === '' || query.endsWith(' ')) { | ||
| return { | ||
| mode: 'key', | ||
| suggestions: [ | ||
| { | ||
| type: 'key', | ||
| label: 'level', | ||
| value: 'level:', | ||
| description: 'Filter by log level', | ||
| }, | ||
| ], | ||
| }; | ||
| } | ||
|
|
||
| return { mode: 'none', suggestions: [] }; | ||
| } | ||
|
|
||
| export function applySuggestion( | ||
| query: string, | ||
| suggestion: AutocompleteSuggestion, | ||
| ): string { | ||
| const trimmed = query.trimEnd(); | ||
| const words = trimmed.split(' '); | ||
| const lastWord = words.pop() || ''; | ||
|
|
||
| if (suggestion.type === 'value') { | ||
| const prefix = lastWord.slice(0, lastWord.indexOf(':') + 1); | ||
| words.push(prefix + suggestion.value); | ||
| } else { | ||
| if (lastWord && 'level:'.startsWith(lastWord.toLowerCase())) { | ||
| words.push(suggestion.value); | ||
| } else { | ||
| words.push(lastWord, suggestion.value); | ||
| } | ||
| } | ||
|
|
||
| return words.filter(Boolean).join(' '); | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.