JITSU-88 feat(console): usage-quota banner#1414
Conversation
Renders the `quotaStatus` ee-api now returns in billing/settings as an in-app banner in the workspace layout: warning80 / warning90 — dismissible (per workspace + level + period) warning95 — persistent warning blocked — persistent error (delivery paused, resumes on reset) The console owns none of the thresholds or the block decision — it only renders what `level` says. quotaStatus is a sibling of subscriptionStatus in the billing/settings response, so parseBillingSettings attaches it onto the parsed BillingSettings (schema gains an optional QuotaStatus). The banner self-gates (null when billing disabled / no quotaStatus / level none) and sits between the workspace header and content, hidden in fullscreen data views. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| return null; | ||
| } | ||
|
|
||
| const key = dismissKey(workspace.id, quota); |
There was a problem hiding this comment.
Potential bug: dismissKey(workspace.id, quota) is evaluated unconditionally before any guard on workspace.id. In some console codepaths useWorkspace() can briefly be undefined/partial during initial load; if that happens this render will throw before returning null, which can regress page load. Would you mind guarding this with optional access (or skipping dismiss logic until workspace?.id is present)?
There was a problem hiding this comment.
Thanks — I looked into this and it isn't a reachable crash path here:
useWorkspace()(lib/context.tsx) throws if there's noWorkspaceContextprovider; it never returnsundefinedor a partial object. So when it returns,workspaceis a fullWorkspaceContextwith a non-nullableid.QuotaBanneronly mounts insideWorkspacePageLayout, which already dereferencesworkspace.deleted(andworkspace.slug) earlier in its own render — ifworkspacewere ever undefined, the layout would throw before the banner is reached.workspaceis typed non-nullable, soworkspace?.idwould be redundant and would trip ourno-unnecessary-conditionlint.
Given that, I'll leave the code as-is. Happy to add a guard if we later start rendering this component outside the workspace layout.
|
Shouldn't we move all the copy to billing? E.g. billing server returns something like: It's more consistent with overall approach of moving cloud related code to billing server |
|
We also can consolidate other things in this PR. E.g. now we block workspaces for overdue invoices too. |
Per review: the console owns no banner copy or policy. billing/settings
now returns `banners` — [{id, severity, dismissible, html, action}] —
and BillingBanners renders them verbatim (QuotaBanner removed). The
server-provided stable id keys client-side dismissal, so the server
re-shows a dismissed banner by changing the id (escalation/new period).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| } | ||
| forceRerender(); | ||
| }} | ||
| message={<span dangerouslySetInnerHTML={{ __html: banner.html }} />} |
There was a problem hiding this comment.
Potential security bug: this renders server-provided HTML via dangerouslySetInnerHTML without any sanitization. Even if the source is our billing service, a compromised service, bad deploy, or accidental unsafe content could execute script in the console origin (XSS). Could we sanitize (DOMPurify/allowlist) before rendering, or switch the payload to structured text/links instead of raw HTML?
Console side of
JITSU-88(PR 3) — the in-app usage-quota banner. The billing logic lives in jitsu-cloud-billing (PR 1); this PR only renders what ee-api describes.What it does
ee-api's
billing/settingsnow returns aquotaStatusobject (see jitsu-cloud-billing#25). This PR renders it as a banner in the workspace layout:warning80warning90warning95blockedCopy matches the issue's in-app banner section (usage/quota, percent, period-reset date, blocked-count when >0). Dismissal is keyed by
workspace + level + period, so an escalation (80→90) or a new billing period re-shows the banner.How it's wired
lib/schemagains an optionalQuotaStatus(mirrors the billing type).quotaStatusis a sibling ofsubscriptionStatusin the billing/settings response, soparseBillingSettingsattaches it onto the parsedBillingSettingsafter parsing the subscription.QuotaBannerreadsuseBilling().settings.quotaStatusand self-gates (renders nothing when billing is disabled, still loading,quotaStatusis null, orlevel === "none"). It mounts inWorkspacePageLayoutbetween the header and content, hidden in fullscreen data views. The UI stays fully usable when blocked (banner, not a modal) so the user can upgrade.Testing
turbo run typecheck --filter=@jitsu-internal/consolepasses. Rendering is a straightforward switch overlevel; behavior verified by reading. No unit test added (pure presentational component; the console has no component-test harness for billing UI).Related
🤖 Generated with Claude Code