Skip to content

JITSU-88 feat(console): usage-quota banner#1414

Open
absorbb wants to merge 2 commits into
newjitsufrom
feat/console-quota-banner
Open

JITSU-88 feat(console): usage-quota banner#1414
absorbb wants to merge 2 commits into
newjitsufrom
feat/console-quota-banner

Conversation

@absorbb

@absorbb absorbb commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

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/settings now returns a quotaStatus object (see jitsu-cloud-billing#25). This PR renders it as a banner in the workspace layout:

level style dismissible
warning80 info yes (per workspace + level + period)
warning90 warning yes
warning95 warning no (persistent)
blocked error no (persistent — delivery paused)

Copy 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/schema gains an optional QuotaStatus (mirrors the billing type). quotaStatus is a sibling of subscriptionStatus in the billing/settings response, so parseBillingSettings attaches it onto the parsed BillingSettings after parsing the subscription.
  • QuotaBanner reads useBilling().settings.quotaStatus and self-gates (renders nothing when billing is disabled, still loading, quotaStatus is null, or level === "none"). It mounts in WorkspacePageLayout between 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.
  • The console owns no thresholds or block decision — those all live in ee-api.

Testing

turbo run typecheck --filter=@jitsu-internal/console passes. Rendering is a straightforward switch over level; 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

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>
jitsu-code-review[bot]
jitsu-code-review Bot previously approved these changes Jul 20, 2026

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed quota-banner + billing-settings wiring; implementation is mostly clean. I left one inline question about a potential runtime crash path when workspace id is missing during initial render.

return null;
}

const key = dismissKey(workspace.id, quota);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — I looked into this and it isn't a reachable crash path here:

  • useWorkspace() (lib/context.tsx) throws if there's no WorkspaceContext provider; it never returns undefined or a partial object. So when it returns, workspace is a full WorkspaceContext with a non-nullable id.
  • QuotaBanner only mounts inside WorkspacePageLayout, which already dereferences workspace.deleted (and workspace.slug) earlier in its own render — if workspace were ever undefined, the layout would throw before the banner is reached.
  • workspace is typed non-nullable, so workspace?.id would be redundant and would trip our no-unnecessary-condition lint.

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.

@absorbb absorbb added the deploy:console Auto-deploy console to beta when this PR merges (JITSU-68) label Jul 20, 2026
@vklimontovich

Copy link
Copy Markdown
Contributor

Shouldn't we move all the copy to billing? E.g. billing server returns something like:

{
   "dismissible": true,
   "blocking": false,
   "content": " ⚠️ You've used <b>90%</b> of your events quota. Period resets ...."   
}

It's more consistent with overall approach of moving cloud related code to billing server

@vklimontovich

Copy link
Copy Markdown
Contributor

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>

@jitsu-code-review jitsu-code-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the billing banner plumbing across BillingProvider, schema updates, and rendering in WorkspacePageLayout. The integration is straightforward, but I found one actionable security concern in banner rendering that should be addressed.

}
forceRerender();
}}
message={<span dangerouslySetInnerHTML={{ __html: banner.html }} />}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deploy:console Auto-deploy console to beta when this PR merges (JITSU-68)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants