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
6 changes: 4 additions & 2 deletions apps/webapp/app/components/layout/MetricsLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,12 @@ function MetricsLayoutMain({ children, scroll }: { children: ReactNode; scroll:
return (
<div className="flex h-full min-h-0 flex-col">
{filters}
{/* overflow-x-clip: without it `overflow-y-auto` promotes x to auto and wide content drags
the charts sideways. Wide children must scroll in their own container. */}
<div
className={
scroll === "page"
? "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"
? "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"
: "flex min-h-0 flex-1 flex-col overflow-hidden"
}
>
Expand Down Expand Up @@ -286,7 +288,7 @@ function MetricsLayoutFilters({
return (
<div
className={cn(
"flex h-10 shrink-0 items-center justify-between gap-2 border-b border-grid-dimmed pl-2.5 pr-3",
"flex h-10 shrink-0 items-center justify-between gap-2 border-b border-grid-dimmed px-2",
Comment thread
samejr marked this conversation as resolved.
className
)}
>
Expand Down
2 changes: 1 addition & 1 deletion apps/webapp/app/components/primitives/Headers.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { cn } from "~/utils/cn";

const headerVariants = {
export const headerVariants = {
header1: {
text: "font-sans text-2xl leading-5 md:leading-6 lg:leading-7 font-semibold tracking-tight",
spacing: "mb-2",
Expand Down
2 changes: 1 addition & 1 deletion apps/webapp/app/components/primitives/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ type TableCellBasicProps = {
type TableHeaderCellProps = TableCellBasicProps & {
hiddenLabel?: boolean;
tooltip?: ReactNode;
/** Extra class merged onto the tooltip content — e.g. widen it past the default max-width. */
/** Extra class merged onto the tooltip content. */
tooltipContentClassName?: string;
disableTooltipHoverableContent?: boolean;
/**
Expand Down
79 changes: 73 additions & 6 deletions apps/webapp/app/components/primitives/Tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,19 @@ import { motion } from "framer-motion";
import { type ReactNode, useRef } from "react";
import { type ShortcutDefinition, useShortcutKeys } from "~/hooks/useShortcutKeys";
import { cn } from "~/utils/cn";
import { headerVariants } from "./Headers";
import { ShortcutKey } from "./ShortcutKey";

export type Variants = "underline" | "pipe-divider" | "segmented";
/** `"title"` names the table below it: header2 text, filter-bar height, underline on the border. */
export type Variants = "underline" | "pipe-divider" | "segmented" | "title";

/** Shared with `TitleBar` so the tabbed and tab-less bars match. */
export const TITLE_BAR_CHROME = "flex h-10 shrink-0 gap-x-6 border-b border-grid-bright";

const titleTabLabel = cn(headerVariants.header2.text, "transition duration-200");
const titleTabIndicator = "h-0.5 w-full bg-indigo-500";
const titleTabIndicatorIdle =
"h-0.5 w-full bg-surface-control-active opacity-0 transition duration-200 group-hover:opacity-100";

export type TabsProps = {
tabs: {
Expand Down Expand Up @@ -58,6 +68,10 @@ export function TabContainer({
);
}

if (variant === "title") {
return <div className={cn(TITLE_BAR_CHROME, "items-stretch", className)}>{children}</div>;
}

if (variant === "underline") {
return (
<div className={cn(`flex gap-x-6 border-b border-grid-bright`, className)}>{children}</div>
Expand Down Expand Up @@ -117,6 +131,39 @@ export function TabLink({
);
}

if (variant === "title") {
return (
<NavLink to={to} className="group flex h-full flex-col focus-custom" end={end}>
{({ isActive, isPending }) => {
const active = isActive || isPending;
return (
<>
<div className="flex flex-1 items-center">
<span
className={cn(
titleTabLabel,
active ? "text-text-bright" : "text-text-dimmed group-hover:text-text-bright"
)}
>
{children}
</span>
</div>
{active ? (
<motion.div
layoutId={layoutId}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
className={titleTabIndicator}
/>
) : (
<div className={titleTabIndicatorIdle} />
)}
</>
);
}}
</NavLink>
);
}

if (variant === "pipe-divider") {
return (
<NavLink
Expand Down Expand Up @@ -177,11 +224,13 @@ export function TabButton({
isActive,
layoutId,
shortcut,
variant = "underline",
...props
}: {
isActive: boolean;
shortcut?: ShortcutDefinition;
layoutId: string;
variant?: Variants;
} & React.ButtonHTMLAttributes<HTMLButtonElement>) {
const ref = useRef<HTMLButtonElement>(null);

Expand All @@ -197,10 +246,13 @@ export function TabButton({
});
}

const title = variant === "title";

return (
<button
className={cn(
"group flex flex-col items-center pt-1 focus-custom",
"group flex flex-col items-center focus-custom",
title ? "h-full" : "pt-1",
props.className,
props.disabled && "pointer-events-none opacity-50"
)}
Expand All @@ -209,8 +261,18 @@ export function TabButton({
{...props}
>
<>
<div className="flex items-center gap-1">
<span className={"text-sm transition duration-200 text-text-bright"}>
<div className={cn("flex items-center gap-1", title && "flex-1")}>
<span
className={cn(
"transition duration-200",
title
? cn(
headerVariants.header2.text,
isActive ? "text-text-bright" : "text-text-dimmed group-hover:text-text-bright"
)
: "text-sm text-text-bright"
)}
>
{props.children}
</span>
{shortcut && <ShortcutKey className={cn("")} shortcut={shortcut} variant={"small"} />}
Expand All @@ -219,10 +281,15 @@ export function TabButton({
<motion.div
layoutId={layoutId}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
className="mt-1 h-0.5 w-full bg-indigo-500"
className={cn("h-0.5 w-full bg-indigo-500", !title && "mt-1")}
/>
) : (
<div className="mt-1 h-0.5 w-full bg-surface-control-active opacity-0 transition duration-200 group-hover:opacity-100" />
<div
className={cn(
"h-0.5 w-full bg-surface-control-active opacity-0 transition duration-200 group-hover:opacity-100",
!title && "mt-1"
)}
/>
)}
</>
</button>
Expand Down
26 changes: 26 additions & 0 deletions apps/webapp/app/components/primitives/TitleBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { type ReactNode } from "react";
import { cn } from "~/utils/cn";
import { Header2 } from "./Headers";
import { TITLE_BAR_CHROME } from "./Tabs";

/**
* Names the table below it. Bottom rule only — it doubles as the table's top edge, so render the
* table with `showTopBorder={false}`. Use `TabContainer variant="title"` for the tabbed form.
*/
export function TitleBar({
title,
children,
className,
}: {
title: ReactNode;
/** Right-aligned controls. */
children?: ReactNode;
className?: string;
}) {
return (
<div className={cn(TITLE_BAR_CHROME, "items-center justify-between pl-2.5 pr-1.5", className)}>
<Header2>{title}</Header2>
{children ? <div className="flex items-center gap-1.5">{children}</div> : null}
</div>
);
}
2 changes: 1 addition & 1 deletion apps/webapp/app/components/primitives/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ const TooltipContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"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",
"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",
Comment thread
samejr marked this conversation as resolved.
variantClasses[variant],
className
)}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -234,28 +234,10 @@ export default function Page() {
</PageAccessories>
</NavBar>
<MetricsLayout.Root>
{/* Filters — the pinned bar under the NavBar: the TimeFilter and pagination that used to
be fused with the tabs now live here, above the charts (Queues list pattern). Left and
right clusters are child divs; the slot's baked justify-between spreads them. */}
<MetricsLayout.Filters>
<div className="flex items-center gap-2">
<TimeFilter defaultPeriod="7d" labelName={tabLabel} />
</div>
<div className="flex items-center gap-2">
{tab === "sessions" ? (
<Suspense fallback={null}>
<TypedAwait resolve={sessionList} errorElement={null}>
{(list) => (list ? <ListPagination list={list} /> : null)}
</TypedAwait>
</Suspense>
) : (
<Suspense fallback={null}>
<TypedAwait resolve={runList} errorElement={null}>
{(list) => (list ? <ListPagination list={list} /> : null)}
</TypedAwait>
</Suspense>
)}
</div>
</MetricsLayout.Filters>

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

{/* Tabs alone on their row (Queue detail pattern), then the table below them. */}
<MetricsLayout.Content>
<TabContainer className="px-3">
<TabButton
isActive={tab === "sessions"}
layoutId="agent-page-tabs"
onClick={() => setTab("sessions")}
>
Sessions
</TabButton>
<TabButton
isActive={tab === "runs"}
layoutId="agent-page-tabs"
onClick={() => setTab("runs")}
>
Runs
</TabButton>
</TabContainer>
<AgentContentArea tab={tab} sessionList={sessionList} runList={runList} />
{/* Single child so Content's gap-2.5 can't separate the bar from the table. */}
<div className="flex flex-col">
<TabContainer variant="title" className="justify-between border-y px-2">
<div className="flex items-stretch gap-x-6">
<TabButton
isActive={tab === "sessions"}
layoutId="agent-page-tabs"
variant="title"
onClick={() => setTab("sessions")}
>
Sessions
</TabButton>
<TabButton
isActive={tab === "runs"}
layoutId="agent-page-tabs"
variant="title"
onClick={() => setTab("runs")}
>
Runs
</TabButton>
</div>
<div className="flex items-center gap-1.5">
{tab === "sessions" ? (
<Suspense fallback={null}>
<TypedAwait resolve={sessionList} errorElement={null}>
{(list) => (list ? <ListPagination list={list} /> : null)}
</TypedAwait>
</Suspense>
) : (
<Suspense fallback={null}>
<TypedAwait resolve={runList} errorElement={null}>
{(list) => (list ? <ListPagination list={list} /> : null)}
</TypedAwait>
</Suspense>
)}
</div>
</TabContainer>
<AgentContentArea tab={tab} sessionList={sessionList} runList={runList} />
</div>
</MetricsLayout.Content>

<MetricsLayout.Sidebar
Expand All @@ -356,8 +360,7 @@ function AgentContentArea({
sessionList,
runList,
}: { tab: AgentTab } & Pick<LoaderData, "sessionList" | "runList">) {
// The table flows in the page-level scroll (MetricsLayout.Root scroll="page"); a sticky header
// keeps the column labels pinned as the whole column scrolls.
// No `stickyHeader` — it drops the table's own overflow-x-auto and the charts scroll with it.
Comment thread
samejr marked this conversation as resolved.
return tab === "sessions" ? (
<Suspense fallback={<TableLoading />}>
<TypedAwait resolve={sessionList} errorElement={<TableLoading />}>
Expand All @@ -367,7 +370,7 @@ function AgentContentArea({
sessions={list.sessions}
filters={list.filters}
hasFilters={list.hasFilters}
stickyHeader
showTopBorder={false}
/>
) : (
<TableLoading />
Expand All @@ -386,7 +389,7 @@ function AgentContentArea({
filters={list.filters}
runs={list.runs}
variant="dimmed"
stickyHeader
showTopBorder={false}
/>
) : (
<TableLoading />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
import { Outlet } from "@remix-run/react";
import { PageContainer } from "~/components/layout/AppLayout";

// No PageContainer — the child pages render their own; nesting two collapses the inner one's height.
export default function Page() {
return (
<PageContainer>
<Outlet />
</PageContainer>
);
return <Outlet />;
Comment thread
samejr marked this conversation as resolved.
}
Loading
Loading