Skip to content

Commit a0dcd78

Browse files
authored
fix(webapp): webhooks dashboard UI pass (#4490)
A UI pass over the webhooks dashboard, on top of #4344. No behaviour changes beyond the fixes below. ## Deliveries list - Whole row is clickable. The external delivery ID, created, processed and error cells had no link, and the target cell only linked when the delivery had a run or session, so most of each row was dead. - Dimmed "None" and "Unknown" cells now brighten with the row on hover. - The new-deliveries button sits inline, left of the pager, instead of on its own row beneath it. - The table scrolls. It was passing `stickyHeader`, which switches the table container to `overflow-visible` and stops it being the scroll container; every other list in the app leaves it off. The header stays sticky either way. - 60 deliveries per page, up from 25. Test tag uses the shared `Badge`, the webhook icon matches the Tasks page, and the Status and More filters menus drop their redundant search fields. ## Delivery detail - Dropped the duplicate status badge from the title bar; the sidebar already has a Status row. - The "nothing was captured" tab messages are centred and a size larger. - Copyable sidebar values ellipsise instead of overflowing their column, so an unbreakable hash or opaque id no longer runs past the edge. `CopyableText` gains an opt-in `truncate` prop that reserves a gutter for the copy button. - The delivery timeline's thick bar is rounded at the top. The run timeline gets that corner from the `start-cap-thick` event above its thick line, but a delivery only has two timestamps, so the line itself starts the bar and had a square top on every succeeded and failed delivery. `RunTimelineLine` gains an opt-in `roundedTop`, so other callers are unaffected. ## Navigation Webhooks was a section containing a single item. It now sits as a top-level item below Sessions, and the page is titled "Webhook deliveries". Registering the page in the favourites registry also fixes its favourite name, which was saving as "Page: Deliveries". ## Also One fix outside the UI: the delivery seed script minted `id` and `friendlyId` as two independent ids, but the detail lookup derives the row id from the friendlyId, so every seeded delivery's page reported that it could not be found.
1 parent 4b75fc4 commit a0dcd78

12 files changed

Lines changed: 151 additions & 118 deletions

File tree

apps/webapp/app/components/navigation/SideMenu.tsx

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -886,23 +886,6 @@ export function SideMenu({
886886
});
887887
}
888888

889-
if (user.admin || user.isImpersonating || featureFlags.hasWebhooksAccess) {
890-
staticSections.push({
891-
id: "webhooks",
892-
title: "Webhooks",
893-
items: [
894-
{
895-
id: "webhook-deliveries",
896-
name: "Deliveries",
897-
icon: WebhookIcon,
898-
activeIconColor: "text-webhooks",
899-
to: v3WebhooksPath(organization, project, environment),
900-
dataAction: "webhook-deliveries",
901-
},
902-
],
903-
});
904-
}
905-
906889
staticSections.push({
907890
id: "deployments",
908891
title: "Deployments",
@@ -1204,6 +1187,19 @@ export function SideMenu({
12041187
isCollapsed={isCollapsed}
12051188
yieldActiveToFavorite
12061189
/>
1190+
{(user.admin || user.isImpersonating || featureFlags.hasWebhooksAccess) && (
1191+
<SideMenuItem
1192+
name="Webhooks"
1193+
icon={WebhookIcon}
1194+
activeIconColor="text-webhooks"
1195+
inactiveIconColor="text-text-dimmed"
1196+
to={v3WebhooksPath(organization, project, environment)}
1197+
data-action="webhooks"
1198+
badge={<NewBadge />}
1199+
isCollapsed={isCollapsed}
1200+
yieldActiveToFavorite
1201+
/>
1202+
)}
12071203
</div>
12081204

12091205
{orderedSectionIds.map((sectionId) => {

apps/webapp/app/components/navigation/favoritePages.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { TasksIcon } from "~/assets/icons/TasksIcon";
4040
import { UsageIcon } from "~/assets/icons/UsageIcon";
4141
import { UserGroupIcon } from "~/assets/icons/UserGroupIcon";
4242
import { WaitpointTokenIcon } from "~/assets/icons/WaitpointTokenIcon";
43+
import { WebhookIcon } from "~/assets/icons/WebhookIcon";
4344
import { VercelLogo } from "~/components/integrations/VercelLogo";
4445
import { useOptionalUser } from "~/hooks/useUser";
4546
import { type FavoritePage } from "~/services/dashboardPreferences.server";
@@ -71,6 +72,7 @@ const FAVORITE_PAGE_ICONS: Record<
7172
"task-agent": { icon: CubeSparkleIcon, activeColor: "text-agents" },
7273
runs: { icon: RunsIcon, activeColor: "text-runs" },
7374
sessions: { icon: AIChatIcon, activeColor: "text-sessions" },
75+
webhooks: { icon: WebhookIcon, activeColor: "text-webhooks" },
7476
prompts: { icon: AIPenIcon, activeColor: "text-aiPrompts" },
7577
models: { icon: Box3DIcon, activeColor: "text-models" },
7678
logs: { icon: LogsIcon, activeColor: "text-logs" },
@@ -213,6 +215,7 @@ const ENV_PAGE_META: Record<string, PageMeta> = {
213215
"": { icon: "tasks", name: "Tasks", singular: "Task" },
214216
runs: { icon: "runs", name: "Runs", singular: "Run" },
215217
sessions: { icon: "sessions", name: "Sessions", singular: "Session" },
218+
webhooks: { icon: "webhooks", name: "Webhook deliveries" },
216219
prompts: { icon: "prompts", name: "Prompts", singular: "Prompt" },
217220
models: { icon: "models", name: "Models", singular: "Model" },
218221
logs: { icon: "logs", name: "Logs" },

apps/webapp/app/components/navigation/sideMenuTypes.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ export const SideMenuSectionIdSchema = z.enum([
99
"deployments",
1010
"project-settings",
1111
"tasks",
12-
"webhooks",
1312
]);
1413

1514
// Inferred type from the schema

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

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export function CopyableText({
1212
asChild,
1313
variant,
1414
hideTooltip,
15+
truncate,
1516
}: {
1617
value: string;
1718
copyValue?: string;
@@ -24,6 +25,12 @@ export function CopyableText({
2425
* fire Radix's global "one tooltip open at a time" close and dismiss the parent.
2526
*/
2627
hideTooltip?: boolean;
28+
/**
29+
* Ellipsise the value rather than letting it overflow its column. For unbreakable strings
30+
* (hashes, opaque ids) that offer no wrap opportunity. The copy button moves into a reserved
31+
* right gutter so it stays visible instead of sitting outside the column.
32+
*/
33+
truncate?: boolean;
2734
}) {
2835
const [isHovered, setIsHovered] = useState(false);
2936
const { copy, copied } = useCopy(copyValue ?? value);
@@ -51,15 +58,26 @@ export function CopyableText({
5158

5259
return (
5360
<span
54-
className={cn("group relative inline-flex h-6 items-center", className)}
61+
className={cn(
62+
"group relative inline-flex h-6 items-center",
63+
truncate && "max-w-full pr-7",
64+
className
65+
)}
5566
onMouseLeave={() => setIsHovered(false)}
5667
>
57-
<span onMouseEnter={() => setIsHovered(true)}>{value}</span>
68+
<span
69+
className={cn(truncate && "min-w-0 truncate")}
70+
onMouseEnter={() => setIsHovered(true)}
71+
>
72+
{value}
73+
</span>
5874
<span
5975
onClick={copy}
6076
onMouseDown={(e) => e.stopPropagation()}
6177
className={cn(
62-
"absolute -right-6 top-0 z-10 size-6 font-sans",
78+
"absolute top-0 z-10 size-6 font-sans",
79+
// Truncated values reserve a right gutter, so the button sits inside it
80+
truncate ? "right-0" : "-right-6",
6381
isHovered ? "flex" : "hidden"
6482
)}
6583
>

apps/webapp/app/components/run/RunTimeline.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -483,18 +483,25 @@ export type RunTimelineLineProps = {
483483
state?: TimelineEventState;
484484
variant?: TimelineLineVariant;
485485
style?: TimelineStyle;
486+
/**
487+
* Round the top of a thick ("normal") line. Needed when the line itself starts the thick bar,
488+
* as in the delivery timeline, where nothing above it supplies a `start-cap-thick`. The run
489+
* timeline always precedes its thick line with that cap, so it leaves this off.
490+
*/
491+
roundedTop?: boolean;
486492
};
487493

488494
export function RunTimelineLine({
489495
title,
490496
state,
491497
variant = "normal",
492498
style = "normal",
499+
roundedTop = false,
493500
}: RunTimelineLineProps) {
494501
return (
495502
<div className="grid h-6 grid-cols-[1.125rem_1fr] gap-1 text-xs">
496503
<div className="flex items-stretch justify-center">
497-
<LineMarker state={state} variant={variant} style={style} />
504+
<LineMarker state={state} variant={variant} style={style} roundedTop={roundedTop} />
498505
</div>
499506
<div className="flex items-center justify-between gap-3">
500507
<span className="text-text-dimmed">{title}</span>
@@ -507,10 +514,12 @@ function LineMarker({
507514
state,
508515
variant,
509516
style,
517+
roundedTop = false,
510518
}: {
511519
state?: TimelineEventState;
512520
variant: TimelineLineVariant;
513521
style?: TimelineStyle;
522+
roundedTop?: boolean;
514523
}) {
515524
let containerClass = "bg-text-dimmed";
516525
switch (state) {
@@ -532,7 +541,7 @@ function LineMarker({
532541
switch (variant) {
533542
case "normal":
534543
return (
535-
<div className={cn("relative w-1.75", containerClass)}>
544+
<div className={cn("relative w-1.75", roundedTop && "rounded-t-xs", containerClass)}>
536545
{state === "inprogress" && (
537546
<div
538547
className="absolute inset-0 h-full w-full animate-tile-scroll opacity-30"

apps/webapp/app/components/webhookDeliveries/v1/DeliveriesTable.tsx

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useLocation, useNavigation } from "@remix-run/react";
33
import { AIChatIcon } from "~/assets/icons/AIChatIcon";
44
import { RunsIcon } from "~/assets/icons/RunsIcon";
55
import { WebhookIcon } from "~/assets/icons/WebhookIcon";
6+
import { Badge } from "~/components/primitives/Badge";
67
import { DateTime } from "~/components/primitives/DateTime";
78
import { MiddleTruncate } from "~/components/primitives/MiddleTruncate";
89
import { Paragraph } from "~/components/primitives/Paragraph";
@@ -121,28 +122,26 @@ export function DeliveriesTable({
121122
<TableCell to={webhookPath}>
122123
{delivery.webhook ? (
123124
<span className="flex items-center gap-x-1">
124-
<WebhookIcon className="size-4 text-webhooks" />
125+
<WebhookIcon className="size-4.5 min-w-4.5 text-webhooks" />
125126
{delivery.webhook.slug}
126127
</span>
127128
) : (
128-
<span className="text-text-dimmed">Unknown</span>
129+
<span className="text-text-dimmed group-hover/table-row:text-text-bright">
130+
Unknown
131+
</span>
129132
)}
130133
</TableCell>
131134
)}
132135
<TableCell to={deliveryPath}>
133136
<span className="flex items-center gap-1.5">
134137
<span className="font-mono text-xs">{delivery.friendlyId}</span>
135-
{delivery.isTest ? (
136-
<span className="rounded-sm bg-charcoal-700 px-1 py-0.5 text-xxs font-semibold uppercase tracking-wide text-text-dimmed">
137-
Test
138-
</span>
139-
) : null}
138+
{delivery.isTest ? <Badge variant="extra-small">Test</Badge> : null}
140139
</span>
141140
</TableCell>
142141
<TableCell to={deliveryPath}>
143142
<DeliveryStatusBadge status={delivery.status} />
144143
</TableCell>
145-
<TableCell>
144+
<TableCell to={deliveryPath}>
146145
{delivery.externalDeliveryId ? (
147146
<div className="w-[24ch]">
148147
<MiddleTruncate
@@ -151,10 +150,13 @@ export function DeliveriesTable({
151150
/>
152151
</div>
153152
) : (
154-
<span className="text-text-dimmed">None</span>
153+
<span className="text-text-dimmed group-hover/table-row:text-text-bright">
154+
None
155+
</span>
155156
)}
156157
</TableCell>
157-
<TableCell to={sessionPath ?? runPath}>
158+
{/* Falls back to the delivery so the whole row stays clickable when there is no target */}
159+
<TableCell to={sessionPath ?? runPath ?? deliveryPath}>
158160
{delivery.session ? (
159161
<span className="flex items-center gap-x-1">
160162
<AIChatIcon className="size-4 text-sessions" />
@@ -166,20 +168,24 @@ export function DeliveriesTable({
166168
<span className="font-mono text-xs">{delivery.run.friendlyId}</span>
167169
</span>
168170
) : (
169-
<span className="text-text-dimmed">None</span>
171+
<span className="text-text-dimmed group-hover/table-row:text-text-bright">
172+
None
173+
</span>
170174
)}
171175
</TableCell>
172-
<TableCell>
176+
<TableCell to={deliveryPath}>
173177
<DateTime date={delivery.createdAt} />
174178
</TableCell>
175-
<TableCell>
179+
<TableCell to={deliveryPath}>
176180
{delivery.processedAt ? (
177181
<DateTime date={delivery.processedAt} />
178182
) : (
179-
<span className="text-text-dimmed">None</span>
183+
<span className="text-text-dimmed group-hover/table-row:text-text-bright">
184+
None
185+
</span>
180186
)}
181187
</TableCell>
182-
<TableCell>
188+
<TableCell to={deliveryPath}>
183189
{delivery.status === "FAILED" && delivery.errorMessage ? (
184190
<SimpleTooltip
185191
content={delivery.errorMessage}
@@ -190,7 +196,9 @@ export function DeliveriesTable({
190196
}
191197
/>
192198
) : (
193-
<span className="text-text-dimmed">None</span>
199+
<span className="text-text-dimmed group-hover/table-row:text-text-bright">
200+
None
201+
</span>
194202
)}
195203
</TableCell>
196204
<DeliveryActionsCell

apps/webapp/app/components/webhookDeliveries/v1/DeliveryTimeline.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ export function DeliveryTimeline({ delivery, runPath, sessionPath }: DeliveryTim
3030
key={item.id}
3131
state={item.state}
3232
variant={item.variant}
33+
// "Received" is a thin start-cap, so a thick line here begins the bar and has to
34+
// round its own top. Thin ("light") lines, as in the FILTERED case, need nothing.
35+
roundedTop={item.variant === "normal"}
3336
title={
3437
<span className="flex items-center gap-1.5">
3538
{item.to ? (

apps/webapp/app/components/webhookDeliveries/v1/WebhookDeliveryFilters.tsx

Lines changed: 5 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -155,20 +155,13 @@ function Menu(props: MenuProps) {
155155
}
156156
}
157157

158-
function MainMenu({ searchValue, trigger, clearSearchValue, setFilterType }: MenuProps) {
159-
const filtered = useMemo(() => {
160-
return filterTypes.filter((item) =>
161-
item.title.toLowerCase().includes(searchValue.toLowerCase())
162-
);
163-
}, [searchValue]);
164-
158+
function MainMenu({ trigger, clearSearchValue, setFilterType }: MenuProps) {
165159
return (
166160
<SelectProvider virtualFocus={true}>
167161
{trigger}
168162
<SelectPopover>
169-
<ComboBox placeholder={"Filter by..."} shortcut={moreFiltersShortcut} value={searchValue} />
170163
<SelectList>
171-
{filtered.map((type, index) => (
164+
{filterTypes.map((type, index) => (
172165
<SelectButtonItem
173166
key={type.name}
174167
onClick={() => {
@@ -190,12 +183,10 @@ function MainMenu({ searchValue, trigger, clearSearchValue, setFilterType }: Men
190183
function StatusDropdown({
191184
trigger,
192185
clearSearchValue,
193-
searchValue,
194186
onClose,
195187
}: {
196188
trigger: ReactNode;
197189
clearSearchValue: () => void;
198-
searchValue: string;
199190
onClose?: () => void;
200191
}) {
201192
const { values, replace } = useSearchParams();
@@ -205,12 +196,6 @@ function StatusDropdown({
205196
replace({ statuses: values, cursor: undefined, direction: undefined });
206197
};
207198

208-
const filtered = useMemo(() => {
209-
return deliveryStatuses.filter((item) =>
210-
item.title.toLowerCase().includes(searchValue.toLowerCase())
211-
);
212-
}, [searchValue]);
213-
214199
return (
215200
<SelectProvider value={values("statuses")} setValue={handleChange} virtualFocus={true}>
216201
{trigger}
@@ -225,9 +210,8 @@ function StatusDropdown({
225210
return true;
226211
}}
227212
>
228-
<ComboBox placeholder={"Filter by status..."} value={searchValue} />
229213
<SelectList>
230-
{filtered.map((item, index) => (
214+
{deliveryStatuses.map((item, index) => (
231215
<SelectItem
232216
key={item.value}
233217
value={item.value}
@@ -264,7 +248,7 @@ function PermanentStatusFilter() {
264248

265249
return (
266250
<FilterMenuProvider>
267-
{(search, setSearch) => (
251+
{(_search, setSearch) => (
268252
<StatusDropdown
269253
trigger={
270254
<Ariakit.TooltipProvider timeout={200}>
@@ -309,7 +293,6 @@ function PermanentStatusFilter() {
309293
</Ariakit.Tooltip>
310294
</Ariakit.TooltipProvider>
311295
}
312-
searchValue={search}
313296
clearSearchValue={() => setSearch("")}
314297
/>
315298
)}
@@ -536,7 +519,7 @@ function PermanentTestFilter() {
536519
/>
537520
) : (
538521
<div className="flex h-6 items-center gap-1.5 rounded border border-charcoal-600 bg-secondary pl-1 pr-2 text-xs text-text-bright transition group-hover:border-charcoal-550 group-hover:bg-charcoal-600">
539-
<BeakerIcon className="size-4 text-text-dimmed" />
522+
<BeakerIcon className="size-4 text-text-bright" />
540523
<span>Test</span>
541524
</div>
542525
)}

apps/webapp/app/presenters/v3/WebhookDeliveriesListPresenter.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {
99
type WebhookDeliveryListItem,
1010
} from "./WebhookDetailPresenter.server";
1111

12-
const DELIVERIES_PAGE_SIZE = 25;
12+
const DELIVERIES_PAGE_SIZE = 60;
1313
type Direction = "forward" | "backward";
1414

1515
export type WebhookDeliveriesListResult = {

0 commit comments

Comments
 (0)