Skip to content

Commit 0351a67

Browse files
committed
feat(webapp): dismissible MollifierBanner on mollified run-detail page
Surfaces an info-style banner above the run-detail page body when the loader resolved the run from the mollifier buffer (`isMollified === true`, set by the read-fallback in route.tsx when the PG row hasn't been materialised yet). Explains the queued state to the operator and points at `batchTrigger` as the long-term shape for high-fan-out workloads. Dismissal is localStorage-only (`mollifier_banner_dismissed`). Plan Task 21 leaves this an explicit choice between localStorage and a new per-org settings endpoint; localStorage is the simpler path and avoids adding a writeable settings endpoint just for a dismissal flag. Reads happen in an effect (not useState's initialiser) so SSR-renders the banner visible by default, then hides it on hydration if dismissed — no flash-of-banner. Styled to match the existing Callout 'info' variant (blue-400 family) without using the Callout primitive directly because Callout's API is oriented around link-style CTAs, not inline-dismissible banners.
1 parent 0fa8ec5 commit 0351a67

2 files changed

Lines changed: 89 additions & 1 deletion

File tree

  • apps/webapp/app
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { InformationCircleIcon, XMarkIcon } from "@heroicons/react/20/solid";
2+
import { useEffect, useState } from "react";
3+
import { cn } from "~/utils/cn";
4+
import { Paragraph } from "../primitives/Paragraph";
5+
6+
// Surfaced on a run-detail page when the run was accepted into the
7+
// mollifier burst buffer and hasn't been materialised into Postgres yet
8+
// (loader sets `isMollified === true`). The drainer will replay the
9+
// snapshot through `engine.trigger` shortly; this banner explains the
10+
// queued state and points the operator at `batchTrigger` as the
11+
// long-term shape for high-fan-out workloads.
12+
//
13+
// Dismissal is localStorage-only for now — per-org server persistence
14+
// can come in a follow-up. Plan Task 21 leaves this an explicit
15+
// choice; the localStorage path avoids adding a write endpoint on the
16+
// hot-fix critical path.
17+
const DISMISSED_KEY = "mollifier_banner_dismissed";
18+
19+
export function MollifierBanner({ className }: { className?: string }) {
20+
// Start un-dismissed on the server (no localStorage) and reconcile in
21+
// useEffect so SSR + first client render agree. If we read
22+
// localStorage in useState's initialiser the client banner can flash
23+
// visible-then-hidden when hydration runs.
24+
const [dismissed, setDismissed] = useState(false);
25+
26+
useEffect(() => {
27+
try {
28+
setDismissed(window.localStorage.getItem(DISMISSED_KEY) === "true");
29+
} catch {
30+
// Some browsers (private mode, embedded webviews) throw on
31+
// localStorage access. Treat as un-dismissed; the user can dismiss
32+
// again next visit without server-side state going stale.
33+
}
34+
}, []);
35+
36+
if (dismissed) return null;
37+
38+
return (
39+
<div
40+
className={cn(
41+
"flex w-full items-start justify-between gap-2.5 rounded-md border border-blue-400/20 bg-blue-400/10 py-2 pl-2 pr-3 shadow-md backdrop-blur-sm",
42+
className
43+
)}
44+
role="status"
45+
>
46+
<div className="flex w-full items-start gap-x-2">
47+
<InformationCircleIcon className="mt-0.5 h-5 w-5 shrink-0 text-blue-400" />
48+
<div className="flex flex-col gap-y-1">
49+
<Paragraph variant="small/bright" className="text-blue-200">
50+
This run was accepted into the burst buffer.
51+
</Paragraph>
52+
<Paragraph variant="small" className="text-blue-200/80">
53+
Your environment briefly exceeded the trigger-rate ceiling, so the
54+
run is queued in Redis and will materialise here shortly. For
55+
high-fan-out workloads consider{" "}
56+
<a
57+
href="https://trigger.dev/docs/triggering#batchtrigger"
58+
target="_blank"
59+
rel="noreferrer"
60+
className="underline hover:text-blue-100"
61+
>
62+
batchTrigger
63+
</a>{" "}
64+
instead — it&apos;s designed for the fan-out shape and bypasses the
65+
burst gate.
66+
</Paragraph>
67+
</div>
68+
</div>
69+
<button
70+
type="button"
71+
aria-label="Dismiss"
72+
onClick={() => {
73+
try {
74+
window.localStorage.setItem(DISMISSED_KEY, "true");
75+
} catch {
76+
// Same fallback as the read above — silent dismiss.
77+
}
78+
setDismissed(true);
79+
}}
80+
className="rounded p-0.5 text-blue-300/70 transition hover:bg-blue-400/20 hover:text-blue-200"
81+
>
82+
<XMarkIcon className="h-4 w-4" />
83+
</button>
84+
</div>
85+
);
86+
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import {
6767
useTree,
6868
} from "~/components/primitives/TreeView/TreeView";
6969
import { type NodesState } from "~/components/primitives/TreeView/reducer";
70+
import { MollifierBanner } from "~/components/runs/MollifierBanner";
7071
import { CancelRunDialog } from "~/components/runs/v3/CancelRunDialog";
7172
import { ReplayRunDialog } from "~/components/runs/v3/ReplayRunDialog";
7273
import { getRunFiltersFromSearchParams } from "~/components/runs/v3/RunFilters";
@@ -381,7 +382,7 @@ async function tryMollifiedRunFallback(args: {
381382
type LoaderData = SerializeFrom<typeof loader>;
382383

383384
export default function Page() {
384-
const { run, trace, maximumLiveReloadingSetting, runsList, resizable } =
385+
const { run, trace, maximumLiveReloadingSetting, runsList, resizable, isMollified } =
385386
useLoaderData<typeof loader>();
386387
const organization = useOrganization();
387388
const project = useProject();
@@ -501,6 +502,7 @@ export default function Page() {
501502
</PageAccessories>
502503
</NavBar>
503504
<PageBody scrollable={false}>
505+
{isMollified ? <MollifierBanner className="mx-3 mt-3" /> : null}
504506
{trace ? (
505507
<TraceView
506508
run={run}

0 commit comments

Comments
 (0)