Skip to content

Commit eaed7d0

Browse files
authored
fix(webapp): UI/UX improvements for logs, query, and shortcuts (#2997)
## ✅ Checklist - [X] I have followed every step in the [contributing guide](https://github.com/triggerdotdev/trigger.dev/blob/main/CONTRIBUTING.md) - [X] The PR title follows the convention. - [X] I ran and tested the code works --- ## Testing Manually tested each implementation. --- ## Changelog * Updated Logs Page with the new implementation in time filter component * In TRQL editor users can now click on empty/blank spaces in the editor and the cursor will appear * Added CMD + / for line commenting in TRQL * Activated proper undo/redo functionality in CodeMirror (TRQL editor) * Added a check for new logs button, previously once the user got to the end of the logs he could not check for newer logs * Added showing MS in logs page Dates * Removed LOG_INFO internal logs, they are available with Admin Debug flag * Added support for correct timezone render on server side. * Increased CLICKHOUSE_LOGS_LIST_MAX_MEMORY_USAGE to 1GB * Changed Previous run/ Next run to J/K, consistent with previous/next page in Runs list
1 parent 9b21f8d commit eaed7d0

File tree

15 files changed

+270
-89
lines changed

15 files changed

+270
-89
lines changed

apps/webapp/app/components/Shortcuts.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,8 @@ function ShortcutContent() {
139139
<ShortcutKey shortcut={{ key: "arrowright" }} variant="medium/bright" />
140140
</Shortcut>
141141
<Shortcut name="Jump to next/previous run">
142-
<ShortcutKey shortcut={{ key: "[" }} variant="medium/bright" />
143-
<ShortcutKey shortcut={{ key: "]" }} variant="medium/bright" />
142+
<ShortcutKey shortcut={{ key: "j" }} variant="medium/bright" />
143+
<ShortcutKey shortcut={{ key: "k" }} variant="medium/bright" />
144144
</Shortcut>
145145
<Shortcut name="Expand all">
146146
<ShortcutKey shortcut={{ key: "e" }} variant="medium/bright" />
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { useFetcher } from "@remix-run/react";
2+
import { useEffect, useRef } from "react";
3+
import { useTypedLoaderData } from "remix-typedjson";
4+
import type { loader } from "~/root";
5+
6+
export function TimezoneSetter() {
7+
const { timezone: storedTimezone } = useTypedLoaderData<typeof loader>();
8+
const fetcher = useFetcher();
9+
const hasSetTimezone = useRef(false);
10+
11+
useEffect(() => {
12+
if (hasSetTimezone.current) return;
13+
14+
const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
15+
16+
if (browserTimezone && browserTimezone !== storedTimezone) {
17+
hasSetTimezone.current = true;
18+
fetcher.submit(
19+
{ timezone: browserTimezone },
20+
{
21+
method: "POST",
22+
action: "/resources/timezone",
23+
encType: "application/json",
24+
}
25+
);
26+
}
27+
}, [storedTimezone, fetcher]);
28+
29+
return null;
30+
}

apps/webapp/app/components/code/TSQLEditor.tsx

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { sql, StandardSQL } from "@codemirror/lang-sql";
22
import { autocompletion, startCompletion } from "@codemirror/autocomplete";
33
import { linter, lintGutter } from "@codemirror/lint";
4-
import { EditorView } from "@codemirror/view";
4+
import { EditorView, keymap } from "@codemirror/view";
55
import type { ViewUpdate } from "@codemirror/view";
66
import { CheckIcon, ClipboardIcon, SparklesIcon, TrashIcon } from "@heroicons/react/20/solid";
77
import {
@@ -60,6 +60,54 @@ const defaultProps: TSQLEditorDefaultProps = {
6060
schema: [],
6161
};
6262

63+
// Toggle comment on current line or selected lines with -- comment symbol
64+
const toggleLineComment = (view: EditorView): boolean => {
65+
const { from, to } = view.state.selection.main;
66+
const startLine = view.state.doc.lineAt(from);
67+
// When `to` is exactly at the start of a line and there's an actual selection,
68+
// the caret sits before that line — so exclude it by stepping back one position.
69+
const adjustedTo = to > from && view.state.doc.lineAt(to).from === to ? to - 1 : to;
70+
const endLine = view.state.doc.lineAt(adjustedTo);
71+
72+
// Collect all lines in the selection
73+
const lines: { from: number; to: number; text: string }[] = [];
74+
for (let i = startLine.number; i <= endLine.number; i++) {
75+
const line = view.state.doc.line(i);
76+
lines.push({ from: line.from, to: line.to, text: line.text });
77+
}
78+
79+
// Determine action: if all non-empty lines are commented, uncomment; otherwise comment
80+
const allCommented = lines.every((line) => {
81+
const trimmed = line.text.trimStart();
82+
return trimmed.length === 0 || trimmed.startsWith("--");
83+
});
84+
85+
const changes = lines
86+
.map((line) => {
87+
const trimmed = line.text.trimStart();
88+
if (trimmed.length === 0) return null; // skip empty lines
89+
const indent = line.text.length - trimmed.length;
90+
91+
if (allCommented) {
92+
// Remove comment: strip "-- " or just "--"
93+
const afterComment = trimmed.slice(2);
94+
const newText = line.text.slice(0, indent) + afterComment.replace(/^\s/, "");
95+
return { from: line.from, to: line.to, insert: newText };
96+
} else {
97+
// Add comment: prepend "-- " to the line content
98+
const newText = line.text.slice(0, indent) + "-- " + trimmed;
99+
return { from: line.from, to: line.to, insert: newText };
100+
}
101+
})
102+
.filter((c): c is { from: number; to: number; insert: string } => c !== null);
103+
104+
if (changes.length > 0) {
105+
view.dispatch({ changes });
106+
}
107+
108+
return true;
109+
};
110+
63111
export function TSQLEditor(opts: TSQLEditorProps) {
64112
const {
65113
defaultValue = "",
@@ -133,6 +181,14 @@ export function TSQLEditor(opts: TSQLEditorProps) {
133181
);
134182
}
135183

184+
// Add keyboard shortcut for toggling comments
185+
exts.push(
186+
keymap.of([
187+
{ key: "Cmd-/", run: toggleLineComment },
188+
{ key: "Ctrl-/", run: toggleLineComment },
189+
])
190+
);
191+
136192
return exts;
137193
}, [schema, linterEnabled]);
138194

@@ -218,6 +274,9 @@ export function TSQLEditor(opts: TSQLEditorProps) {
218274
"min-h-0 flex-1 overflow-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
219275
)}
220276
ref={editor}
277+
onClick={() => {
278+
view?.focus();
279+
}}
221280
onBlur={() => {
222281
if (!onBlur) return;
223282
if (!view) return;

apps/webapp/app/components/code/codeMirrorSetup.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { closeBrackets } from "@codemirror/autocomplete";
2-
import { indentWithTab } from "@codemirror/commands";
2+
import { indentWithTab, history, historyKeymap, undo, redo } from "@codemirror/commands";
33
import { bracketMatching } from "@codemirror/language";
44
import { lintKeymap } from "@codemirror/lint";
55
import { highlightSelectionMatches } from "@codemirror/search";
@@ -18,6 +18,7 @@ export function getEditorSetup(showLineNumbers = true, showHighlights = true): A
1818
const options = [
1919
drawSelection(),
2020
dropCursor(),
21+
history(),
2122
bracketMatching(),
2223
closeBrackets(),
2324
Prec.highest(
@@ -31,7 +32,15 @@ export function getEditorSetup(showLineNumbers = true, showHighlights = true): A
3132
},
3233
])
3334
),
34-
keymap.of([indentWithTab, ...lintKeymap]),
35+
// Explicit undo/redo keybindings with high precedence
36+
Prec.high(
37+
keymap.of([
38+
{ key: "Mod-z", run: undo },
39+
{ key: "Mod-Shift-z", run: redo },
40+
{ key: "Mod-y", run: redo },
41+
])
42+
),
43+
keymap.of([indentWithTab, ...historyKeymap, ...lintKeymap]),
3544
];
3645

3746
if (showLineNumbers) {

apps/webapp/app/components/logs/LogDetailView.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { useEffect, useState } from "react";
88
import { useTypedFetcher } from "remix-typedjson";
99
import { cn } from "~/utils/cn";
1010
import { Button } from "~/components/primitives/Buttons";
11-
import { DateTime } from "~/components/primitives/DateTime";
11+
import { DateTimeAccurate } from "~/components/primitives/DateTime";
1212
import { Header2, Header3 } from "~/components/primitives/Headers";
1313
import { Paragraph } from "~/components/primitives/Paragraph";
1414
import { Spinner } from "~/components/primitives/Spinner";
@@ -234,7 +234,7 @@ function DetailsTab({ log, runPath, searchTerm }: { log: LogEntry; runPath: stri
234234
<div className="mb-6">
235235
<Header3 className="mb-2">Timestamp</Header3>
236236
<div className="text-sm text-text-dimmed">
237-
<DateTime date={log.startTime} />
237+
<DateTimeAccurate date={log.startTime} />
238238
</div>
239239
</div>
240240

apps/webapp/app/components/logs/LogsTable.tsx

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ArrowPathIcon, ArrowTopRightOnSquareIcon } from "@heroicons/react/20/solid";
2+
import { Link } from "@remix-run/react";
23
import { useEffect, useRef, useState } from "react";
34
import { cn } from "~/utils/cn";
45
import { Button } from "~/components/primitives/Buttons";
@@ -8,7 +9,7 @@ import { useProject } from "~/hooks/useProject";
89
import type { LogEntry } from "~/presenters/v3/LogsListPresenter.server";
910
import { getLevelColor, highlightSearchText } from "~/utils/logUtils";
1011
import { v3RunSpanPath } from "~/utils/pathBuilder";
11-
import { DateTime } from "../primitives/DateTime";
12+
import { DateTimeAccurate } from "../primitives/DateTime";
1213
import { Paragraph } from "../primitives/Paragraph";
1314
import { Spinner } from "../primitives/Spinner";
1415
import { TruncatedCopyableValue } from "../primitives/TruncatedCopyableValue";
@@ -24,8 +25,6 @@ import {
2425
TableRow,
2526
type TableVariant,
2627
} from "../primitives/Table";
27-
import { PopoverMenuItem } from "~/components/primitives/Popover";
28-
import { Link } from "@remix-run/react";
2928

3029
type LogsTableProps = {
3130
logs: LogEntry[];
@@ -34,6 +33,7 @@ type LogsTableProps = {
3433
isLoadingMore?: boolean;
3534
hasMore?: boolean;
3635
onLoadMore?: () => void;
36+
onCheckForMore?: () => void;
3737
variant?: TableVariant;
3838
selectedLogId?: string;
3939
onLogSelect?: (logId: string) => void;
@@ -63,6 +63,7 @@ export function LogsTable({
6363
isLoadingMore = false,
6464
hasMore = false,
6565
onLoadMore,
66+
onCheckForMore,
6667
selectedLogId,
6768
onLogSelect,
6869
}: LogsTableProps) {
@@ -161,7 +162,7 @@ export function LogsTable({
161162
boxShadow: getLevelBoxShadow(log.level),
162163
}}
163164
>
164-
<DateTime date={log.startTime} />
165+
<DateTimeAccurate date={log.startTime} />
165166
</TableCell>
166167
<TableCell className="min-w-24">
167168
<TruncatedCopyableValue value={log.runId} />
@@ -203,20 +204,15 @@ export function LogsTable({
203204
{/* Infinite scroll trigger */}
204205
{hasMore && logs.length > 0 && (
205206
<div ref={loadMoreRef} className="flex items-center justify-center py-12">
206-
<div
207-
className={cn(
208-
"flex items-center gap-2",
209-
!showLoadMoreSpinner && "invisible"
210-
)}
211-
>
207+
<div className={cn("flex items-center gap-2", !showLoadMoreSpinner && "invisible")}>
212208
<Spinner /> <span className="text-text-dimmed">Loading more…</span>
213209
</div>
214210
</div>
215211
)}
216-
{/* Show all logs message */}
212+
{/* Show all logs message with check for more button */}
217213
{!hasMore && logs.length > 0 && (
218214
<div className="flex items-center justify-center py-12">
219-
<div className="flex items-center gap-2">
215+
<div className="flex flex-col items-center gap-3">
220216
<span className="text-text-dimmed">Showing all {logs.length} logs</span>
221217
</div>
222218
</div>

apps/webapp/app/components/primitives/DateTime.tsx

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { GlobeAltIcon, GlobeAmericasIcon } from "@heroicons/react/20/solid";
2+
import { useRouteLoaderData } from "@remix-run/react";
23
import { Laptop } from "lucide-react";
34
import { memo, type ReactNode, useMemo, useSyncExternalStore } from "react";
45
import { CopyButton } from "./CopyButton";
@@ -19,7 +20,7 @@ function getLocalTimeZone(): string {
1920
// For SSR compatibility: returns "UTC" on server, actual timezone on client
2021
function subscribeToTimeZone() {
2122
// No-op - timezone doesn't change
22-
return () => { };
23+
return () => {};
2324
}
2425

2526
function getTimeZoneSnapshot(): string {
@@ -39,6 +40,18 @@ export function useLocalTimeZone(): string {
3940
return useSyncExternalStore(subscribeToTimeZone, getTimeZoneSnapshot, getServerTimeZoneSnapshot);
4041
}
4142

43+
/**
44+
* Hook to get the user's preferred timezone.
45+
* Returns the timezone stored in the user's preferences cookie (from root loader),
46+
* falling back to the browser's local timezone if not set.
47+
*/
48+
export function useUserTimeZone(): string {
49+
const rootData = useRouteLoaderData("root") as { timezone?: string } | undefined;
50+
const localTimeZone = useLocalTimeZone();
51+
// Use stored timezone from cookie, or fall back to browser's local timezone
52+
return rootData?.timezone && rootData.timezone !== "UTC" ? rootData.timezone : localTimeZone;
53+
}
54+
4255
type DateTimeProps = {
4356
date: Date | string;
4457
timeZone?: string;
@@ -63,15 +76,15 @@ export const DateTime = ({
6376
hour12 = true,
6477
}: DateTimeProps) => {
6578
const locales = useLocales();
66-
const localTimeZone = useLocalTimeZone();
79+
const userTimeZone = useUserTimeZone();
6780

6881
const realDate = useMemo(() => (typeof date === "string" ? new Date(date) : date), [date]);
6982

7083
const formattedDateTime = (
7184
<span suppressHydrationWarning>
7285
{formatDateTime(
7386
realDate,
74-
timeZone ?? localTimeZone,
87+
timeZone ?? userTimeZone,
7588
locales,
7689
includeSeconds,
7790
includeTime,
@@ -91,7 +104,7 @@ export const DateTime = ({
91104
<TooltipContent
92105
realDate={realDate}
93106
timeZone={timeZone}
94-
localTimeZone={localTimeZone}
107+
localTimeZone={userTimeZone}
95108
locales={locales}
96109
/>
97110
}
@@ -167,7 +180,7 @@ export function formatDateTimeISO(date: Date, timeZone: string): string {
167180
// New component that only shows date when it changes
168181
export const SmartDateTime = ({ date, previousDate = null, hour12 = true }: DateTimeProps) => {
169182
const locales = useLocales();
170-
const localTimeZone = useLocalTimeZone();
183+
const userTimeZone = useUserTimeZone();
171184
const realDate = typeof date === "string" ? new Date(date) : date;
172185
const realPrevDate = previousDate
173186
? typeof previousDate === "string"
@@ -180,8 +193,8 @@ export const SmartDateTime = ({ date, previousDate = null, hour12 = true }: Date
180193

181194
// Format with appropriate function
182195
const formattedDateTime = showDatePart
183-
? formatSmartDateTime(realDate, localTimeZone, locales, hour12)
184-
: formatTimeOnly(realDate, localTimeZone, locales, hour12);
196+
? formatSmartDateTime(realDate, userTimeZone, locales, hour12)
197+
: formatTimeOnly(realDate, userTimeZone, locales, hour12);
185198

186199
return <span suppressHydrationWarning>{formattedDateTime.replace(/\s/g, String.fromCharCode(32))}</span>;
187200
};
@@ -235,14 +248,16 @@ function formatTimeOnly(
235248

236249
const DateTimeAccurateInner = ({
237250
date,
238-
timeZone = "UTC",
251+
timeZone,
239252
previousDate = null,
240253
showTooltip = true,
241254
hideDate = false,
242255
hour12 = true,
243256
}: DateTimeProps) => {
244257
const locales = useLocales();
245-
const localTimeZone = useLocalTimeZone();
258+
const userTimeZone = useUserTimeZone();
259+
// Use provided timeZone prop if available, otherwise fall back to user's preferred timezone
260+
const displayTimeZone = timeZone ?? userTimeZone;
246261
const realDate = typeof date === "string" ? new Date(date) : date;
247262
const realPrevDate = previousDate
248263
? typeof previousDate === "string"
@@ -253,13 +268,13 @@ const DateTimeAccurateInner = ({
253268
// Smart formatting based on whether date changed
254269
const formattedDateTime = useMemo(() => {
255270
return hideDate
256-
? formatTimeOnly(realDate, localTimeZone, locales, hour12)
271+
? formatTimeOnly(realDate, displayTimeZone, locales, hour12)
257272
: realPrevDate
258273
? isSameDay(realDate, realPrevDate)
259-
? formatTimeOnly(realDate, localTimeZone, locales, hour12)
260-
: formatDateTimeAccurate(realDate, localTimeZone, locales, hour12)
261-
: formatDateTimeAccurate(realDate, localTimeZone, locales, hour12);
262-
}, [realDate, localTimeZone, locales, hour12, hideDate, previousDate]);
274+
? formatTimeOnly(realDate, displayTimeZone, locales, hour12)
275+
: formatDateTimeAccurate(realDate, displayTimeZone, locales, hour12)
276+
: formatDateTimeAccurate(realDate, displayTimeZone, locales, hour12);
277+
}, [realDate, displayTimeZone, locales, hour12, hideDate, previousDate]);
263278

264279
if (!showTooltip)
265280
return <span suppressHydrationWarning>{formattedDateTime.replace(/\s/g, String.fromCharCode(32))}</span>;
@@ -268,7 +283,7 @@ const DateTimeAccurateInner = ({
268283
<TooltipContent
269284
realDate={realDate}
270285
timeZone={timeZone}
271-
localTimeZone={localTimeZone}
286+
localTimeZone={userTimeZone}
272287
locales={locales}
273288
/>
274289
);
@@ -328,9 +343,9 @@ function formatDateTimeAccurate(
328343

329344
export const DateTimeShort = ({ date, hour12 = true }: DateTimeProps) => {
330345
const locales = useLocales();
331-
const localTimeZone = useLocalTimeZone();
346+
const userTimeZone = useUserTimeZone();
332347
const realDate = typeof date === "string" ? new Date(date) : date;
333-
const formattedDateTime = formatDateTimeShort(realDate, localTimeZone, locales, hour12);
348+
const formattedDateTime = formatDateTimeShort(realDate, userTimeZone, locales, hour12);
334349

335350
return <span suppressHydrationWarning>{formattedDateTime.replace(/\s/g, String.fromCharCode(32))}</span>;
336351
};

0 commit comments

Comments
 (0)