[5/n] Render per-user/team/own rows in cycle usage section#11123
[5/n] Render per-user/team/own rows in cycle usage section#11123IsaiahWitzke wants to merge 16 commits into
Conversation
Replaces the placeholder body of the billing cycle usage section with real per-row rendering driven by the resolved `UsageVisibility` model: * OwnOnly viewers see one self row with their cost-type breakdown * TeamAggregate admins see one synthetic 'Team total' row * PerUserTotals admins see one row per workspace member (zero rows for members with no usage), plus a Local/Cloud source filter when the workspace has any cloud usage Each row renders a stacked horizontal bar (filled portion sized to `total_credits / team_max_credits`, min 5%) with cost-type segments, and a hover tooltip showing the per-cost-type breakdown plus totals. Code lives in a new `billing_cycle_usage_rows` module; the section view only owns the `source_filter` state, the `RowMouseStates`, and a new `ChangeSourceFilter` action that the toggle dispatches. Visual model mirrors the admin-panel reference at `warp-server/client/src/components/admin/billing/memberUsageUtils.ts` + `MemberUsageTable.tsx` (sort orders, color palette, zero-row backfill for the current cycle only, Voice / SuggestedCodeDiffs / Team subjects filtered before aggregation). Enterprise admins still short-circuit to the admin-panel CTA before `render_body` runs. Co-Authored-By: Oz <oz-agent@warp.dev>
…indow edge Without explicit width bounds, the per-row tooltip's inner rows use `MainAxisSize::Max` (for their `SpaceBetween` alignment) and the overlay layout inherits the window width via `ParentOffsetBounds::WindowByPosition` \u2014 the tooltip greedily expands to fill the entire panel. Wrap the tooltip in a `ConstrainedBox` with sensible min/max width bounds (240\u2013360px) so it sizes like a tooltip. Also bump `TOOLTIP_GAP` from 4px to 6px for a slightly more legible separation between the row and the tooltip. Co-Authored-By: Oz <oz-agent@warp.dev>
The tooltip_lines Vec was always initialized as segments.clone() at every call site and only ever read as a separate source of breakdown lines. The distinction would only matter if we wanted zero-credit lines to appear in the tooltip but not in the bar; we don't synthesize those today, so the two fields stayed in lockstep. Drop tooltip_lines and use segments directly in render_usage_tooltip_content and render_member_row's empty-row check. Co-Authored-By: Oz <oz-agent@warp.dev>
Previously used theme.tooltip_background() which resolves to neutral_6 (60% foreground blend) and in light mode came out as a muddy mid-gray that didn't sit in the rest of the surface palette. Switch to theme.background() so the tooltip reads as a clean card-on-page popover with the outline border providing the only separation from the rows beneath it. Text colors are computed against the new background so contrast stays correct in light + dark themes. Co-Authored-By: Oz <oz-agent@warp.dev>
The bar sits flush against the top edge of the row card, which has a rounded with_top corner radius. Container.with_corner_radius only rounds the Container's own painted rect, not its children \u2014 so the rectangular colored segments inside the bar were visibly overflowing into the card's rounded top-left/top-right corners. Clipped only does rectangular clipping so it can't help here. Apply per-corner radii directly to the bar's outermost children: - Empty/zero bar: muted track with CornerRadius::with_top - Leftmost colored segment: top_left - Rightmost colored segment (only if no muted tail): top_right - Muted track tail (if present): top_right This keeps the bar visually flush with the card and removes the overflow. Co-Authored-By: Oz <oz-agent@warp.dev>
* Switch row card background from surface_1 to theme.background() so the rows visually match the tooltip (which already uses background()). Border color goes from surface_3 to theme.outline() for the same reason. * Apply the change to the empty-filter-state card too. * Container paints its child inside the border, so the stacked bar begins 1px inset from the card's outer edge. Using the card's full outer radius (8px) on the bar produced a visible 1px step at the top-left/top-right corners. Introduce BAR_CORNER_RADIUS = ROW_BORDER_RADIUS - ROW_BORDER_WIDTH so the bar's inner curve aligns flush with the card's inner curve. Co-Authored-By: Oz <oz-agent@warp.dev>
…, dropdown fix
(a) Tooltip drop shadow so it clearly floats above the rows.
(b) Period dropdown: prevent_interaction_with_other_elements() on the
menu so a click on the trigger while the menu is open is consumed
by the dismiss handler rather than also firing the trigger's
on_click, which previously closed-then-reopened the menu.
(c) Tooltip rows: credits and $cost render in fixed-width
right-aligned columns so the numbers line up vertically across
rows even though each row is laid out independently. Total row
drops the swatch and uses semibold for emphasis.
(d) No 'cr' suffix anywhere. Tooltip uses '/' between the credit count
and the dollar amount, matching the admin-panel reference.
(e) Per-row credit cluster:
- MemberUsageRow gains a base_limit: Option<i64>, populated from
WorkspaceMember.usage_info.request_limit when the member is not
flagged is_unlimited. Service accounts and the synthetic team-
aggregate row stay None.
- render_rows looks up the viewer's member entry for OwnOnly so
the viewer sees their own base limit too.
- Row body renders [CoinsStacked] X[/limit] (cluster gap)
[Credits-icon] $cost. No 'cr'. The credit-card slot uses
Icon::Credits as a stand-in; swap to a real credit-card icon
when one is added to the bundled set.
Co-Authored-By: Oz <oz-agent@warp.dev>
4ff0c42 to
8decc7f
Compare
Drop in a credit-card SVG (~/Downloads/Vector.svg, exported from Figma) as bundled/svg/credit-card.svg, register Icon::CreditCard in the warp_core icon enum + path map, and wire up the cycle-usage row to use it: - Coin slot (left of credits text) switches from Icon::CoinsStacked to Icon::Credits so it matches the 'Buy credits' denomination buttons. - Cost slot (left of $cost text) switches from the Icon::Credits stand-in to the new Icon::CreditCard. Co-Authored-By: Oz <oz-agent@warp.dev>
Previously the synthetic `Aggregate` cost type shared the pink/purple swatch of `BaseLimit`, which was confusing in TeamAggregate and PerUserTotals modes where every bar/tooltip was painted base-pink even though the segments aren't actually base credits. Introduces a neutral gray (#8C8C8C) reserved for the aggregated bucket, adds an "Aggregated" legend entry alongside the existing four chromatic ones, and renames the label from "Total" to "Aggregated" so it no longer collides with the tooltip's "Total usage" footer. Also fixes a pre-existing `let_underscore_future` clippy violation in the Refresh action handler so the workspace stays warning-clean. Co-Authored-By: Oz <oz-agent@warp.dev>
…m-aggregate topper - Swap the row cards, tooltip, and empty-state placeholder borders from the theme outline to the same fixed gray (#2B2B2B / CARD_BORDER_COLOR) the Balance section's Base/Personal/Team credit cards use, so they read as part of the same card family. - Inline ROW_CLUSTER_GAP, TOOLTIP_CREDITS_COL_WIDTH, and TOOLTIP_COST_COL_WIDTH (each was only used once and didn't earn a module-level const). - Tighten the BAR_CORNER_RADIUS, MemberUsageRow.base_limit, MemberUsageRow.segments, cost_type_color, entry_is_renderable, aggregate_segments, build_own_usage_row, build_team_aggregate_row, build_member_usage_rows, render_stacked_bar, render_usage_tooltip_content, render_tooltip_row, build_row_card, render_member_row, render_source_filter_toggle, and render_rows comments; drop the verbose AGGREGATE_CREDITS_DOT_COLOR doc. - Render the Team total row at the top of PerUserTotals view: the team total fills the bar 100% and each member's bar reads as their share of the team's usage. Co-Authored-By: Oz <oz-agent@warp.dev>
…, lighter borders - Rename the synthetic Aggregate cost type's label from "Aggregated" to "All sources" everywhere (row code + section legend). - Add a small hover tooltip on the "All sources" legend entry: "Includes all credit sources." Rendered as a positioned overlay anchored below the dot/label cluster. - PerUserTotals sizing: team row always fills the bar 100%; member rows scale against the top individual member instead of the team total, so the heaviest user fills the bar and others read as a fraction of that user rather than vanishing to ~10% of the team aggregate. - Revert row cards / tooltip / empty-state placeholder borders to theme.outline().into_solid() (lighter), and also flip the Balance section's Base/Personal/Team cards to the same lighter border instead of CARD_BORDER_COLOR. CARD_BORDER_COLOR is no longer referenced. - Tighten the verbose comments in billing_cycle_usage_rows.rs (kept doc comments on public items, dropped long inline rationales) and inline ROW_CLUSTER_GAP / TOOLTIP_CREDITS_COL_WIDTH / TOOLTIP_COST_COL_WIDTH (single-use values). Co-Authored-By: Oz <oz-agent@warp.dev>
… width - New copy: "Aggregates usage from base, add-on, pay-as-you-go, and ambient credits." — names every source instead of leaving "all credit sources" as a vague chip. - Wrap the tooltip text via Text::new (multi-line capable) and constrain width to 220-280px so it wraps to two lines instead of shrink-wrapping to a single-line chip that read as a badge. - Bump padding to 12 horizontal / 8 vertical for a less cramped feel. Co-Authored-By: Oz <oz-agent@warp.dev>
…to match legend - Drop the ConstrainedBox min/max width and switch back to Text::new_inline so the tooltip renders as a single horizontal chip rather than wrapping awkwardly to two lines. - Rename 'ambient credits' to 'ambient-only credits' to match the legend label rendered right next to the tooltip and the row tooltip's cost-type label (both already say 'Ambient-only'). The trial widget on the same page uses 'Cloud agent trial' — there's a broader page-wide inconsistency between 'Cloud agent' and 'Ambient-only' that's out of scope here; the local choice is to match the adjacent legend. Co-Authored-By: Oz <oz-agent@warp.dev>
…into iw/billing-cycle-usage-rows
…side label - The Usage section header now mirrors the admin panel layout: bold "Usage" title with secondary muted "Resets MMM dd at h:mm a" subtext directly to its right.\n- Removed the duplicate render_resets_label from the right side of the header. The right side now only carries the period selector (when the tier exposes multiple cycles) and the refresh icon.\n- The subtext is hidden when the viewer has selected a past cycle from the period dropdown — in that case the dropdown itself shows the\n cycle's date range, and "Resets ..." would be misleading.\n- Timezone abbreviation is intentionally omitted, matching the requested\n pattern (e.g. "Resets May 27 at 11:24 PM").\n\nCo-Authored-By: Oz <oz-agent@warp.dev>
|
I'm starting a first review of this pull request. You can view the conversation on Warp. I completed the review and no human review was requested for this pull request. Comment Powered by Oz |
There was a problem hiding this comment.
Overview
This PR adds billing cycle usage row rendering for own, team aggregate, and per-user visibility modes, including stacked bars, tooltips, source filtering, and a new credit-card icon. The PR includes visual evidence in the description.
Concerns
- Team aggregate visibility can render as empty when the server supplies the already-collapsed
Teamentries because the aggregate builder reuses a predicate that drops those rows. - Own-only filtering treats missing subject UIDs as belonging to the viewer, which weakens the client-side privacy guard.
- A previously selected Cloud/Local source filter can become stuck when the toggle is hidden for a later data set with no cloud usage.
Security
- The OwnOnly row should fail closed when the authenticated viewer UID is known but an entry has no subject UID; otherwise malformed or partially redacted rows can be displayed in a viewer-only surface.
Verdict
Found: 0 critical, 3 important, 0 suggestions
Request changes
Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).
Powered by Oz
| .filter(|e| match (viewer_uid, e.subject_uid.as_deref()) { | ||
| (Some(uid), Some(entry_uid)) => uid == entry_uid, | ||
| (_, None) => true, | ||
| (None, _) => true, |
There was a problem hiding this comment.
viewer_uid is known, entries without a subject_uid are treated as the viewer's own usage, so a malformed or redacted non-viewer row could be exposed in OwnOnly views. Fail closed for missing UIDs in the Some(uid) case.
| .filter(|e| match (viewer_uid, e.subject_uid.as_deref()) { | |
| (Some(uid), Some(entry_uid)) => uid == entry_uid, | |
| (_, None) => true, | |
| (None, _) => true, | |
| .filter(|e| match (viewer_uid, e.subject_uid.as_deref()) { | |
| (Some(uid), Some(entry_uid)) => uid == entry_uid, | |
| (Some(_), None) => false, | |
| (None, _) => true, |
| ) -> MemberUsageRow { | ||
| let filtered = entries | ||
| .iter() | ||
| .filter(|e| entry_is_renderable(e)) |
There was a problem hiding this comment.
build_team_aggregate_row still filters through entry_is_renderable, which drops SubjectType::Team; if the server sends only the collapsed team aggregate row for TeamAggregate visibility, this renders zero usage. Include team aggregate entries here or branch on whether collapsed rows are present.
| let member_rows = build_member_usage_rows(entries, &workspace.members, source_filter); | ||
|
|
||
| // Only show the toggle if there's cloud usage to filter against. | ||
| if has_cloud_usage(entries) { |
There was a problem hiding this comment.
source_filter can still be Cloud from a previous period; then the UI shows No cloud usage with no way to switch back. Keep the toggle visible when source_filter != SourceFilter::All or reset the filter on period/data changes.
Uh oh!
There was an error while loading. Please reload this page.