Skip to content

Commit 794cc9b

Browse files
committed
Merge branch 'pjones/multi-gadget-ux-iter' into 'main'
Iterate on multi-app ui See merge request cloudflare/developers/gadgets!255
2 parents 878ada3 + 3dde140 commit 794cc9b

16 files changed

Lines changed: 1733 additions & 1292 deletions

packages/workshop-backend/src/overseer.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7250,6 +7250,7 @@ class OverseerClientInterface extends RpcTarget implements Overseer {
72507250
// second step after the record is first persisted (see addGatekeeper). It can be absent if
72517251
// that describe() failed, or for records predating the field, so fall back to a placeholder.
72527252
resourceTitle: gk.resourceTitle || "(title unavailable)",
7253+
vendorId: gk.creationSpec?.type === "gatekeeper" ? gk.creationSpec.vendorId : undefined,
72537254
actionKind,
72547255
alreadyEnabled:
72557256
this.impl.storage.autoApproveTags.get(`${gk.id}:${actionKind.tag}`) !== undefined,

packages/workshop-frontend/src/Activity.tsx

Lines changed: 531 additions & 456 deletions
Large diffs are not rendered by default.
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { useMemo, useState } from 'react'
2+
import { Popover } from '@cloudflare/kumo'
3+
import { ArrowRight, Pulse } from '@phosphor-icons/react'
4+
import type { RpcStub } from 'capnweb'
5+
import type { ActionLogEntry, Overseer } from '@gadgets/workshop-shared/api'
6+
import { CountBadge } from './components/CountBadge'
7+
import { ResolveButton } from './components/ResolveButton'
8+
import { formatRelativeTime, type ActivityView } from './Activity'
9+
import { useResolveAction } from './useResolveAction'
10+
11+
interface ActivityNotificationsProps {
12+
overseer: RpcStub<Overseer>
13+
pendingActions: ActionLogEntry[]
14+
onViewActivity: (view: ActivityView) => void
15+
}
16+
17+
const PREVIEW_LIMIT = 3
18+
19+
export default function ActivityNotifications({
20+
overseer,
21+
pendingActions,
22+
onViewActivity,
23+
}: ActivityNotificationsProps) {
24+
const [open, setOpen] = useState(false)
25+
const [processing, setProcessing] = useState<Set<number>>(new Set())
26+
const resolveAction = useResolveAction(overseer, setProcessing)
27+
28+
const pending = useMemo(() => pendingActions.toSorted((a, b) =>
29+
new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime() || a.id - b.id),
30+
[pendingActions])
31+
32+
const openFullView = (view: ActivityView) => {
33+
setOpen(false)
34+
onViewActivity(view)
35+
}
36+
37+
return (
38+
<Popover open={open} onOpenChange={setOpen}>
39+
<Popover.Trigger
40+
render={
41+
<button
42+
type="button"
43+
aria-label={pending.length > 0
44+
? `Activity — ${pending.length} ${pending.length === 1 ? 'request needs' : 'requests need'} review`
45+
: 'Activity'}
46+
className={`relative flex h-8 w-8 shrink-0 cursor-pointer items-center justify-center rounded-md transition-colors duration-150 hover:bg-kumo-tint focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-kumo-ring ${
47+
pending.length > 0 ? 'text-kumo-strong' : 'text-kumo-subtle hover:text-kumo-default'
48+
}`}
49+
>
50+
<Pulse size={16} weight={pending.length > 0 ? 'bold' : 'regular'} />
51+
<CountBadge count={pending.length} tone="solid" className="absolute -right-0.5 -top-0.5" />
52+
</button>
53+
}
54+
/>
55+
{/* Kumo always renders base-ui's arrow as the popup's first child; hide it so this sits flush
56+
like the header's profile menu, which has no arrow. */}
57+
<Popover.Content
58+
align="end"
59+
sideOffset={8}
60+
positionMethod="fixed"
61+
className="themed-floating-shadow !z-[1100] !w-[min(340px,calc(100vw-24px))] !min-w-0 overflow-hidden rounded-lg border border-kumo-line !outline-none bg-kumo-base !p-0 [&>:first-child]:hidden"
62+
>
63+
<div className="flex items-center justify-between gap-2 px-3.5 pb-1 pt-2.5">
64+
<Popover.Title className="text-[11px] font-medium uppercase tracking-[0.06em] text-kumo-inactive">
65+
Needs review
66+
</Popover.Title>
67+
<CountBadge count={pending.length} />
68+
</div>
69+
70+
{pending.length === 0 ? (
71+
<p className="m-0 px-3.5 pb-3 pt-1 text-[13px] leading-[18px] tracking-[-0.25px] text-kumo-subtle">
72+
Nothing is waiting on you.
73+
</p>
74+
) : (
75+
<div className="max-h-[min(58vh,420px)] overflow-y-auto pb-1">
76+
{pending.slice(0, PREVIEW_LIMIT).map((action, index) => {
77+
const isProcessing = processing.has(action.id)
78+
return (
79+
<div
80+
key={action.id}
81+
className={`px-3.5 py-2.5 ${index === 0 ? '' : 'border-t border-kumo-line'}`}
82+
>
83+
<div className="flex items-start gap-2">
84+
<button
85+
type="button"
86+
onClick={() => openFullView('review')}
87+
className="min-w-[7rem] flex-1 cursor-pointer text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-kumo-ring"
88+
>
89+
<span className="block truncate text-[13px] font-medium leading-[18px] tracking-[-0.25px] text-kumo-default">
90+
{action.description.title}
91+
</span>
92+
<span className="mt-0.5 block truncate text-[11.5px] leading-4 tracking-[-0.1px] text-kumo-inactive">
93+
{action.resourceTitle}
94+
<span className="px-1">·</span>
95+
{formatRelativeTime(action.createdAt)}
96+
</span>
97+
<span className="mt-1.5 block line-clamp-2 text-[12.5px] leading-[18px] tracking-[-0.2px] text-kumo-subtle">
98+
{action.description.description}
99+
</span>
100+
</button>
101+
<div className="ml-auto flex flex-shrink-0 items-center gap-0.5">
102+
<ResolveButton
103+
tone="deny"
104+
disabled={isProcessing}
105+
onClick={() => void resolveAction(action.id, 'deny')}
106+
/>
107+
<ResolveButton
108+
tone="approve"
109+
disabled={isProcessing}
110+
onClick={() => void resolveAction(action.id, 'approve')}
111+
/>
112+
</div>
113+
</div>
114+
</div>
115+
)
116+
})}
117+
</div>
118+
)}
119+
120+
<div className="border-t border-kumo-line p-1">
121+
<button
122+
type="button"
123+
onClick={() => openFullView(pending.length > 0 ? 'review' : 'history')}
124+
className="flex w-full cursor-pointer items-center justify-between rounded-md px-2.5 py-1.5 text-left text-[13px] leading-[18px] tracking-[-0.25px] text-kumo-default transition-colors hover:bg-kumo-tint focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-kumo-ring"
125+
>
126+
<span>
127+
{pending.length > PREVIEW_LIMIT
128+
? `View all ${pending.length} requests`
129+
: 'View all activity'}
130+
</span>
131+
<ArrowRight size={13} className="text-kumo-inactive" />
132+
</button>
133+
</div>
134+
</Popover.Content>
135+
</Popover>
136+
)
137+
}

0 commit comments

Comments
 (0)