Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions apps/webapp/app/components/Shortcuts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@ function ShortcutContent() {
<ShortcutKey shortcut={{ key: "arrowright" }} variant="medium/bright" />
</Shortcut>
<Shortcut name="Jump to next/previous run">
<ShortcutKey shortcut={{ key: "[" }} variant="medium/bright" />
<ShortcutKey shortcut={{ key: "]" }} variant="medium/bright" />
<ShortcutKey shortcut={{ key: "j" }} variant="medium/bright" />
<ShortcutKey shortcut={{ key: "k" }} variant="medium/bright" />
</Shortcut>
<Shortcut name="Expand all">
<ShortcutKey shortcut={{ key: "e" }} variant="medium/bright" />
Expand Down
30 changes: 30 additions & 0 deletions apps/webapp/app/components/TimezoneSetter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { useFetcher } from "@remix-run/react";
import { useEffect, useRef } from "react";
import { useTypedLoaderData } from "remix-typedjson";
import type { loader } from "~/root";

export function TimezoneSetter() {
const { timezone: storedTimezone } = useTypedLoaderData<typeof loader>();
const fetcher = useFetcher();
const hasSetTimezone = useRef(false);

useEffect(() => {
if (hasSetTimezone.current) return;

const browserTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;

if (browserTimezone && browserTimezone !== storedTimezone) {
hasSetTimezone.current = true;
fetcher.submit(
{ timezone: browserTimezone },
{
method: "POST",
action: "/resources/timezone",
encType: "application/json",
}
);
}
}, [storedTimezone, fetcher]);

return null;
}
61 changes: 60 additions & 1 deletion apps/webapp/app/components/code/TSQLEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { sql, StandardSQL } from "@codemirror/lang-sql";
import { autocompletion, startCompletion } from "@codemirror/autocomplete";
import { linter, lintGutter } from "@codemirror/lint";
import { EditorView } from "@codemirror/view";
import { EditorView, keymap } from "@codemirror/view";
import type { ViewUpdate } from "@codemirror/view";
import { CheckIcon, ClipboardIcon, SparklesIcon, TrashIcon } from "@heroicons/react/20/solid";
import {
Expand Down Expand Up @@ -60,6 +60,54 @@ const defaultProps: TSQLEditorDefaultProps = {
schema: [],
};

// Toggle comment on current line or selected lines with -- comment symbol
const toggleLineComment = (view: EditorView): boolean => {
const { from, to } = view.state.selection.main;
const startLine = view.state.doc.lineAt(from);
// When `to` is exactly at the start of a line and there's an actual selection,
// the caret sits before that line — so exclude it by stepping back one position.
const adjustedTo = to > from && view.state.doc.lineAt(to).from === to ? to - 1 : to;
const endLine = view.state.doc.lineAt(adjustedTo);

// Collect all lines in the selection
const lines: { from: number; to: number; text: string }[] = [];
for (let i = startLine.number; i <= endLine.number; i++) {
const line = view.state.doc.line(i);
lines.push({ from: line.from, to: line.to, text: line.text });
}

// Determine action: if all non-empty lines are commented, uncomment; otherwise comment
const allCommented = lines.every((line) => {
const trimmed = line.text.trimStart();
return trimmed.length === 0 || trimmed.startsWith("--");
});

const changes = lines
.map((line) => {
const trimmed = line.text.trimStart();
if (trimmed.length === 0) return null; // skip empty lines
const indent = line.text.length - trimmed.length;

if (allCommented) {
// Remove comment: strip "-- " or just "--"
const afterComment = trimmed.slice(2);
const newText = line.text.slice(0, indent) + afterComment.replace(/^\s/, "");
return { from: line.from, to: line.to, insert: newText };
} else {
// Add comment: prepend "-- " to the line content
const newText = line.text.slice(0, indent) + "-- " + trimmed;
return { from: line.from, to: line.to, insert: newText };
}
})
.filter((c): c is { from: number; to: number; insert: string } => c !== null);

if (changes.length > 0) {
view.dispatch({ changes });
}

return true;
};

export function TSQLEditor(opts: TSQLEditorProps) {
const {
defaultValue = "",
Expand Down Expand Up @@ -133,6 +181,14 @@ export function TSQLEditor(opts: TSQLEditorProps) {
);
}

// Add keyboard shortcut for toggling comments
exts.push(
keymap.of([
{ key: "Cmd-/", run: toggleLineComment },
{ key: "Ctrl-/", run: toggleLineComment },
])
);

return exts;
}, [schema, linterEnabled]);

Expand Down Expand Up @@ -218,6 +274,9 @@ export function TSQLEditor(opts: TSQLEditorProps) {
"min-h-0 flex-1 overflow-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-charcoal-600"
)}
ref={editor}
onClick={() => {
view?.focus();
}}
onBlur={() => {
if (!onBlur) return;
if (!view) return;
Expand Down
13 changes: 11 additions & 2 deletions apps/webapp/app/components/code/codeMirrorSetup.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { closeBrackets } from "@codemirror/autocomplete";
import { indentWithTab } from "@codemirror/commands";
import { indentWithTab, history, historyKeymap, undo, redo } from "@codemirror/commands";
import { bracketMatching } from "@codemirror/language";
import { lintKeymap } from "@codemirror/lint";
import { highlightSelectionMatches } from "@codemirror/search";
Expand All @@ -18,6 +18,7 @@ export function getEditorSetup(showLineNumbers = true, showHighlights = true): A
const options = [
drawSelection(),
dropCursor(),
history(),
bracketMatching(),
closeBrackets(),
Prec.highest(
Expand All @@ -31,7 +32,15 @@ export function getEditorSetup(showLineNumbers = true, showHighlights = true): A
},
])
),
keymap.of([indentWithTab, ...lintKeymap]),
// Explicit undo/redo keybindings with high precedence
Prec.high(
keymap.of([
{ key: "Mod-z", run: undo },
{ key: "Mod-Shift-z", run: redo },
{ key: "Mod-y", run: redo },
])
),
keymap.of([indentWithTab, ...historyKeymap, ...lintKeymap]),
];

if (showLineNumbers) {
Expand Down
4 changes: 2 additions & 2 deletions apps/webapp/app/components/logs/LogDetailView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { useEffect, useState } from "react";
import { useTypedFetcher } from "remix-typedjson";
import { cn } from "~/utils/cn";
import { Button } from "~/components/primitives/Buttons";
import { DateTime } from "~/components/primitives/DateTime";
import { DateTimeAccurate } from "~/components/primitives/DateTime";
import { Header2, Header3 } from "~/components/primitives/Headers";
import { Paragraph } from "~/components/primitives/Paragraph";
import { Spinner } from "~/components/primitives/Spinner";
Expand Down Expand Up @@ -234,7 +234,7 @@ function DetailsTab({ log, runPath, searchTerm }: { log: LogEntry; runPath: stri
<div className="mb-6">
<Header3 className="mb-2">Timestamp</Header3>
<div className="text-sm text-text-dimmed">
<DateTime date={log.startTime} />
<DateTimeAccurate date={log.startTime} />
</div>
</div>

Expand Down
20 changes: 8 additions & 12 deletions apps/webapp/app/components/logs/LogsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { ArrowPathIcon, ArrowTopRightOnSquareIcon } from "@heroicons/react/20/solid";
import { Link } from "@remix-run/react";
import { useEffect, useRef, useState } from "react";
import { cn } from "~/utils/cn";
import { Button } from "~/components/primitives/Buttons";
Expand All @@ -8,7 +9,7 @@ import { useProject } from "~/hooks/useProject";
import type { LogEntry } from "~/presenters/v3/LogsListPresenter.server";
import { getLevelColor, highlightSearchText } from "~/utils/logUtils";
import { v3RunSpanPath } from "~/utils/pathBuilder";
import { DateTime } from "../primitives/DateTime";
import { DateTimeAccurate } from "../primitives/DateTime";
import { Paragraph } from "../primitives/Paragraph";
import { Spinner } from "../primitives/Spinner";
import { TruncatedCopyableValue } from "../primitives/TruncatedCopyableValue";
Expand All @@ -24,8 +25,6 @@ import {
TableRow,
type TableVariant,
} from "../primitives/Table";
import { PopoverMenuItem } from "~/components/primitives/Popover";
import { Link } from "@remix-run/react";

type LogsTableProps = {
logs: LogEntry[];
Expand All @@ -34,6 +33,7 @@ type LogsTableProps = {
isLoadingMore?: boolean;
hasMore?: boolean;
onLoadMore?: () => void;
onCheckForMore?: () => void;
variant?: TableVariant;
selectedLogId?: string;
onLogSelect?: (logId: string) => void;
Expand Down Expand Up @@ -63,6 +63,7 @@ export function LogsTable({
isLoadingMore = false,
hasMore = false,
onLoadMore,
onCheckForMore,
selectedLogId,
onLogSelect,
}: LogsTableProps) {
Expand Down Expand Up @@ -161,7 +162,7 @@ export function LogsTable({
boxShadow: getLevelBoxShadow(log.level),
}}
>
<DateTime date={log.startTime} />
<DateTimeAccurate date={log.startTime} />
</TableCell>
<TableCell className="min-w-24">
<TruncatedCopyableValue value={log.runId} />
Expand Down Expand Up @@ -203,20 +204,15 @@ export function LogsTable({
{/* Infinite scroll trigger */}
{hasMore && logs.length > 0 && (
<div ref={loadMoreRef} className="flex items-center justify-center py-12">
<div
className={cn(
"flex items-center gap-2",
!showLoadMoreSpinner && "invisible"
)}
>
<div className={cn("flex items-center gap-2", !showLoadMoreSpinner && "invisible")}>
<Spinner /> <span className="text-text-dimmed">Loading more…</span>
</div>
</div>
)}
{/* Show all logs message */}
{/* Show all logs message with check for more button */}
{!hasMore && logs.length > 0 && (
<div className="flex items-center justify-center py-12">
<div className="flex items-center gap-2">
<div className="flex flex-col items-center gap-3">
<span className="text-text-dimmed">Showing all {logs.length} logs</span>
</div>
</div>
Comment on lines 213 to 218

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 onCheckForMore callback is accepted by LogsTable but never invoked

The PR description states "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." The route component creates handleCheckForMore at route.tsx:475-485 and passes it as onCheckForMore={handleCheckForMore} to LogsTable at route.tsx:498. The LogsTable component accepts and destructures this prop at LogsTable.tsx:66, but never calls it anywhere in the component. There is no button or UI element that triggers onCheckForMore.

Root Cause and Impact

The entire "check for new logs" pipeline is wired up on the backend side: handleCheckForMore builds a URL without a cursor, sets isCheckingForNewRef.current = true, and fires fetcher.load(). The effect at route.tsx:397-423 handles the response by prepending new logs when isCheckingForNewRef.current is true. But the LogsTable component at LogsTable.tsx:59-221 never renders a button or calls onCheckForMore(), so this entire feature is dead code.

The "Show all logs" message area at LogsTable.tsx:213-219 was updated (the diff shows the div changing from flex items-center gap-2 to flex flex-col items-center gap-3), which looks like it was prepared to include a "Check for new" button but the button itself was never added.

Impact: Users who reach the end of their logs list have no way to check for newer logs without manually refreshing the page, which is exactly the problem this feature was supposed to fix.

(Refers to lines 213-219)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Expand Down
49 changes: 32 additions & 17 deletions apps/webapp/app/components/primitives/DateTime.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { GlobeAltIcon, GlobeAmericasIcon } from "@heroicons/react/20/solid";
import { useRouteLoaderData } from "@remix-run/react";
import { Laptop } from "lucide-react";
import { memo, type ReactNode, useMemo, useSyncExternalStore } from "react";
import { CopyButton } from "./CopyButton";
Expand All @@ -19,7 +20,7 @@ function getLocalTimeZone(): string {
// For SSR compatibility: returns "UTC" on server, actual timezone on client
function subscribeToTimeZone() {
// No-op - timezone doesn't change
return () => { };
return () => {};
}

function getTimeZoneSnapshot(): string {
Expand All @@ -39,6 +40,18 @@ export function useLocalTimeZone(): string {
return useSyncExternalStore(subscribeToTimeZone, getTimeZoneSnapshot, getServerTimeZoneSnapshot);
}

/**
* Hook to get the user's preferred timezone.
* Returns the timezone stored in the user's preferences cookie (from root loader),
* falling back to the browser's local timezone if not set.
*/
export function useUserTimeZone(): string {
const rootData = useRouteLoaderData("root") as { timezone?: string } | undefined;
const localTimeZone = useLocalTimeZone();
// Use stored timezone from cookie, or fall back to browser's local timezone
return rootData?.timezone && rootData.timezone !== "UTC" ? rootData.timezone : localTimeZone;
}

type DateTimeProps = {
date: Date | string;
timeZone?: string;
Expand All @@ -63,15 +76,15 @@ export const DateTime = ({
hour12 = true,
}: DateTimeProps) => {
const locales = useLocales();
const localTimeZone = useLocalTimeZone();
const userTimeZone = useUserTimeZone();

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

const formattedDateTime = (
<span suppressHydrationWarning>
{formatDateTime(
realDate,
timeZone ?? localTimeZone,
timeZone ?? userTimeZone,
locales,
includeSeconds,
includeTime,
Expand All @@ -91,7 +104,7 @@ export const DateTime = ({
<TooltipContent
realDate={realDate}
timeZone={timeZone}
localTimeZone={localTimeZone}
localTimeZone={userTimeZone}
locales={locales}
/>
}
Expand Down Expand Up @@ -167,7 +180,7 @@ export function formatDateTimeISO(date: Date, timeZone: string): string {
// New component that only shows date when it changes
export const SmartDateTime = ({ date, previousDate = null, hour12 = true }: DateTimeProps) => {
const locales = useLocales();
const localTimeZone = useLocalTimeZone();
const userTimeZone = useUserTimeZone();
const realDate = typeof date === "string" ? new Date(date) : date;
const realPrevDate = previousDate
? typeof previousDate === "string"
Expand All @@ -180,8 +193,8 @@ export const SmartDateTime = ({ date, previousDate = null, hour12 = true }: Date

// Format with appropriate function
const formattedDateTime = showDatePart
? formatSmartDateTime(realDate, localTimeZone, locales, hour12)
: formatTimeOnly(realDate, localTimeZone, locales, hour12);
? formatSmartDateTime(realDate, userTimeZone, locales, hour12)
: formatTimeOnly(realDate, userTimeZone, locales, hour12);

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

const DateTimeAccurateInner = ({
date,
timeZone = "UTC",
timeZone,
previousDate = null,
showTooltip = true,
hideDate = false,
hour12 = true,
}: DateTimeProps) => {
const locales = useLocales();
const localTimeZone = useLocalTimeZone();
const userTimeZone = useUserTimeZone();
// Use provided timeZone prop if available, otherwise fall back to user's preferred timezone
const displayTimeZone = timeZone ?? userTimeZone;
const realDate = typeof date === "string" ? new Date(date) : date;
const realPrevDate = previousDate
? typeof previousDate === "string"
Expand All @@ -253,13 +268,13 @@ const DateTimeAccurateInner = ({
// Smart formatting based on whether date changed
const formattedDateTime = useMemo(() => {
return hideDate
? formatTimeOnly(realDate, localTimeZone, locales, hour12)
? formatTimeOnly(realDate, displayTimeZone, locales, hour12)
: realPrevDate
? isSameDay(realDate, realPrevDate)
? formatTimeOnly(realDate, localTimeZone, locales, hour12)
: formatDateTimeAccurate(realDate, localTimeZone, locales, hour12)
: formatDateTimeAccurate(realDate, localTimeZone, locales, hour12);
}, [realDate, localTimeZone, locales, hour12, hideDate, previousDate]);
? formatTimeOnly(realDate, displayTimeZone, locales, hour12)
: formatDateTimeAccurate(realDate, displayTimeZone, locales, hour12)
: formatDateTimeAccurate(realDate, displayTimeZone, locales, hour12);
}, [realDate, displayTimeZone, locales, hour12, hideDate, previousDate]);

if (!showTooltip)
return <span suppressHydrationWarning>{formattedDateTime.replace(/\s/g, String.fromCharCode(32))}</span>;
Expand All @@ -268,7 +283,7 @@ const DateTimeAccurateInner = ({
<TooltipContent
realDate={realDate}
timeZone={timeZone}
localTimeZone={localTimeZone}
localTimeZone={userTimeZone}
locales={locales}
/>
);
Expand Down Expand Up @@ -328,9 +343,9 @@ function formatDateTimeAccurate(

export const DateTimeShort = ({ date, hour12 = true }: DateTimeProps) => {
const locales = useLocales();
const localTimeZone = useLocalTimeZone();
const userTimeZone = useUserTimeZone();
const realDate = typeof date === "string" ? new Date(date) : date;
const formattedDateTime = formatDateTimeShort(realDate, localTimeZone, locales, hour12);
const formattedDateTime = formatDateTimeShort(realDate, userTimeZone, locales, hour12);

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