Skip to content
Merged
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>
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