chore(webapp): Run navigation UI improvements (#2802)

**Improvements to the run ID copy button and run navigation buttons for
consistency**

- Adds some x-padding and layout adjustment to the copy ID button.
<img width="664" height="114" alt="CleanShot 2025-12-19 at 16 05 21@2x"
src="https://github.com/user-attachments/assets/ebc8e0de-011b-419c-bdcc-eb4157553d1c"
/>

- New custom navigation icons that work better at tiny sizes
<img width="330" height="196" alt="CleanShot 2025-12-19 at 16 06 38@2x"
src="https://github.com/user-attachments/assets/bfd8d6b8-8a65-4eac-9ce1-d70acf0ad265"
/>

Some other small improvements/fixes:
- Fixes a browser html error where there was a <button> inside a
<button>
- Updates the shortcut description to match the tooltip text for
consistency
- Made the hover states more consistent
- The shortcut bar at the bottom snaps to the list sooner because there
are more items now
This commit is contained in:
James Ritchie
2026-01-06 18:20:54 +00:00
committed by GitHub
parent edf5b142fc
commit db0df17a6a
5 changed files with 112 additions and 56 deletions
@@ -0,0 +1,13 @@
export function ChevronExtraSmallDown({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M15 6L9.75926 12.1142C9.36016 12.5798 8.63984 12.5798 8.24074 12.1142L3 6"
stroke="currentColor"
strokeWidth="2"
strokeMiterlimit="1.00244"
strokeLinecap="round"
/>
</svg>
);
}
@@ -0,0 +1,13 @@
export function ChevronExtraSmallUp({ className }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M3 12L8.24074 5.8858C8.63984 5.42019 9.36016 5.42019 9.75926 5.8858L15 12"
stroke="currentColor"
strokeWidth="2"
strokeMiterlimit="1.00244"
strokeLinecap="round"
/>
</svg>
);
}
+1 -1
View File
@@ -134,7 +134,7 @@ function ShortcutContent() {
<ShortcutKey shortcut={{ key: "arrowleft" }} variant="medium/bright" />
<ShortcutKey shortcut={{ key: "arrowright" }} variant="medium/bright" />
</Shortcut>
<Shortcut name="Jump to adjacent">
<Shortcut name="Jump to next/previous run">
<ShortcutKey shortcut={{ key: "[" }} variant="medium/bright" />
<ShortcutKey shortcut={{ key: "]" }} variant="medium/bright" />
</Shortcut>
@@ -72,23 +72,24 @@ export function CopyableText({
button={
<Button
variant="minimal/small"
onClick={(e) => {
onClick={(e) => {
e.stopPropagation();
copy();
copy();
}}
className={cn(
"cursor-pointer bg-transparent py-0 px-1 text-left text-text-bright transition-colors hover:text-white hover:bg-transparent",
"cursor-pointer bg-transparent px-1 py-0 text-left text-text-dimmed transition-colors hover:bg-transparent",
className
)}
>
<span>{value}</span>
<span className="transition-colors group-hover/button:text-text-bright">{value}</span>
</Button>
}
content={copied ? "Copied" : "Click to copy"}
className="font-sans px-2 py-1"
content={copied ? "Copied" : "Copy"}
className="px-2 py-1 font-sans"
disableHoverableContent
open={isHovered || copied}
onOpenChange={setIsHovered}
asChild
/>
);
}
@@ -2,7 +2,6 @@ import {
ArrowUturnLeftIcon,
BoltSlashIcon,
BookOpenIcon,
ChevronUpIcon,
ChevronDownIcon,
ChevronRightIcon,
InformationCircleIcon,
@@ -12,6 +11,7 @@ import {
MagnifyingGlassPlusIcon,
StopCircleIcon,
} from "@heroicons/react/20/solid";
import { useLoaderData, useRevalidator } from "@remix-run/react";
import { type LoaderFunctionArgs, type SerializeFrom, json } from "@remix-run/server-runtime";
import { type Virtualizer } from "@tanstack/react-virtual";
@@ -26,6 +26,8 @@ import { motion } from "framer-motion";
import { useCallback, useEffect, useRef, useState } from "react";
import { useHotkeys } from "react-hotkeys-hook";
import { redirect } from "remix-typedjson";
import { ChevronExtraSmallDown } from "~/assets/icons/ChevronExtraSmallDown";
import { ChevronExtraSmallUp } from "~/assets/icons/ChevronExtraSmallUp";
import { MoveToTopIcon } from "~/assets/icons/MoveToTopIcon";
import { MoveUpIcon } from "~/assets/icons/MoveUpIcon";
import tileBgPath from "~/assets/images/error-banner-tile@2x.png";
@@ -35,6 +37,7 @@ import { AdminDebugTooltip } from "~/components/admin/debugTooltip";
import { PageBody } from "~/components/layout/AppLayout";
import { Badge } from "~/components/primitives/Badge";
import { Button, LinkButton } from "~/components/primitives/Buttons";
import { CopyableText } from "~/components/primitives/CopyableText";
import { DateTimeShort } from "~/components/primitives/DateTime";
import { Dialog, DialogTrigger } from "~/components/primitives/Dialog";
import { Header3 } from "~/components/primitives/Headers";
@@ -62,6 +65,7 @@ import {
import { type NodesState } from "~/components/primitives/TreeView/reducer";
import { CancelRunDialog } from "~/components/runs/v3/CancelRunDialog";
import { ReplayRunDialog } from "~/components/runs/v3/ReplayRunDialog";
import { getRunFiltersFromSearchParams } from "~/components/runs/v3/RunFilters";
import { RunIcon } from "~/components/runs/v3/RunIcon";
import {
SpanTitle,
@@ -69,6 +73,7 @@ import {
eventBorderClassName,
} from "~/components/runs/v3/SpanTitle";
import { TaskRunStatusIcon, runStatusClassNameColor } from "~/components/runs/v3/TaskRunStatus";
import { $replica } from "~/db.server";
import { useDebounce } from "~/hooks/useDebounce";
import { useEnvironment } from "~/hooks/useEnvironment";
import { useEventSource } from "~/hooks/useEventSource";
@@ -76,10 +81,16 @@ import { useInitialDimensions } from "~/hooks/useInitialDimensions";
import { useOrganization } from "~/hooks/useOrganizations";
import { useProject } from "~/hooks/useProject";
import { useReplaceSearchParams } from "~/hooks/useReplaceSearchParams";
import { useSearchParams } from "~/hooks/useSearchParam";
import { type Shortcut, useShortcutKeys } from "~/hooks/useShortcutKeys";
import { useHasAdminAccess } from "~/hooks/useUser";
import { findProjectBySlug } from "~/models/project.server";
import { findEnvironmentBySlug } from "~/models/runtimeEnvironment.server";
import { NextRunListPresenter } from "~/presenters/v3/NextRunListPresenter.server";
import { RunEnvironmentMismatchError, RunPresenter } from "~/presenters/v3/RunPresenter.server";
import { clickhouseClient } from "~/services/clickhouseInstance.server";
import { getImpersonationId } from "~/services/impersonation.server";
import { logger } from "~/services/logger.server";
import { getResizableSnapshot } from "~/services/resizablePanel.server";
import { requireUserId } from "~/services/session.server";
import { cn } from "~/utils/cn";
@@ -94,18 +105,9 @@ import {
v3RunStreamingPath,
v3RunsPath,
} from "~/utils/pathBuilder";
import type { SpanOverride } from "~/v3/eventRepository/eventRepository.types";
import { useCurrentPlan } from "../_app.orgs.$organizationSlug/route";
import { SpanView } from "../resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route";
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";
import { logger } from "~/services/logger.server";
const resizableSettings = {
parent: {
@@ -210,7 +212,10 @@ async function getRunsListFromTableState({
}
}
if (currentRunIndex === currentPageResult.runs.length - 1 && currentPageResult.pagination.next) {
if (
currentRunIndex === currentPageResult.runs.length - 1 &&
currentPageResult.pagination.next
) {
const nextPageResult = await runsListPresenter.call(project.organizationId, environment.id, {
userId,
projectId: project.id,
@@ -313,7 +318,16 @@ export default function Page() {
const tabParam = value("tab") ?? undefined;
const spanParam = value("span") ?? undefined;
const [previousRunPath, nextRunPath] = useAdjacentRunPaths({organization, project, environment, tableState, run, runsList, tabParam, useSpan: !!spanParam});
const [previousRunPath, nextRunPath] = useAdjacentRunPaths({
organization,
project,
environment,
tableState,
run,
runsList,
tabParam,
useSpan: !!spanParam,
});
return (
<>
@@ -323,13 +337,21 @@ export default function Page() {
to: v3RunsPath(organization, project, environment, filters),
text: "Runs",
}}
title={<>
<CopyableText value={run.friendlyId} variant="text-below" className="font-mono px-0 py-0 pb-[2px]"/>
{tableState && (<div className="flex">
<PreviousRunButton to={previousRunPath} />
<NextRunButton to={nextRunPath} />
</div>)}
</>}
title={
<div className="flex items-center gap-x-0">
<CopyableText
value={run.friendlyId}
variant="text-below"
className="-ml-[0.4375rem] h-6 px-1.5 font-mono text-xs hover:text-text-bright"
/>
{tableState && (
<div className="flex">
<PreviousRunButton to={previousRunPath} />
<NextRunButton to={nextRunPath} />
</div>
)}
</div>
}
/>
{environment.type === "DEVELOPMENT" && <DevDisconnectedBanner isConnected={isConnected} />}
<PageAccessories>
@@ -407,16 +429,18 @@ export default function Page() {
maximumLiveReloadingSetting={maximumLiveReloadingSetting}
/>
) : (
<NoLogsView
run={run}
/>
<NoLogsView run={run} />
)}
</PageBody>
</>
);
}
function TraceView({ run, trace, maximumLiveReloadingSetting }: Pick<LoaderData, "run" | "trace" | "maximumLiveReloadingSetting">) {
function TraceView({
run,
trace,
maximumLiveReloadingSetting,
}: Pick<LoaderData, "run" | "trace" | "maximumLiveReloadingSetting">) {
const organization = useOrganization();
const project = useProject();
const environment = useEnvironment();
@@ -875,7 +899,7 @@ function TasksTreeView({
</ResizablePanelGroup>
<div className="flex items-center justify-between gap-2 border-t border-grid-dimmed px-4">
<div className="grow @container">
<div className="hidden items-center gap-4 @[42rem]:flex">
<div className="hidden items-center gap-4 @[48rem]:flex">
<KeyboardShortcuts
expandAllBelowDepth={expandAllBelowDepth}
collapseAllBelowDepth={collapseAllBelowDepth}
@@ -883,7 +907,7 @@ function TasksTreeView({
setShowDurations={setShowDurations}
/>
</div>
<div className="@[42rem]:hidden">
<div className="@[48rem]:hidden">
<Popover>
<PopoverArrowTrigger>Shortcuts</PopoverArrowTrigger>
<PopoverContent
@@ -1131,7 +1155,9 @@ function TimelineView({
"-ml-[0.5px] h-[0.5625rem] w-px rounded-none",
eventBackgroundClassName(node.data)
)}
layoutId={disableSpansAnimations ? undefined : `${node.id}-${event.name}`}
layoutId={
disableSpansAnimations ? undefined : `${node.id}-${event.name}`
}
animate={disableSpansAnimations ? false : undefined}
/>
)}
@@ -1150,7 +1176,9 @@ function TimelineView({
"-ml-[0.1562rem] size-[0.3125rem] rounded-full border bg-background-bright",
eventBorderClassName(node.data)
)}
layoutId={disableSpansAnimations ? undefined : `${node.id}-${event.name}`}
layoutId={
disableSpansAnimations ? undefined : `${node.id}-${event.name}`
}
animate={disableSpansAnimations ? false : undefined}
/>
)}
@@ -1444,7 +1472,12 @@ function SpanWithDuration({
fadeLeft,
disableAnimations,
...props
}: Timeline.SpanProps & { node: TraceEvent; showDuration: boolean; fadeLeft: boolean; disableAnimations?: boolean }) {
}: Timeline.SpanProps & {
node: TraceEvent;
showDuration: boolean;
fadeLeft: boolean;
disableAnimations?: boolean;
}) {
return (
<Timeline.Span {...props}>
<motion.div
@@ -1583,13 +1616,15 @@ function KeyboardShortcuts({
}
function AdjacentRunsShortcuts() {
return (<div className="flex items-center gap-0.5">
return (
<div className="flex items-center gap-0.5">
<ShortcutKey shortcut={{ key: "[" }} variant="medium" className="ml-0 mr-0 px-1" />
<ShortcutKey shortcut={{ key: "]" }} variant="medium" className="ml-0 mr-0 px-1" />
<Paragraph variant="extra-small" className="ml-1.5 whitespace-nowrap">
Adjacent runs
Next/previous run
</Paragraph>
</div>);
</div>
);
}
function ArrowKeyShortcuts() {
@@ -1679,13 +1714,13 @@ function useAdjacentRunPaths({
run,
runsList,
tabParam,
useSpan
useSpan,
}: {
organization: { slug: string };
project: { slug: string };
environment: { slug: string };
tableState: string;
run: { friendlyId: string, spanId: string };
run: { friendlyId: string; spanId: string };
runsList: RunsListNavigation | null;
tabParam?: string;
useSpan?: boolean;
@@ -1695,7 +1730,7 @@ function useAdjacentRunPaths({
}
const currentIndex = runsList.runs.findIndex((r) => r.friendlyId === run.friendlyId);
if (currentIndex === -1) {
return [null, null];
}
@@ -1751,18 +1786,15 @@ function useAdjacentRunPaths({
return [previousRunPath, nextRunPath];
}
function PreviousRunButton({ to }: { to: string | null }) {
return (
<div className={cn("peer/prev order-1", !to && "pointer-events-none")}>
<LinkButton
to={to ? to : '#'}
to={to ? to : "#"}
variant={"minimal/small"}
LeadingIcon={ChevronUpIcon}
className={cn(
"flex items-center rounded-r-none border-r-0 pl-2 pr-[0.5625rem]",
!to && "cursor-not-allowed opacity-50"
)}
LeadingIcon={ChevronExtraSmallUp}
leadingIconClassName="size-3 group-hover/button:text-text-bright transition-colors"
className={cn("flex size-6 max-w-6 items-center", !to && "cursor-not-allowed opacity-50")}
onClick={(e) => !to && e.preventDefault()}
shortcut={{ key: "[" }}
tooltip="Previous Run"
@@ -1777,13 +1809,11 @@ function NextRunButton({ to }: { to: string | null }) {
return (
<div className={cn("peer/next order-3", !to && "pointer-events-none")}>
<LinkButton
to={to ? to : '#'}
to={to ? to : "#"}
variant={"minimal/small"}
TrailingIcon={ChevronDownIcon}
className={cn(
"flex items-center rounded-l-none border-l-0 pl-[0.5625rem] pr-2",
!to && "cursor-not-allowed opacity-50"
)}
LeadingIcon={ChevronExtraSmallDown}
leadingIconClassName="size-3 group-hover/button:text-text-bright transition-colors"
className={cn("flex size-6 max-w-6 items-center", !to && "cursor-not-allowed opacity-50")}
onClick={(e) => !to && e.preventDefault()}
shortcut={{ key: "]" }}
tooltip="Next Run"
@@ -1793,4 +1823,3 @@ function NextRunButton({ to }: { to: string | null }) {
</div>
);
}