From fefc53ea7dbe02da66086e0cc7c64042fe0f7055 Mon Sep 17 00:00:00 2001 From: Oskar Date: Fri, 5 Dec 2025 10:24:26 +0100 Subject: [PATCH 1/4] feat(webapp): persist runs table filters via tableState URL parameter - Store current filter state from runs table as `tableState` search param when navigating to individual run pages - Restore filters when navigating back from run detail view to runs list - Update `v3RunPath` and `v3RunSpanPath` helpers to accept optional searchParams - Use `useOptimisticLocation` to capture current search params in TaskRunsTable - Parse `tableState` param in run detail route and pass filters to back button This improves UX by remembering filter selections (task, status, date range, etc.) when users click into a run and then navigate back to the runs list. --- .../app/components/runs/v3/TaskRunsTable.tsx | 11 +++++++++-- .../route.tsx | 7 ++++++- apps/webapp/app/utils/pathBuilder.ts | 15 ++++++++++----- 3 files changed, 25 insertions(+), 8 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx index 60017c69fb..8ba5591718 100644 --- a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx +++ b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx @@ -55,6 +55,7 @@ import { filterableTaskRunStatuses, TaskRunStatusCombo, } from "./TaskRunStatus"; +import { useOptimisticLocation } from "~/hooks/useOptimisticLocation"; type RunsTableProps = { total: number; @@ -81,6 +82,8 @@ export function TaskRunsTable({ const checkboxes = useRef<(HTMLInputElement | null)[]>([]); const { has, hasAll, select, deselect, toggle } = useSelectedItems(allowSelection); const { isManagedCloud } = useFeatures(); + const location = useOptimisticLocation(); + const tableStateParam = encodeURIComponent(location.search); const showCompute = isManagedCloud; @@ -293,16 +296,20 @@ export function TaskRunsTable({ ) : ( runs.map((run, index) => { + const searchParams = new URLSearchParams(); + if (tableStateParam) { + searchParams.set("tableState", tableStateParam); + } const path = v3RunSpanPath(organization, project, run.environment, run, { spanId: run.spanId, - }); + }, searchParams); return ( {allowSelection && ( { + onChange={() => { toggle(run.friendlyId); }} ref={(r) => { diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx index 79ab0b8e5b..4cfb3f378f 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx @@ -98,6 +98,7 @@ import { SpanView } from "../resources.orgs.$organizationSlug.projects.$projectP import { useSearchParams } from "~/hooks/useSearchParam"; import { CopyableText } from "~/components/primitives/CopyableText"; import type { SpanOverride } from "~/v3/eventRepository/eventRepository.types"; +import { getRunFiltersFromSearchParams } from "~/components/runs/v3/RunFilters"; const resizableSettings = { parent: { @@ -191,13 +192,17 @@ export default function Page() { logCount: trace?.events.length ?? 0, isCompleted: run.completedAt !== null, }); + const { value } = useSearchParams(); + const params = decodeURIComponent(value("tableState") ?? ""); + const searchParams = new URLSearchParams(params); + const filters = getRunFiltersFromSearchParams(searchParams); return ( <> } diff --git a/apps/webapp/app/utils/pathBuilder.ts b/apps/webapp/app/utils/pathBuilder.ts index f82165ae9d..a9a4bab906 100644 --- a/apps/webapp/app/utils/pathBuilder.ts +++ b/apps/webapp/app/utils/pathBuilder.ts @@ -288,15 +288,17 @@ export function v3RunPath( organization: OrgForPath, project: ProjectForPath, environment: EnvironmentForPath, - run: v3RunForPath + run: v3RunForPath, + searchParams?: URLSearchParams ) { - return `${v3RunsPath(organization, project, environment)}/${run.friendlyId}`; + const query = searchParams ? `?${searchParams.toString()}` : ""; + return `${v3RunsPath(organization, project, environment)}/${run.friendlyId}${query}`; } export function v3RunRedirectPath( organization: OrgForPath, project: ProjectForPath, - run: v3RunForPath + run: v3RunForPath, ) { return `${v3ProjectPath(organization, project)}/runs/${run.friendlyId}`; } @@ -310,9 +312,12 @@ export function v3RunSpanPath( project: ProjectForPath, environment: EnvironmentForPath, run: v3RunForPath, - span: v3SpanForPath + span: v3SpanForPath, + searchParams?: URLSearchParams ) { - return `${v3RunPath(organization, project, environment, run)}?span=${span.spanId}`; + searchParams = searchParams ?? new URLSearchParams(); + searchParams.set("span", encodeURIComponent(span.spanId)); + return `${v3RunPath(organization, project, environment, run, searchParams)}`; } export function v3RunStreamingPath( From c6debec9798ffb8283f6638d9f4c32e6f840c754 Mon Sep 17 00:00:00 2001 From: Oskar Date: Fri, 5 Dec 2025 11:19:11 +0100 Subject: [PATCH 2/4] feat(CopyableText): Add text-below variant with click-to-copy tooltip Add new text-below variant that shows "Click to copy" tooltip on hover and "Copied" on click. Also add controlled open/onOpenChange props to SimpleTooltip for managing tooltip visibility. --- .../components/primitives/CopyableText.tsx | 108 ++++++++++++------ .../app/components/primitives/Tooltip.tsx | 8 +- .../route.tsx | 2 +- 3 files changed, 78 insertions(+), 40 deletions(-) diff --git a/apps/webapp/app/components/primitives/CopyableText.tsx b/apps/webapp/app/components/primitives/CopyableText.tsx index 67e01af795..ac43f30c76 100644 --- a/apps/webapp/app/components/primitives/CopyableText.tsx +++ b/apps/webapp/app/components/primitives/CopyableText.tsx @@ -9,53 +9,87 @@ export function CopyableText({ copyValue, className, asChild, + variant, }: { value: string; copyValue?: string; className?: string; asChild?: boolean; + variant?: "icon-right" | "text-below"; }) { const [isHovered, setIsHovered] = useState(false); const { copy, copied } = useCopy(copyValue ?? value); - return ( - setIsHovered(false)} - > - setIsHovered(true)}>{value} + variant = variant ?? "icon-right"; + + if (variant === "icon-right") { + return ( e.stopPropagation()} - className={cn( - "absolute -right-6 top-0 z-10 size-6 font-sans", - isHovered ? "flex" : "hidden" - )} + className={cn("group relative inline-flex h-6 items-center", className)} + onMouseLeave={() => setIsHovered(false)} > - - {copied ? ( - - ) : ( - - )} - - } - content={copied ? "Copied!" : "Copy"} - className="font-sans" - disableHoverableContent - asChild={asChild} - /> + setIsHovered(true)}>{value} + e.stopPropagation()} + className={cn( + "absolute -right-6 top-0 z-10 size-6 font-sans", + isHovered ? "flex" : "hidden" + )} + > + + {copied ? ( + + ) : ( + + )} + + } + content={copied ? "Copied!" : "Copy"} + className="font-sans" + disableHoverableContent + asChild={asChild} + /> + - - ); + ); + } + + if (variant === "text-below") { + return ( + { + e.stopPropagation(); + copy(); + }} + className={cn( + "cursor-pointer text-text-bright transition-colors hover:text-white", + className + )} + > + {value} + + } + content={copied ? "Copied" : "Click to copy"} + className="font-sans px-2 py-1" + disableHoverableContent + open={isHovered || copied} + onOpenChange={setIsHovered} + /> + ); + } + + return null; } diff --git a/apps/webapp/app/components/primitives/Tooltip.tsx b/apps/webapp/app/components/primitives/Tooltip.tsx index 15dd72894a..5c681927b5 100644 --- a/apps/webapp/app/components/primitives/Tooltip.tsx +++ b/apps/webapp/app/components/primitives/Tooltip.tsx @@ -6,7 +6,7 @@ import { cn } from "~/utils/cn"; const variantClasses = { basic: "bg-background-bright border border-grid-bright rounded px-3 py-2 text-sm text-text-bright shadow-md fade-in-50", - dark: "bg-background-dimmed border border-grid-bright rounded px-3 py-2 text-sm text-text-bright shadow-md fade-in-50", + dark: "bg-background-dimmed border border-grid-bright rounded px-3 py-2 text-sm text-text-bright shadow-md fade-in-50" }; type Variant = keyof typeof variantClasses; @@ -64,6 +64,8 @@ function SimpleTooltip({ buttonStyle, asChild = false, sideOffset, + open, + onOpenChange, }: { button: React.ReactNode; content: React.ReactNode; @@ -76,10 +78,12 @@ function SimpleTooltip({ buttonStyle?: React.CSSProperties; asChild?: boolean; sideOffset?: number; + open?: boolean; + onOpenChange?: (open: boolean) => void; }) { return ( - + } + title={} /> {environment.type === "DEVELOPMENT" && } From 4e79186939dd8c77514210ff2600e52d52bfe6a8 Mon Sep 17 00:00:00 2001 From: Oskar Date: Fri, 5 Dec 2025 17:01:58 +0100 Subject: [PATCH 3/4] feat(webapp): Add navigation for adjacent runs for Run page (with keyboard shortcuts) Add previous/next run navigation buttons to run detail page header Support [ and ] keyboard shortcuts to jump between adjacent runs Preserve runs table state (filters, pagination) when navigating Preload adjacent page runs at boundaries for seamless navigation Add actions prop to PageTitle component Document shortcut in keyboard shortcuts panel --- apps/webapp/app/components/Shortcuts.tsx | 4 + .../app/components/primitives/PageHeader.tsx | 4 +- .../app/components/runs/v3/TaskRunsTable.tsx | 2 +- .../route.tsx | 230 +++++++++++++++++- 4 files changed, 231 insertions(+), 9 deletions(-) diff --git a/apps/webapp/app/components/Shortcuts.tsx b/apps/webapp/app/components/Shortcuts.tsx index ab328afde7..b21c556595 100644 --- a/apps/webapp/app/components/Shortcuts.tsx +++ b/apps/webapp/app/components/Shortcuts.tsx @@ -134,6 +134,10 @@ function ShortcutContent() { + + + + diff --git a/apps/webapp/app/components/primitives/PageHeader.tsx b/apps/webapp/app/components/primitives/PageHeader.tsx index 7855e241e3..e6d3091db9 100644 --- a/apps/webapp/app/components/primitives/PageHeader.tsx +++ b/apps/webapp/app/components/primitives/PageHeader.tsx @@ -36,9 +36,10 @@ type PageTitleProps = { to: string; text: string; }; + actions?: ReactNode; }; -export function PageTitle({ title, backButton }: PageTitleProps) { +export function PageTitle({ title, backButton, actions }: PageTitleProps) { return (
{backButton && ( @@ -53,6 +54,7 @@ export function PageTitle({ title, backButton }: PageTitleProps) {
)} {title} + {actions &&
{actions}
} ); } diff --git a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx index 8ba5591718..c0ae8d2f62 100644 --- a/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx +++ b/apps/webapp/app/components/runs/v3/TaskRunsTable.tsx @@ -83,7 +83,7 @@ export function TaskRunsTable({ const { has, hasAll, select, deselect, toggle } = useSelectedItems(allowSelection); const { isManagedCloud } = useFeatures(); const location = useOptimisticLocation(); - const tableStateParam = encodeURIComponent(location.search); + const tableStateParam = encodeURIComponent(location.search ? `${location.search}&rt=1` : "rt=1"); const showCompute = isManagedCloud; diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx index 2f780a14ba..a1de8026b0 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx @@ -2,6 +2,7 @@ import { ArrowUturnLeftIcon, BoltSlashIcon, BookOpenIcon, + ChevronUpIcon, ChevronDownIcon, ChevronRightIcon, InformationCircleIcon, @@ -20,9 +21,9 @@ import { nanosecondsToMilliseconds, tryCatch, } from "@trigger.dev/core/v3"; -import type { RuntimeEnvironmentType } from "@trigger.dev/database"; +import type { $Enums, RuntimeEnvironmentType } from "@trigger.dev/database"; import { motion } from "framer-motion"; -import { useCallback, useEffect, useRef, useState } from "react"; +import React, { useCallback, useEffect, useRef, useState } from "react"; import { useHotkeys } from "react-hotkeys-hook"; import { redirect } from "remix-typedjson"; import { MoveToTopIcon } from "~/assets/icons/MoveToTopIcon"; @@ -68,7 +69,6 @@ import { eventBorderClassName, } from "~/components/runs/v3/SpanTitle"; import { TaskRunStatusIcon, runStatusClassNameColor } from "~/components/runs/v3/TaskRunStatus"; -import { env } from "~/env.server"; import { useDebounce } from "~/hooks/useDebounce"; import { useEnvironment } from "~/hooks/useEnvironment"; import { useEventSource } from "~/hooks/useEventSource"; @@ -88,6 +88,7 @@ import { docsPath, v3BillingPath, v3RunParamsSchema, + v3RunPath, v3RunRedirectPath, v3RunSpanPath, v3RunStreamingPath, @@ -99,6 +100,11 @@ import { useSearchParams } from "~/hooks/useSearchParam"; import { CopyableText } from "~/components/primitives/CopyableText"; import type { SpanOverride } from "~/v3/eventRepository/eventRepository.types"; import { getRunFiltersFromSearchParams } from "~/components/runs/v3/RunFilters"; +import { NextRunListPresenter } from "~/presenters/v3/NextRunListPresenter.server"; +import { $replica } from "~/db.server"; +import { clickhouseClient } from "~/services/clickhouseInstance.server"; +import { findProjectBySlug } from "~/models/project.server"; +import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server"; const resizableSettings = { parent: { @@ -170,6 +176,82 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { const parent = await getResizableSnapshot(request, resizableSettings.parent.autosaveId); const tree = await getResizableSnapshot(request, resizableSettings.tree.autosaveId); + // Load runs list data from tableState if present + let runsList: { + runs: Array<{ friendlyId: string }>; + pagination: { next?: string; previous?: string }; + prevPageLastRun?: { friendlyId: string; cursor: string }; + nextPageFirstRun?: { friendlyId: string; cursor: string }; + } | null = null; + const tableStateParam = url.searchParams.get("tableState"); + if (tableStateParam) { + try { + const tableStateSearchParams = new URLSearchParams(decodeURIComponent(tableStateParam)); + const filters = getRunFiltersFromSearchParams(tableStateSearchParams); + + const project = await findProjectBySlug(organizationSlug, projectParam, userId); + const environment = await findEnvironmentBySlug(project?.id ?? "", envParam, userId); + + if (project && environment) { + const runsListPresenter = new NextRunListPresenter($replica, clickhouseClient); + const currentPageResult = await runsListPresenter.call(project.organizationId, environment.id, { + userId, + projectId: project.id, + ...filters, + pageSize: 25, // Load enough runs to provide navigation context + }); + + runsList = { + runs: currentPageResult.runs, + pagination: currentPageResult.pagination, + }; + + // Check if the current run is at the boundary and preload adjacent page if needed + const currentRunIndex = currentPageResult.runs.findIndex((r) => r.friendlyId === runParam); + + // If current run is first in list and there's a previous page, load the last run from prev page + if (currentRunIndex === 0 && currentPageResult.pagination.previous) { + const prevPageResult = await runsListPresenter.call(project.organizationId, environment.id, { + userId, + projectId: project.id, + ...filters, + cursor: currentPageResult.pagination.previous, + direction: "backward", + pageSize: 1, // We only need the last run from the previous page + }); + if (prevPageResult.runs.length > 0) { + runsList.prevPageLastRun = { + friendlyId: prevPageResult.runs[0].friendlyId, + cursor: currentPageResult.pagination.previous, + }; + } + } + + // If current run is last in list and there's a next page, load the first run from next page + if (currentRunIndex === currentPageResult.runs.length - 1 && currentPageResult.pagination.next) { + const nextPageResult = await runsListPresenter.call(project.organizationId, environment.id, { + userId, + projectId: project.id, + ...filters, + cursor: currentPageResult.pagination.next, + direction: "forward", + pageSize: 1, // We only need the first run from the next page + }); + if (nextPageResult.runs.length > 0) { + runsList.nextPageFirstRun = { + friendlyId: nextPageResult.runs[0].friendlyId, + cursor: currentPageResult.pagination.next, + }; + } + } + } + } catch (error) { + // If there's an error parsing or loading runs list, just ignore it + // and don't include the runsList in the response + console.error("Error loading runs list from tableState:", error); + } + } + return json({ run: result.run, trace: result.trace, @@ -178,13 +260,14 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { parent, tree, }, + runsList, }); }; type LoaderData = SerializeFrom; export default function Page() { - const { run, trace, resizable, maximumLiveReloadingSetting } = useLoaderData(); + const { run, trace, resizable, maximumLiveReloadingSetting, runsList } = useLoaderData(); const organization = useOrganization(); const project = useProject(); const environment = useEnvironment(); @@ -193,9 +276,11 @@ export default function Page() { isCompleted: run.completedAt !== null, }); const { value } = useSearchParams(); - const params = decodeURIComponent(value("tableState") ?? ""); - const searchParams = new URLSearchParams(params); - const filters = getRunFiltersFromSearchParams(searchParams); + const tableState = decodeURIComponent(value("tableState") ?? ""); + const tableStateSearchParams = new URLSearchParams(tableState); + const filters = getRunFiltersFromSearchParams(tableStateSearchParams); + + const [previousRunPath, nextRunPath] = useAdjacentRunPaths({organization, project, environment, tableStateSearchParams, run, runsList}); return ( <> @@ -206,6 +291,12 @@ export default function Page() { text: "Runs", }} title={} + actions={ + tableState && (
+ + +
) + } /> {environment.type === "DEVELOPMENT" && } @@ -282,6 +373,7 @@ export default function Page() { trace={trace} maximumLiveReloadingSetting={maximumLiveReloadingSetting} resizable={resizable} + runsList={runsList} /> ) : ( )} @@ -1437,6 +1530,7 @@ function KeyboardShortcuts({ return ( <> + expandAllBelowDepth(0)} @@ -1453,6 +1547,17 @@ function KeyboardShortcuts({ ); } +function AdjacentRunsShortcuts({ +}) { + return (
+ + + + Adjacent runs + +
); +} + function ArrowKeyShortcuts() { return (
@@ -1531,3 +1636,114 @@ function SearchField({ onChange }: { onChange: (value: string) => void }) { /> ); } + +function useAdjacentRunPaths({ + organization, + project, + environment, + tableStateSearchParams, + run, + runsList, +}: { + organization: { slug: string }; + project: { slug: string }; + environment: { slug: string }; + tableStateSearchParams: URLSearchParams; + run: { friendlyId: string }; + runsList: { + runs: Array<{ friendlyId: string }>; + pagination: { next?: string; previous?: string }; + prevPageLastRun?: { friendlyId: string; cursor: string }; + nextPageFirstRun?: { friendlyId: string; cursor: string }; + } | null; +}): [string | null, string | null] { + return React.useMemo(() => { + if (!runsList || runsList.runs.length === 0) { + return [null, null]; + } + + const currentIndex = runsList.runs.findIndex((r) => r.friendlyId === run.friendlyId); + + if (currentIndex === -1) { + return [null, null]; + } + + // Determine previous run: use prevPageLastRun if at first position, otherwise use previous run in list + let previousRun: { friendlyId: string } | null = null; + const previousRunTableState = new URLSearchParams(tableStateSearchParams.toString()); + if (currentIndex > 0) { + previousRun = runsList.runs[currentIndex - 1]; + } else if (runsList.prevPageLastRun) { + previousRun = runsList.prevPageLastRun; + // Update tableState with the new cursor for the previous page + previousRunTableState.set("cursor", runsList.prevPageLastRun.cursor); + previousRunTableState.set("direction", "backward"); + } + + // Determine next run: use nextPageFirstRun if at last position, otherwise use next run in list + let nextRun: { friendlyId: string } | null = null; + const nextRunTableState = new URLSearchParams(tableStateSearchParams.toString()); + if (currentIndex < runsList.runs.length - 1) { + nextRun = runsList.runs[currentIndex + 1]; + } else if (runsList.nextPageFirstRun) { + nextRun = runsList.nextPageFirstRun; + // Update tableState with the new cursor for the next page + nextRunTableState.set("cursor", runsList.nextPageFirstRun.cursor); + nextRunTableState.set("direction", "forward"); + } + + const previousURLSearchParams = new URLSearchParams(); + previousURLSearchParams.set("tableState", previousRunTableState.toString()); + const previousRunPath = previousRun + ? v3RunPath(organization, project, environment, previousRun, previousURLSearchParams) + : null; + + const nextURLSearchParams = new URLSearchParams(); + nextURLSearchParams.set("tableState", nextRunTableState.toString()); + const nextRunPath = nextRun + ? v3RunPath(organization, project, environment, nextRun, nextURLSearchParams) + : null; + + return [previousRunPath, nextRunPath]; + }, [organization, project, environment, tableStateSearchParams, run.friendlyId, runsList]); +} + + +function PreviousRunButton({ to }: { to: string | null }) { + return ( +
+ !to && e.preventDefault()} + shortcut={{ key: "[" }} + tooltip="Previous Run" + /> +
+ ); +} + +function NextRunButton({ to }: { to: string | null }) { + return ( +
+ !to && e.preventDefault()} + shortcut={{ key: "]" }} + tooltip="Next Run" + /> +
+ ); +} + From 6ee1295407f7a4b1346f4ac50641abdef2f02d06 Mon Sep 17 00:00:00 2001 From: Oskar Date: Fri, 5 Dec 2025 21:00:51 +0100 Subject: [PATCH 4/4] feat(webapp): UI improvements, PR feedback changes --- .../app/components/primitives/Buttons.tsx | 2 +- .../app/components/primitives/CopyableText.tsx | 16 +++++++++------- .../app/components/primitives/PageHeader.tsx | 4 +--- .../app/components/primitives/ShortcutKey.tsx | 6 +++--- .../route.tsx | 17 +++++++++-------- apps/webapp/app/utils/pathBuilder.ts | 2 +- 6 files changed, 24 insertions(+), 23 deletions(-) diff --git a/apps/webapp/app/components/primitives/Buttons.tsx b/apps/webapp/app/components/primitives/Buttons.tsx index 47c8d3d674..e7767b2bc7 100644 --- a/apps/webapp/app/components/primitives/Buttons.tsx +++ b/apps/webapp/app/components/primitives/Buttons.tsx @@ -408,7 +408,7 @@ export const NavLinkButton = ({ to, className, target, ...props }: NavLinkPropsT return ( {({ isActive, isPending }) => ( diff --git a/apps/webapp/app/components/primitives/CopyableText.tsx b/apps/webapp/app/components/primitives/CopyableText.tsx index ac43f30c76..9284897be5 100644 --- a/apps/webapp/app/components/primitives/CopyableText.tsx +++ b/apps/webapp/app/components/primitives/CopyableText.tsx @@ -3,6 +3,7 @@ import { useState } from "react"; import { SimpleTooltip } from "~/components/primitives/Tooltip"; import { useCopy } from "~/hooks/useCopy"; import { cn } from "~/utils/cn"; +import { Button } from "./Buttons"; export function CopyableText({ value, @@ -20,9 +21,9 @@ export function CopyableText({ const [isHovered, setIsHovered] = useState(false); const { copy, copied } = useCopy(copyValue ?? value); - variant = variant ?? "icon-right"; + const resolvedVariant = variant ?? "icon-right"; - if (variant === "icon-right") { + if (resolvedVariant === "icon-right") { return ( { e.stopPropagation(); copy(); }} className={cn( - "cursor-pointer text-text-bright transition-colors hover:text-white", + "cursor-pointer bg-transparent py-0 px-1 text-left text-text-bright transition-colors hover:text-white hover:bg-transparent", className )} > - {value} - + {value} + } content={copied ? "Copied" : "Click to copy"} className="font-sans px-2 py-1" diff --git a/apps/webapp/app/components/primitives/PageHeader.tsx b/apps/webapp/app/components/primitives/PageHeader.tsx index e6d3091db9..7855e241e3 100644 --- a/apps/webapp/app/components/primitives/PageHeader.tsx +++ b/apps/webapp/app/components/primitives/PageHeader.tsx @@ -36,10 +36,9 @@ type PageTitleProps = { to: string; text: string; }; - actions?: ReactNode; }; -export function PageTitle({ title, backButton, actions }: PageTitleProps) { +export function PageTitle({ title, backButton }: PageTitleProps) { return (
{backButton && ( @@ -54,7 +53,6 @@ export function PageTitle({ title, backButton, actions }: PageTitleProps) {
)} {title} - {actions &&
{actions}
}
); } diff --git a/apps/webapp/app/components/primitives/ShortcutKey.tsx b/apps/webapp/app/components/primitives/ShortcutKey.tsx index 04b1f36737..567cf68d61 100644 --- a/apps/webapp/app/components/primitives/ShortcutKey.tsx +++ b/apps/webapp/app/components/primitives/ShortcutKey.tsx @@ -9,11 +9,11 @@ import { useOperatingSystem } from "./OperatingSystemProvider"; import { KeyboardEnterIcon } from "~/assets/icons/KeyboardEnterIcon"; const medium = - "text-[0.75rem] font-medium min-w-[17px] rounded-[2px] tabular-nums px-1 ml-1 -mr-0.5 flex items-center gap-x-1.5 border border-dimmed/40 text-text-dimmed group-hover:text-text-bright/80 group-hover:border-dimmed/60 transition uppercase"; + "justify-center min-w-[1.25rem] min-h-[1.25rem] text-[0.65rem] font-mono font-medium rounded-[2px] tabular-nums px-1 ml-1 -mr-0.5 flex items-center gap-x-1.5 border border-dimmed/40 text-text-dimmed group-hover:text-text-bright/80 group-hover:border-dimmed/60 transition uppercase"; export const variants = { small: - "text-[0.6rem] font-medium min-w-[17px] rounded-[2px] tabular-nums px-1 ml-1 -mr-0.5 flex items-center gap-x-1 border border-text-dimmed/40 text-text-dimmed group-hover:text-text-bright/80 group-hover:border-text-dimmed/60 transition uppercase", + "justify-center text-[0.6rem] font-mono font-medium min-w-[1rem] min-h-[1rem] rounded-[2px] tabular-nums px-1 ml-1 -mr-0.5 flex items-center gap-x-1 border border-text-dimmed/40 text-text-dimmed group-hover:text-text-bright/80 group-hover:border-text-dimmed/60 transition uppercase", medium: cn(medium, "group-hover:border-charcoal-550"), "medium/bright": cn(medium, "bg-charcoal-750 text-text-bright border-charcoal-650"), }; @@ -57,7 +57,7 @@ export function ShortcutKey({ shortcut, variant, className }: ShortcutKeyProps) function keyString(key: string, isMac: boolean, variant: "small" | "medium" | "medium/bright") { key = key.toLowerCase(); - const className = variant === "small" ? "w-2.5 h-4" : "w-3 h-5"; + const className = variant === "small" ? "w-2.5 h-4" : "w-2.5 h-4.5"; switch (key) { case "enter": diff --git a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx index a1de8026b0..757a94a427 100644 --- a/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx +++ b/apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx @@ -290,13 +290,13 @@ export default function Page() { to: v3RunsPath(organization, project, environment, filters), text: "Runs", }} - title={} - actions={ - tableState && (
+ title={<> + + {tableState && (
-
) - } +
)} + } /> {environment.type === "DEVELOPMENT" && } @@ -1547,8 +1547,7 @@ function KeyboardShortcuts({ ); } -function AdjacentRunsShortcuts({ -}) { +function AdjacentRunsShortcuts() { return (
@@ -1604,7 +1603,7 @@ function NumberShortcuts({ toggleLevel }: { toggleLevel: (depth: number) => void return (
0 - + 9 Toggle level @@ -1723,6 +1722,7 @@ function PreviousRunButton({ to }: { to: string | null }) { onClick={(e) => !to && e.preventDefault()} shortcut={{ key: "[" }} tooltip="Previous Run" + disabled={!to} />
); @@ -1742,6 +1742,7 @@ function NextRunButton({ to }: { to: string | null }) { onClick={(e) => !to && e.preventDefault()} shortcut={{ key: "]" }} tooltip="Next Run" + disabled={!to} />
); diff --git a/apps/webapp/app/utils/pathBuilder.ts b/apps/webapp/app/utils/pathBuilder.ts index a9a4bab906..3061082ed9 100644 --- a/apps/webapp/app/utils/pathBuilder.ts +++ b/apps/webapp/app/utils/pathBuilder.ts @@ -316,7 +316,7 @@ export function v3RunSpanPath( searchParams?: URLSearchParams ) { searchParams = searchParams ?? new URLSearchParams(); - searchParams.set("span", encodeURIComponent(span.spanId)); + searchParams.set("span", span.spanId); return `${v3RunPath(organization, project, environment, run, searchParams)}`; }