Skip to content

Commit 6c9f7be

Browse files
committed
feat(webapp): runs title bar and layout fixes on task and agent pages
The task, scheduled task and agent pages now name their runs table with its own title bar, with the page controls beside the table they act on rather than in the top bar. Also fixes two agent page layout bugs: scrolling a wide runs table sideways dragged the charts off screen with it, and the details panel stopped short of the bottom of the window. Reverts the global tooltip max-width so longer tooltips are no longer squeezed into a column.
1 parent 0445b8e commit 6c9f7be

10 files changed

Lines changed: 236 additions & 130 deletions

File tree

apps/webapp/app/components/layout/MetricsLayout.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,10 +175,12 @@ function MetricsLayoutMain({ children, scroll }: { children: ReactNode; scroll:
175175
return (
176176
<div className="flex h-full min-h-0 flex-col">
177177
{filters}
178+
{/* overflow-x-clip: without it `overflow-y-auto` promotes x to auto and wide content drags
179+
the charts sideways. Wide children must scroll in their own container. */}
178180
<div
179181
className={
180182
scroll === "page"
181-
? "flex min-h-0 flex-1 flex-col gap-2.5 overflow-y-auto py-2.5 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control"
183+
? "flex min-h-0 flex-1 flex-col gap-2.5 overflow-y-auto overflow-x-clip py-2.5 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-surface-control"
182184
: "flex min-h-0 flex-1 flex-col overflow-hidden"
183185
}
184186
>
@@ -286,7 +288,7 @@ function MetricsLayoutFilters({
286288
return (
287289
<div
288290
className={cn(
289-
"flex h-10 shrink-0 items-center justify-between gap-2 border-b border-grid-dimmed pl-2.5 pr-3",
291+
"flex h-10 shrink-0 items-center justify-between gap-2 border-b border-grid-dimmed px-2",
290292
className
291293
)}
292294
>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { cn } from "~/utils/cn";
22

3-
const headerVariants = {
3+
export const headerVariants = {
44
header1: {
55
text: "font-sans text-2xl leading-5 md:leading-6 lg:leading-7 font-semibold tracking-tight",
66
spacing: "mb-2",

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ type TableCellBasicProps = {
181181
type TableHeaderCellProps = TableCellBasicProps & {
182182
hiddenLabel?: boolean;
183183
tooltip?: ReactNode;
184-
/** Extra class merged onto the tooltip content — e.g. widen it past the default max-width. */
184+
/** Extra class merged onto the tooltip content. */
185185
tooltipContentClassName?: string;
186186
disableTooltipHoverableContent?: boolean;
187187
/**

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

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,19 @@ import { motion } from "framer-motion";
33
import { type ReactNode, useRef } from "react";
44
import { type ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys";
55
import { cn } from "~/utils/cn";
6+
import { headerVariants } from "./Headers";
67
import { ShortcutKey } from "./ShortcutKey";
78

8-
export type Variants = "underline" | "pipe-divider" | "segmented";
9+
/** `"title"` names the table below it: header2 text, filter-bar height, underline on the border. */
10+
export type Variants = "underline" | "pipe-divider" | "segmented" | "title";
11+
12+
/** Shared with `TitleBar` so the tabbed and tab-less bars match. */
13+
export const TITLE_BAR_CHROME = "flex h-10 shrink-0 gap-x-6 border-b border-grid-bright";
14+
15+
const titleTabLabel = cn(headerVariants.header2.text, "transition duration-200");
16+
const titleTabIndicator = "h-0.5 w-full bg-indigo-500";
17+
const titleTabIndicatorIdle =
18+
"h-0.5 w-full bg-surface-control-active opacity-0 transition duration-200 group-hover:opacity-100";
919

1020
export type TabsProps = {
1121
tabs: {
@@ -58,6 +68,10 @@ export function TabContainer({
5868
);
5969
}
6070

71+
if (variant === "title") {
72+
return <div className={cn(TITLE_BAR_CHROME, "items-stretch", className)}>{children}</div>;
73+
}
74+
6175
if (variant === "underline") {
6276
return (
6377
<div className={cn(`flex gap-x-6 border-b border-grid-bright`, className)}>{children}</div>
@@ -117,6 +131,39 @@ export function TabLink({
117131
);
118132
}
119133

134+
if (variant === "title") {
135+
return (
136+
<NavLink to={to} className="group flex h-full flex-col focus-custom" end={end}>
137+
{({ isActive, isPending }) => {
138+
const active = isActive || isPending;
139+
return (
140+
<>
141+
<div className="flex flex-1 items-center">
142+
<span
143+
className={cn(
144+
titleTabLabel,
145+
active ? "text-text-bright" : "text-text-dimmed group-hover:text-text-bright"
146+
)}
147+
>
148+
{children}
149+
</span>
150+
</div>
151+
{active ? (
152+
<motion.div
153+
layoutId={layoutId}
154+
transition={{ type: "spring", stiffness: 500, damping: 30 }}
155+
className={titleTabIndicator}
156+
/>
157+
) : (
158+
<div className={titleTabIndicatorIdle} />
159+
)}
160+
</>
161+
);
162+
}}
163+
</NavLink>
164+
);
165+
}
166+
120167
if (variant === "pipe-divider") {
121168
return (
122169
<NavLink
@@ -177,11 +224,13 @@ export function TabButton({
177224
isActive,
178225
layoutId,
179226
shortcut,
227+
variant = "underline",
180228
...props
181229
}: {
182230
isActive: boolean;
183231
shortcut?: ShortcutDefinition;
184232
layoutId: string;
233+
variant?: Variants;
185234
} & React.ButtonHTMLAttributes<HTMLButtonElement>) {
186235
const ref = useRef<HTMLButtonElement>(null);
187236

@@ -197,10 +246,13 @@ export function TabButton({
197246
});
198247
}
199248

249+
const title = variant === "title";
250+
200251
return (
201252
<button
202253
className={cn(
203-
"group flex flex-col items-center pt-1 focus-custom",
254+
"group flex flex-col items-center focus-custom",
255+
title ? "h-full" : "pt-1",
204256
props.className,
205257
props.disabled && "pointer-events-none opacity-50"
206258
)}
@@ -209,8 +261,18 @@ export function TabButton({
209261
{...props}
210262
>
211263
<>
212-
<div className="flex items-center gap-1">
213-
<span className={"text-sm transition duration-200 text-text-bright"}>
264+
<div className={cn("flex items-center gap-1", title && "flex-1")}>
265+
<span
266+
className={cn(
267+
"transition duration-200",
268+
title
269+
? cn(
270+
headerVariants.header2.text,
271+
isActive ? "text-text-bright" : "text-text-dimmed group-hover:text-text-bright"
272+
)
273+
: "text-sm text-text-bright"
274+
)}
275+
>
214276
{props.children}
215277
</span>
216278
{shortcut && <ShortcutKey className={cn("")} shortcut={shortcut} variant={"small"} />}
@@ -219,10 +281,15 @@ export function TabButton({
219281
<motion.div
220282
layoutId={layoutId}
221283
transition={{ type: "spring", stiffness: 500, damping: 30 }}
222-
className="mt-1 h-0.5 w-full bg-indigo-500"
284+
className={cn("h-0.5 w-full bg-indigo-500", !title && "mt-1")}
223285
/>
224286
) : (
225-
<div className="mt-1 h-0.5 w-full bg-surface-control-active opacity-0 transition duration-200 group-hover:opacity-100" />
287+
<div
288+
className={cn(
289+
"h-0.5 w-full bg-surface-control-active opacity-0 transition duration-200 group-hover:opacity-100",
290+
!title && "mt-1"
291+
)}
292+
/>
226293
)}
227294
</>
228295
</button>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { type ReactNode } from "react";
2+
import { cn } from "~/utils/cn";
3+
import { Header2 } from "./Headers";
4+
import { TITLE_BAR_CHROME } from "./Tabs";
5+
6+
/**
7+
* Names the table below it. Bottom rule only — it doubles as the table's top edge, so render the
8+
* table with `showTopBorder={false}`. Use `TabContainer variant="title"` for the tabbed form.
9+
*/
10+
export function TitleBar({
11+
title,
12+
children,
13+
className,
14+
}: {
15+
title: ReactNode;
16+
/** Right-aligned controls. */
17+
children?: ReactNode;
18+
className?: string;
19+
}) {
20+
return (
21+
<div className={cn(TITLE_BAR_CHROME, "items-center justify-between pl-2.5 pr-1.5", className)}>
22+
<Header2>{title}</Header2>
23+
{children ? <div className="flex items-center gap-1.5">{children}</div> : null}
24+
</div>
25+
);
26+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ const TooltipContent = React.forwardRef<
4242
ref={ref}
4343
sideOffset={sideOffset}
4444
className={cn(
45-
"z-50 max-w-[230px] overflow-hidden animate-in data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1 focus-visible:outline-hidden",
45+
"z-50 overflow-hidden animate-in data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1 focus-visible:outline-hidden",
4646
variantClasses[variant],
4747
className
4848
)}

apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.agents.$agentParam/route.tsx

Lines changed: 42 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -234,28 +234,10 @@ export default function Page() {
234234
</PageAccessories>
235235
</NavBar>
236236
<MetricsLayout.Root>
237-
{/* Filters — the pinned bar under the NavBar: the TimeFilter and pagination that used to
238-
be fused with the tabs now live here, above the charts (Queues list pattern). Left and
239-
right clusters are child divs; the slot's baked justify-between spreads them. */}
240237
<MetricsLayout.Filters>
241238
<div className="flex items-center gap-2">
242239
<TimeFilter defaultPeriod="7d" labelName={tabLabel} />
243240
</div>
244-
<div className="flex items-center gap-2">
245-
{tab === "sessions" ? (
246-
<Suspense fallback={null}>
247-
<TypedAwait resolve={sessionList} errorElement={null}>
248-
{(list) => (list ? <ListPagination list={list} /> : null)}
249-
</TypedAwait>
250-
</Suspense>
251-
) : (
252-
<Suspense fallback={null}>
253-
<TypedAwait resolve={runList} errorElement={null}>
254-
{(list) => (list ? <ListPagination list={list} /> : null)}
255-
</TypedAwait>
256-
</Suspense>
257-
)}
258-
</div>
259241
</MetricsLayout.Filters>
260242

261243
{/* Activity / LLM spend / Token charts as a fixed-height chart row (three-up), synced +
@@ -315,23 +297,45 @@ export default function Page() {
315297

316298
{/* Tabs alone on their row (Queue detail pattern), then the table below them. */}
317299
<MetricsLayout.Content>
318-
<TabContainer className="px-3">
319-
<TabButton
320-
isActive={tab === "sessions"}
321-
layoutId="agent-page-tabs"
322-
onClick={() => setTab("sessions")}
323-
>
324-
Sessions
325-
</TabButton>
326-
<TabButton
327-
isActive={tab === "runs"}
328-
layoutId="agent-page-tabs"
329-
onClick={() => setTab("runs")}
330-
>
331-
Runs
332-
</TabButton>
333-
</TabContainer>
334-
<AgentContentArea tab={tab} sessionList={sessionList} runList={runList} />
300+
{/* Single child so Content's gap-2.5 can't separate the bar from the table. */}
301+
<div className="flex flex-col">
302+
<TabContainer variant="title" className="justify-between border-y px-2">
303+
<div className="flex items-stretch gap-x-6">
304+
<TabButton
305+
isActive={tab === "sessions"}
306+
layoutId="agent-page-tabs"
307+
variant="title"
308+
onClick={() => setTab("sessions")}
309+
>
310+
Sessions
311+
</TabButton>
312+
<TabButton
313+
isActive={tab === "runs"}
314+
layoutId="agent-page-tabs"
315+
variant="title"
316+
onClick={() => setTab("runs")}
317+
>
318+
Runs
319+
</TabButton>
320+
</div>
321+
<div className="flex items-center gap-1.5">
322+
{tab === "sessions" ? (
323+
<Suspense fallback={null}>
324+
<TypedAwait resolve={sessionList} errorElement={null}>
325+
{(list) => (list ? <ListPagination list={list} /> : null)}
326+
</TypedAwait>
327+
</Suspense>
328+
) : (
329+
<Suspense fallback={null}>
330+
<TypedAwait resolve={runList} errorElement={null}>
331+
{(list) => (list ? <ListPagination list={list} /> : null)}
332+
</TypedAwait>
333+
</Suspense>
334+
)}
335+
</div>
336+
</TabContainer>
337+
<AgentContentArea tab={tab} sessionList={sessionList} runList={runList} />
338+
</div>
335339
</MetricsLayout.Content>
336340

337341
<MetricsLayout.Sidebar
@@ -356,8 +360,7 @@ function AgentContentArea({
356360
sessionList,
357361
runList,
358362
}: { tab: AgentTab } & Pick<LoaderData, "sessionList" | "runList">) {
359-
// The table flows in the page-level scroll (MetricsLayout.Root scroll="page"); a sticky header
360-
// keeps the column labels pinned as the whole column scrolls.
363+
// No `stickyHeader` — it drops the table's own overflow-x-auto and the charts scroll with it.
361364
return tab === "sessions" ? (
362365
<Suspense fallback={<TableLoading />}>
363366
<TypedAwait resolve={sessionList} errorElement={<TableLoading />}>
@@ -367,7 +370,7 @@ function AgentContentArea({
367370
sessions={list.sessions}
368371
filters={list.filters}
369372
hasFilters={list.hasFilters}
370-
stickyHeader
373+
showTopBorder={false}
371374
/>
372375
) : (
373376
<TableLoading />
@@ -386,7 +389,7 @@ function AgentContentArea({
386389
filters={list.filters}
387390
runs={list.runs}
388391
variant="dimmed"
389-
stickyHeader
392+
showTopBorder={false}
390393
/>
391394
) : (
392395
<TableLoading />
Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
11
import { Outlet } from "@remix-run/react";
2-
import { PageContainer } from "~/components/layout/AppLayout";
32

3+
// No PageContainer — the child pages render their own; nesting two collapses the inner one's height.
44
export default function Page() {
5-
return (
6-
<PageContainer>
7-
<Outlet />
8-
</PageContainer>
9-
);
5+
return <Outlet />;
106
}

0 commit comments

Comments
 (0)