feat(webapp): fix agent overview page scroll bug + layout fixes on task and agent pages (#4454)
## Summary The task, scheduled task and agent pages now name their runs table with its own title bar, and the controls that page the table sit beside it rather than in the bar at the top of the page. The top bar keeps just the date filter. Two agent page layout bugs are fixed along the way: 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. ## Fix The charts moved because the runs table had no horizontal scroller of its own. `stickyHeader` swaps the table's `overflow-x-auto` for `overflow-visible`, so the overflow escaped up to the page scroll box, and setting only `overflow-y-auto` on that box leaves the computed `overflow-x` at `visible`, which CSS then promotes to `auto`. The chart grid is a sibling inside that box, so it scrolled too. The table now keeps its own scroller (the same rule the queues list already documents) and the page box clips x so this cannot recur. The short panel was a second `PageContainer` wrapping the agent routes. `PageContainer` is `grid-rows-[auto_1fr]`, so a lone child lands in the `auto` row and its `h-full` resolves against content height instead of the viewport. This also reverts the global tooltip `max-w-[230px]` introduced in [#4131](https://github.com/triggerdotdev/trigger.dev/pull/4131), so longer tooltips are no longer squeezed into a narrow column. ### Agent overview page showing table now scrolling <img width="3452" height="1648" alt="CleanShot 2026-08-01 at 12 04 38@2x" src="https://github.com/user-attachments/assets/ef1ac55d-8ffb-4278-983b-031ed21c1f55" />
This commit is contained in:
@@ -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"
|
||||
}
|
||||
>
|
||||
@@ -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",
|
||||
className
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
/**
|
||||
|
||||
@@ -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: {
|
||||
@@ -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>
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
)}
|
||||
@@ -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"} />}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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",
|
||||
variantClasses[variant],
|
||||
className
|
||||
)}
|
||||
|
||||
+42
-39
@@ -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 +
|
||||
@@ -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
|
||||
@@ -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.
|
||||
return tab === "sessions" ? (
|
||||
<Suspense fallback={<TableLoading />}>
|
||||
<TypedAwait resolve={sessionList} errorElement={<TableLoading />}>
|
||||
@@ -367,7 +370,7 @@ function AgentContentArea({
|
||||
sessions={list.sessions}
|
||||
filters={list.filters}
|
||||
hasFilters={list.hasFilters}
|
||||
stickyHeader
|
||||
showTopBorder={false}
|
||||
/>
|
||||
) : (
|
||||
<TableLoading />
|
||||
@@ -386,7 +389,7 @@ function AgentContentArea({
|
||||
filters={list.filters}
|
||||
runs={list.runs}
|
||||
variant="dimmed"
|
||||
stickyHeader
|
||||
showTopBorder={false}
|
||||
/>
|
||||
) : (
|
||||
<TableLoading />
|
||||
|
||||
+2
-6
@@ -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 />;
|
||||
}
|
||||
|
||||
+50
-42
@@ -28,6 +28,7 @@ import {
|
||||
DialogTrigger,
|
||||
} from "~/components/primitives/Dialog";
|
||||
import { Header2 } from "~/components/primitives/Headers";
|
||||
import { TitleBar } from "~/components/primitives/TitleBar";
|
||||
import { InfoPanel } from "~/components/primitives/InfoPanel";
|
||||
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { PaginationControls } from "~/components/primitives/Pagination";
|
||||
@@ -292,31 +293,15 @@ export default function Page() {
|
||||
<ResizablePanelGroup orientation="horizontal" className="max-h-full">
|
||||
<ResizablePanel id="scheduled-task-main" min="300px">
|
||||
<div className="grid h-full grid-rows-[auto_1fr_auto] overflow-hidden">
|
||||
{/* Top bar — title on the left; actions + TimeFilter + pagination on the right.
|
||||
h-10 matches the right-hand sidebar header height. */}
|
||||
<div className="flex min-h-10 items-center gap-2 border-b border-grid-dimmed bg-background-bright py-2 pl-3 pr-2">
|
||||
<Header2>Runs</Header2>
|
||||
<div className="flex min-h-10 items-center gap-2 border-b border-grid-dimmed bg-background-bright px-2 py-2">
|
||||
<TimeFilter defaultPeriod="7d" labelName="Runs" />
|
||||
<div className="ml-auto flex flex-wrap items-center justify-end gap-1.5">
|
||||
<CreateScheduleButton
|
||||
isAtLimit={isAtLimit}
|
||||
limits={limits}
|
||||
canUpgrade={canUpgrade}
|
||||
canPurchaseSchedules={scheduleList?.canPurchaseSchedules ?? false}
|
||||
extraSchedules={scheduleList?.extraSchedules ?? 0}
|
||||
maxScheduleQuota={scheduleList?.maxScheduleQuota ?? 0}
|
||||
planScheduleLimit={scheduleList?.planScheduleLimit ?? 0}
|
||||
schedulePricing={scheduleList?.schedulePricing ?? null}
|
||||
onCreate={openCreateSchedule}
|
||||
disabled={isCreatingSchedule}
|
||||
/>
|
||||
{newRunsCount > 0 ? (
|
||||
<NewRunsButton count={newRunsCount} onClick={() => showNewRunsRef.current()} />
|
||||
) : null}
|
||||
<TimeFilter defaultPeriod="7d" labelName="Runs" />
|
||||
<LinkButton
|
||||
variant="secondary/small"
|
||||
to={v3RunsPath(organization, project, environment, filters)}
|
||||
LeadingIcon={RunsIcon}
|
||||
// -7px halves the icon's default 6px of space on each side.
|
||||
leadingIconClassName="-mx-[7px]"
|
||||
>
|
||||
View all runs
|
||||
</LinkButton>
|
||||
@@ -335,11 +320,18 @@ export default function Page() {
|
||||
>
|
||||
Bulk replay…
|
||||
</LinkButton>
|
||||
<Suspense fallback={null}>
|
||||
<TypedAwait resolve={runList} errorElement={null}>
|
||||
{(list) => (list ? <ListPagination list={list} /> : null)}
|
||||
</TypedAwait>
|
||||
</Suspense>
|
||||
<CreateScheduleButton
|
||||
isAtLimit={isAtLimit}
|
||||
limits={limits}
|
||||
canUpgrade={canUpgrade}
|
||||
canPurchaseSchedules={scheduleList?.canPurchaseSchedules ?? false}
|
||||
extraSchedules={scheduleList?.extraSchedules ?? 0}
|
||||
maxScheduleQuota={scheduleList?.maxScheduleQuota ?? 0}
|
||||
planScheduleLimit={scheduleList?.planScheduleLimit ?? 0}
|
||||
schedulePricing={scheduleList?.schedulePricing ?? null}
|
||||
onCreate={openCreateSchedule}
|
||||
disabled={isCreatingSchedule}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -363,23 +355,39 @@ export default function Page() {
|
||||
|
||||
{/* Runs table */}
|
||||
<ResizablePanel id="scheduled-task-content" min="160px">
|
||||
<div className="h-full overflow-hidden">
|
||||
<Suspense fallback={<TableLoading />}>
|
||||
<TypedAwait resolve={runList} errorElement={<TableLoading />}>
|
||||
{(list) =>
|
||||
list ? (
|
||||
<TaskRunsList
|
||||
list={list}
|
||||
taskSlug={task.slug}
|
||||
onNewRunsCountChange={setNewRunsCount}
|
||||
showNewRunsRef={showNewRunsRef}
|
||||
/>
|
||||
) : (
|
||||
<TableLoading />
|
||||
)
|
||||
}
|
||||
</TypedAwait>
|
||||
</Suspense>
|
||||
<div className="grid h-full grid-rows-[auto_1fr] overflow-hidden">
|
||||
{/* -mt-px absorbs the spare pixel below the handle's rule, centring the title. */}
|
||||
<TitleBar title="Runs" className="-mt-px">
|
||||
{newRunsCount > 0 ? (
|
||||
<NewRunsButton
|
||||
count={newRunsCount}
|
||||
onClick={() => showNewRunsRef.current()}
|
||||
/>
|
||||
) : null}
|
||||
<Suspense fallback={null}>
|
||||
<TypedAwait resolve={runList} errorElement={null}>
|
||||
{(list) => (list ? <ListPagination list={list} /> : null)}
|
||||
</TypedAwait>
|
||||
</Suspense>
|
||||
</TitleBar>
|
||||
<div className="min-h-0 overflow-hidden">
|
||||
<Suspense fallback={<TableLoading />}>
|
||||
<TypedAwait resolve={runList} errorElement={<TableLoading />}>
|
||||
{(list) =>
|
||||
list ? (
|
||||
<TaskRunsList
|
||||
list={list}
|
||||
taskSlug={task.slug}
|
||||
onNewRunsCountChange={setNewRunsCount}
|
||||
showNewRunsRef={showNewRunsRef}
|
||||
/>
|
||||
) : (
|
||||
<TableLoading />
|
||||
)
|
||||
}
|
||||
</TypedAwait>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
|
||||
+36
-32
@@ -20,6 +20,7 @@ import { statusColor } from "~/components/primitives/charts/statusColors";
|
||||
import { CopyableText } from "~/components/primitives/CopyableText";
|
||||
import { DateTime } from "~/components/primitives/DateTime";
|
||||
import { Header2 } from "~/components/primitives/Headers";
|
||||
import { TitleBar } from "~/components/primitives/TitleBar";
|
||||
import { NavBar, PageTitle } from "~/components/primitives/PageHeader";
|
||||
import { Paragraph } from "~/components/primitives/Paragraph";
|
||||
import * as Property from "~/components/primitives/PropertyTable";
|
||||
@@ -234,21 +235,8 @@ export default function Page() {
|
||||
<ResizablePanelGroup orientation="horizontal" className="max-h-full">
|
||||
<ResizablePanel id="task-main" min="300px">
|
||||
<div className="grid h-full grid-rows-[auto_1fr] overflow-hidden">
|
||||
{/* Top bar — title on the left; TimeFilter + pagination on the right.
|
||||
h-10 matches the right-hand sidebar header height. */}
|
||||
<div className="flex h-10 items-center border-b border-grid-dimmed bg-background-bright pl-3 pr-2">
|
||||
<Header2>Runs</Header2>
|
||||
<div className="ml-auto flex items-center gap-1.5">
|
||||
{newRunsCount > 0 ? (
|
||||
<NewRunsButton count={newRunsCount} onClick={() => showNewRunsRef.current()} />
|
||||
) : null}
|
||||
<TimeFilter defaultPeriod="7d" labelName="Runs" />
|
||||
<Suspense fallback={null}>
|
||||
<TypedAwait resolve={runList} errorElement={null}>
|
||||
{(list) => (list ? <ListPagination list={list} /> : null)}
|
||||
</TypedAwait>
|
||||
</Suspense>
|
||||
</div>
|
||||
<div className="flex h-10 items-center border-b border-grid-dimmed bg-background-bright px-2">
|
||||
<TimeFilter defaultPeriod="7d" labelName="Runs" />
|
||||
</div>
|
||||
|
||||
<ResizablePanelGroup orientation="vertical" className="max-h-full">
|
||||
@@ -263,23 +251,39 @@ export default function Page() {
|
||||
|
||||
{/* Runs table */}
|
||||
<ResizablePanel id="task-content" min="160px">
|
||||
<div className="h-full overflow-hidden">
|
||||
<Suspense fallback={<TableLoading />}>
|
||||
<TypedAwait resolve={runList} errorElement={<TableLoading />}>
|
||||
{(list) =>
|
||||
list ? (
|
||||
<TaskRunsList
|
||||
list={list}
|
||||
taskSlug={task.slug}
|
||||
onNewRunsCountChange={setNewRunsCount}
|
||||
showNewRunsRef={showNewRunsRef}
|
||||
/>
|
||||
) : (
|
||||
<TableLoading />
|
||||
)
|
||||
}
|
||||
</TypedAwait>
|
||||
</Suspense>
|
||||
<div className="grid h-full grid-rows-[auto_1fr] overflow-hidden">
|
||||
{/* -mt-px absorbs the spare pixel below the handle's rule, centring the title. */}
|
||||
<TitleBar title="Runs" className="-mt-px">
|
||||
{newRunsCount > 0 ? (
|
||||
<NewRunsButton
|
||||
count={newRunsCount}
|
||||
onClick={() => showNewRunsRef.current()}
|
||||
/>
|
||||
) : null}
|
||||
<Suspense fallback={null}>
|
||||
<TypedAwait resolve={runList} errorElement={null}>
|
||||
{(list) => (list ? <ListPagination list={list} /> : null)}
|
||||
</TypedAwait>
|
||||
</Suspense>
|
||||
</TitleBar>
|
||||
<div className="min-h-0 overflow-hidden">
|
||||
<Suspense fallback={<TableLoading />}>
|
||||
<TypedAwait resolve={runList} errorElement={<TableLoading />}>
|
||||
{(list) =>
|
||||
list ? (
|
||||
<TaskRunsList
|
||||
list={list}
|
||||
taskSlug={task.slug}
|
||||
onNewRunsCountChange={setNewRunsCount}
|
||||
showNewRunsRef={showNewRunsRef}
|
||||
/>
|
||||
) : (
|
||||
<TableLoading />
|
||||
)
|
||||
}
|
||||
</TypedAwait>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
|
||||
Reference in New Issue
Block a user