@@ -15,6 +15,7 @@ import Link from "next/link";
1515import { useHotkeys } from "react-hotkeys-hook" ;
1616import { KeyboardShortcut } from "@ui/KeyboardShortcut" ;
1717import { Callout } from "@ui/Callout" ;
18+ import { ITEM_SIZE } from "@common/features/logs/components/LogListItem" ;
1819import { FunctionCallTree } from "./FunctionCallTree" ;
1920import { LogMetadata } from "./LogMetadata" ;
2021import { InterleavedLog , getTimestamp , getLogKey } from "../lib/interleaveLogs" ;
@@ -28,6 +29,7 @@ export function LogDrilldown({
2829 onHitBoundary,
2930 shownInterleavedLogs,
3031 allUdfLogs,
32+ logListContainerRef,
3133} : {
3234 requestId ?: string ;
3335 shownInterleavedLogs : InterleavedLog [ ] ;
@@ -37,6 +39,7 @@ export function LogDrilldown({
3739 onFilterByRequestId ?: ( requestId : string ) => void ;
3840 onSelectLog : ( log : InterleavedLog ) => void ;
3941 onHitBoundary : ( boundary : "top" | "bottom" | null ) => void ;
42+ logListContainerRef ?: React . RefObject < HTMLDivElement > ;
4043} ) {
4144 const [ selectedTabIndex , setSelectedTabIndex ] = useState ( 0 ) ;
4245 const tabGroupRef = useRef < HTMLDivElement > ( null ) ;
@@ -51,6 +54,7 @@ export function LogDrilldown({
5154 onClose ,
5255 onHitBoundary ,
5356 rightPanelRef ,
57+ logListContainerRef ,
5458 ) ;
5559
5660 if ( ! selectedLog ) {
@@ -267,49 +271,130 @@ export function LogDrilldown({
267271 ) ;
268272}
269273
274+ const shortcutItemClass = "grid grid-cols-[1fr_9.5rem] gap-x-2 min-w-0" ;
275+ const shortcutKeysClass = "flex items-center justify-end gap-1" ;
276+ const shortcutLabelClass = "truncate min-w-0" ;
277+
270278function KeyboardShortcutsSection ( {
271279 selectedLog,
272280} : {
273281 selectedLog : InterleavedLog ;
274282} ) {
275283 return (
276- < section className = "border-t bg-background-tertiary px-4 py-2" >
277- < div className = "grid grid-cols-[auto_1fr] gap-x-1 gap-y-1 text-xs text-content-secondary" >
278- < div className = "flex items-center justify-end gap-1" >
279- < KeyboardShortcut value = { [ "Down" ] } />
280- < span > /</ span >
281- < KeyboardShortcut value = { [ "Up" ] } />
284+ < section className = "scrollbar overflow-x-auto border-t bg-background-tertiary px-4 py-2" >
285+ < div className = "grid grid-cols-[16.5rem_14rem] gap-x-4 gap-y-1 text-xs text-content-secondary" >
286+ < div className = { shortcutItemClass } >
287+ < div className = { shortcutKeysClass } >
288+ < KeyboardShortcut value = { [ "Down" ] } />
289+ < span > /</ span >
290+ < KeyboardShortcut value = { [ "Up" ] } />
291+ </ div >
292+ < span className = { shortcutLabelClass } > Navigate</ span >
293+ </ div >
294+
295+ < div className = { shortcutItemClass } >
296+ < div className = { shortcutKeysClass } >
297+ < KeyboardShortcut value = { [ "CtrlOrCmd" ] } />
298+ < span > +</ span >
299+ < KeyboardShortcut value = { [ "A" ] } />
300+ </ div >
301+ < span className = { shortcutLabelClass } > Jump to top</ span >
282302 </ div >
283- < span > Navigate</ span >
284303
285304 { selectedLog . kind === "ExecutionLog" && (
286305 < >
287- < div className = "flex items-center justify-end gap-1" >
306+ < div className = { shortcutItemClass } >
307+ < div className = { shortcutKeysClass } >
308+ < KeyboardShortcut value = { [ "Shift" ] } />
309+ < span > +</ span >
310+ < KeyboardShortcut value = { [ "Down" ] } />
311+ < span > /</ span >
312+ < KeyboardShortcut value = { [ "Up" ] } />
313+ </ div >
314+ < span className = { shortcutLabelClass } > Navigate request</ span >
315+ </ div >
316+
317+ < div className = { shortcutItemClass } >
318+ < div className = { shortcutKeysClass } >
319+ < KeyboardShortcut value = { [ "CtrlOrCmd" ] } />
320+ < span > +</ span >
321+ < KeyboardShortcut value = { [ "E" ] } />
322+ </ div >
323+ < span className = { shortcutLabelClass } > Jump to bottom</ span >
324+ </ div >
325+
326+ < div className = { shortcutItemClass } >
327+ < div className = { shortcutKeysClass } >
328+ < KeyboardShortcut value = { [ "CtrlOrCmd" ] } />
329+ < span > +</ span >
330+ < KeyboardShortcut value = { [ "Down" ] } />
331+ < span > /</ span >
332+ < KeyboardShortcut value = { [ "Up" ] } />
333+ </ div >
334+ < span className = { shortcutLabelClass } > Navigate execution</ span >
335+ </ div >
336+
337+ < div className = { shortcutItemClass } >
338+ < div className = { shortcutKeysClass } >
339+ < KeyboardShortcut value = { [ "CtrlOrCmd" ] } />
340+ < KeyboardShortcut value = { [ "Shift" ] } />
341+ < span > +</ span >
342+ < KeyboardShortcut value = { [ "A" ] } />
343+ </ div >
344+ < span className = { shortcutLabelClass } > Jump to top of request</ span >
345+ </ div >
346+ </ >
347+ ) }
348+
349+ < div className = { shortcutItemClass } >
350+ < div className = { shortcutKeysClass } >
351+ < KeyboardShortcut value = { [ "CtrlOrCmd" ] } />
352+ < span > +</ span >
353+ < KeyboardShortcut value = { [ "PageUp" ] } />
354+ < span > /</ span >
355+ < KeyboardShortcut value = { [ "PageDown" ] } />
356+ </ div >
357+ < span className = { shortcutLabelClass } > Navigate page</ span >
358+ </ div >
359+
360+ { selectedLog . kind === "ExecutionLog" ? (
361+ < div className = { shortcutItemClass } >
362+ < div className = { shortcutKeysClass } >
363+ < KeyboardShortcut value = { [ "CtrlOrCmd" ] } />
288364 < KeyboardShortcut value = { [ "Shift" ] } />
289365 < span > +</ span >
290- < KeyboardShortcut value = { [ "Down" ] } />
291- < span > /</ span >
292- < KeyboardShortcut value = { [ "Up" ] } />
366+ < KeyboardShortcut value = { [ "E" ] } />
293367 </ div >
294- < span > Navigate within request</ span >
295-
296- < div className = "flex items-center justify-end gap-1" >
368+ < span className = { shortcutLabelClass } >
369+ Jump to bottom of request
370+ </ span >
371+ </ div >
372+ ) : (
373+ < div className = { shortcutItemClass } >
374+ < div className = { shortcutKeysClass } >
297375 < KeyboardShortcut value = { [ "CtrlOrCmd" ] } />
298376 < span > +</ span >
299- < KeyboardShortcut value = { [ "Down" ] } />
300- < span > /</ span >
301- < KeyboardShortcut value = { [ "Up" ] } />
377+ < KeyboardShortcut value = { [ "E" ] } />
302378 </ div >
303- < span > Navigate within execution </ span >
304- </ >
379+ < span className = { shortcutLabelClass } > Jump to bottom </ span >
380+ </ div >
305381 ) }
306382
307- < div className = "flex items-center justify-end gap-1" >
308- < KeyboardShortcut value = { [ "Shift" ] } />
309- < span > +</ span >
310- < KeyboardShortcut value = { [ "Right" ] } />
383+ < div className = { shortcutItemClass } >
384+ < div className = { shortcutKeysClass } >
385+ < KeyboardShortcut value = { [ "Shift" ] } />
386+ < span > +</ span >
387+ < KeyboardShortcut value = { [ "Right" ] } />
388+ </ div >
389+ < span className = { shortcutLabelClass } > Focus this panel</ span >
390+ </ div >
391+
392+ < div className = { shortcutItemClass } >
393+ < div className = { shortcutKeysClass } >
394+ < KeyboardShortcut value = { [ "Esc" ] } />
395+ </ div >
396+ < span className = { shortcutLabelClass } > Close this panel</ span >
311397 </ div >
312- < span > Focus this panel</ span >
313398 </ div >
314399 </ section >
315400 ) ;
@@ -322,7 +407,16 @@ export function useNavigateLogs(
322407 onClose : ( ) => void ,
323408 onHitBoundary : ( boundary : "top" | "bottom" | null ) => void ,
324409 rightPanelRef : React . RefObject < HTMLDivElement > ,
410+ logListContainerRef ?: React . RefObject < HTMLDivElement > ,
325411) {
412+ // Calculate the number of items that fit in one page based on container height
413+ const calculatePageSize = useCallback ( ( ) => {
414+ if ( ! logListContainerRef ?. current ) {
415+ return 10 ; // Default fallback
416+ }
417+ const containerHeight = logListContainerRef . current . clientHeight ;
418+ return Math . floor ( containerHeight / ITEM_SIZE ) ;
419+ } , [ logListContainerRef ] ) ;
326420 // Get logs for the current execution (both log entries and outcomes)
327421 const executionLogs =
328422 selectedLog && selectedLog . kind === "ExecutionLog"
@@ -433,4 +527,106 @@ export function useNavigateLogs(
433527 preventDefault : true ,
434528 } ,
435529 ) ;
530+
531+ // Navigate to top/bottom of list
532+ useHotkeys (
533+ [ "ctrl+a" , "meta+a" ] ,
534+ ( ) => {
535+ if ( logs . length > 0 ) {
536+ onSelectLog ( logs [ 0 ] ) ;
537+ onHitBoundary ( null ) ;
538+ }
539+ } ,
540+ {
541+ preventDefault : true ,
542+ } ,
543+ ) ;
544+ useHotkeys (
545+ [ "ctrl+e" , "meta+e" ] ,
546+ ( ) => {
547+ if ( logs . length > 0 ) {
548+ onSelectLog ( logs [ logs . length - 1 ] ) ;
549+ onHitBoundary ( null ) ;
550+ }
551+ } ,
552+ {
553+ preventDefault : true ,
554+ } ,
555+ ) ;
556+
557+ // Navigate to top/bottom within request
558+ useHotkeys (
559+ [ "ctrl+shift+a" , "meta+shift+a" ] ,
560+ ( ) => {
561+ if ( requestLogs && requestLogs . length > 0 ) {
562+ onSelectLog ( requestLogs [ 0 ] ) ;
563+ onHitBoundary ( null ) ;
564+ }
565+ } ,
566+ {
567+ preventDefault : true ,
568+ } ,
569+ ) ;
570+ useHotkeys (
571+ [ "ctrl+shift+e" , "meta+shift+e" ] ,
572+ ( ) => {
573+ if ( requestLogs && requestLogs . length > 0 ) {
574+ onSelectLog ( requestLogs [ requestLogs . length - 1 ] ) ;
575+ onHitBoundary ( null ) ;
576+ }
577+ } ,
578+ {
579+ preventDefault : true ,
580+ } ,
581+ ) ;
582+
583+ // Navigate by page (based on container height)
584+ useHotkeys (
585+ [ "ctrl+pageup" , "meta+pageup" ] ,
586+ ( ) => {
587+ if ( ! selectedLog ) return ;
588+ const pageSize = calculatePageSize ( ) ;
589+ const selectedLogKey = getLogKey ( selectedLog ) ;
590+ const currentIndex = logs . findIndex (
591+ ( log ) => getLogKey ( log ) === selectedLogKey ,
592+ ) ;
593+ if ( currentIndex === - 1 ) return ;
594+
595+ const newIndex = Math . max ( 0 , currentIndex - pageSize ) ;
596+ onSelectLog ( logs [ newIndex ] ) ;
597+ if ( newIndex === 0 ) {
598+ onHitBoundary ( "top" ) ;
599+ } else {
600+ onHitBoundary ( null ) ;
601+ }
602+ } ,
603+ {
604+ preventDefault : true ,
605+ } ,
606+ [ selectedLog , logs , onSelectLog , onHitBoundary , calculatePageSize ] ,
607+ ) ;
608+ useHotkeys (
609+ [ "ctrl+pagedown" , "meta+pagedown" ] ,
610+ ( ) => {
611+ if ( ! selectedLog ) return ;
612+ const pageSize = calculatePageSize ( ) ;
613+ const selectedLogKey = getLogKey ( selectedLog ) ;
614+ const currentIndex = logs . findIndex (
615+ ( log ) => getLogKey ( log ) === selectedLogKey ,
616+ ) ;
617+ if ( currentIndex === - 1 ) return ;
618+
619+ const newIndex = Math . min ( logs . length - 1 , currentIndex + pageSize ) ;
620+ onSelectLog ( logs [ newIndex ] ) ;
621+ if ( newIndex === logs . length - 1 ) {
622+ onHitBoundary ( "bottom" ) ;
623+ } else {
624+ onHitBoundary ( null ) ;
625+ }
626+ } ,
627+ {
628+ preventDefault : true ,
629+ } ,
630+ [ selectedLog , logs , onSelectLog , onHitBoundary , calculatePageSize ] ,
631+ ) ;
436632}
0 commit comments