Skip to content

Commit 2747d6b

Browse files
atrakhConvex, Inc.
authored andcommitted
dashboard: add more meta shortcuts for logs page (#41735)
Adds more shortcuts for navigating the logs list - by page and to the top / bottom GitOrigin-RevId: b48a47e409c79c8979b6c1bf6c23c8a17a23ec1f
1 parent 3234269 commit 2747d6b

File tree

3 files changed

+233
-26
lines changed

3 files changed

+233
-26
lines changed

npm-packages/@convex-dev/design-system/src/KeyboardShortcut.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import classNames from "classnames";
22
import { useEffect, useState } from "react";
33

44
export type Key =
5+
| "PageUp"
6+
| "PageDown"
57
| "CtrlOrCmd" // depending on platform
68
| "Ctrl"
79
| "Alt"
@@ -70,9 +72,13 @@ const appleKeyNameOverrides: PlatformKeyNameOverrides = {
7072
Up: "↑",
7173
Down: "↓",
7274
Tab: "⇥",
75+
PageUp: "PgUp",
76+
PageDown: "PgDn",
7377
};
7478
const nonAppleKeyNameOverrides: PlatformKeyNameOverrides = {
7579
CtrlOrCmd: "Ctrl",
80+
PageUp: "PgUp",
81+
PageDown: "PgDn",
7682
};
7783

7884
export function KeyboardShortcut({

npm-packages/dashboard-common/src/features/logs/components/LogDrilldown.tsx

Lines changed: 220 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import Link from "next/link";
1515
import { useHotkeys } from "react-hotkeys-hook";
1616
import { KeyboardShortcut } from "@ui/KeyboardShortcut";
1717
import { Callout } from "@ui/Callout";
18+
import { ITEM_SIZE } from "@common/features/logs/components/LogListItem";
1819
import { FunctionCallTree } from "./FunctionCallTree";
1920
import { LogMetadata } from "./LogMetadata";
2021
import { 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+
270278
function 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
}

npm-packages/dashboard-common/src/features/logs/components/LogList.tsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ export function LogList({
138138
// Ref to the virtualized list for programmatic scrolling
139139
const listRef = useRef<FixedSizeList>(null);
140140

141+
// Ref to the outer div container for calculating page size
142+
const outerRef = useRef<HTMLDivElement>(null);
143+
141144
const { newLogsPageSidepanel } = useContext(DeploymentInfoContext);
142145

143146
const handleSelectLog = useCallback(
@@ -219,6 +222,7 @@ export function LogList({
219222
hitBoundary,
220223
shownLog,
221224
listRef,
225+
outerRef,
222226
newLogsPageSidepanel,
223227
}}
224228
/>
@@ -258,6 +262,7 @@ export function LogList({
258262
}}
259263
onSelectLog={handleSelectLog}
260264
onHitBoundary={setHitBoundary}
265+
logListContainerRef={outerRef}
261266
/>
262267
</Panel>
263268
</>
@@ -290,6 +295,7 @@ function WindowedLogList({
290295
shownLog,
291296
hitBoundary,
292297
listRef,
298+
outerRef,
293299
newLogsPageSidepanel,
294300
}: {
295301
interleavedLogs: InterleavedLog[];
@@ -303,10 +309,9 @@ function WindowedLogList({
303309
shownLog?: InterleavedLog;
304310
hitBoundary: "top" | "bottom" | null;
305311
listRef: React.RefObject<FixedSizeList>;
312+
outerRef: React.RefObject<HTMLDivElement>;
306313
newLogsPageSidepanel?: boolean;
307314
}) {
308-
const outerRef = useRef<HTMLDivElement>(null);
309-
310315
return (
311316
<div className="scrollbar flex h-full min-w-0 flex-col overflow-x-auto overflow-y-hidden">
312317
<div className="flex h-full min-w-fit flex-col">

0 commit comments

Comments
 (0)