Skip to content

[WIP]: feat(usage-dashboard): usage dashboard prototype: show every state on fixture data - #8192

Draft
talissoncosta wants to merge 20 commits into
mainfrom
spike/usage-billing-transparency
Draft

[WIP]: feat(usage-dashboard): usage dashboard prototype: show every state on fixture data#8192
talissoncosta wants to merge 20 commits into
mainfrom
spike/usage-billing-transparency

Conversation

@talissoncosta

@talissoncosta talissoncosta commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Thanks for submitting a PR! Please check the boxes below:

  • I have read the Contributing Guide.
  • I have added information to docs/ if required so people know about the feature.
  • I have filled in the "Changes" section below.
  • I have filled in the "How did you test this code" section below.

Changes

Contributes to #8184

Prototype, not for merge. It replaces the usage page on this branch so the v0.2 designs can be reviewed before we commit to building them. The production work is tracked under #8183.

The page runs on fixtures, so every designed state can be shown without depending on what an organisation has actually used:

  • healthy
  • approaching the limit
  • over limit, grace period covering this month
  • over limit, grace period used and charged
  • free plan, inside the 7 day countdown
  • free plan, restricted

A "Live data" option uses the real usage endpoints.

On the page: billing period with reset date and grace chip, "% of plan consumed" meter with notification markers, tiles that change per state, cumulative usage against the plan limit with a shaded overage zone and a dashed projection, request-type breakdown, and the notifications screen.

Reset date, grace state and cost are faked here. The API work that would make them real is in #8183.

Also included: the fix for #6099, current billing period ignoring terms longer than a year. One line and a test, no dependency on the prototype, and it can be split out to merge on its own.

How did you test this code?

Type checking and linting only. The page has not been rendered yet, I had no local API running to load it against.

To try it: check out the branch, run the frontend against an API, and open Organisation Settings, then Usage. The state switch at the top moves between the six fixtures, and the tabs move between the usage and notifications screens. Only "Live data" needs the API to return anything.

talissoncosta and others added 11 commits July 31, 2026 12:13
Reframes OrganisationUsagePage with a "usage vs plan limit" summary:
% of plan consumed, a meter, and a cumulative-vs-limit chart, all wired
to the existing usage-data + max_api_calls (no backend change).

Everything the API cannot feed yet is marked TODO(BE) and listed in the
component so the diff doubles as the backend ask: reset date / billing
period boundaries, the current-billing-period date-range bug, projection,
grace-period status, and cost in currency.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expands the prototype to the full v0.2 layout for the global view:
billing-period strip, hero meter with Notify markers, stat tiles,
cumulative usage-vs-limit chart (Area + plan-limit ReferenceLine + today
dot), and the request-type breakdown. Global view now renders this in
place of the old totals + bar chart; By SDK view is unchanged.

Still wired only to existing data; reset date, projection and cost remain
TODO(BE).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Swaps the hardcoded hex (accent/danger/success/warning + panel/border/
text/track) for the --color-* semantic tokens via common/theme/tokens, so
the prototype flips correctly under .dark.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`_get_start_date_and_stop_date_for_subscribed_organisation` derived the
current period start from relativedelta(...).months only, dropping the
years component, so terms that started >12 months ago (annual plans)
resolved to the wrong year. Add years*12, mirroring the existing
PREVIOUS_BILLING_PERIOD branch, and cover the annual case with a test.

Note: api env (uv) not available locally; needs CI to run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The reframed design drops the left "By Endpoint / By SDK" sidebar (SDK
becomes a breakdown-dimension option instead), so remove the sidebar and
the global/user-agents toggle from the page.

Keep the Period/Project/Environment filters. Per the agreed behaviour, the
plan limit is org-level, so the meter + cumulative chart use an org-wide
query (period only) while Project/Environment filter a second query that
feeds only the request-type breakdown.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Environment is a drill-down within a project and too granular for
org-level usage; project is the meaningful pivot. Remove the environment
select from UsageChartFilters and the page's filtered query.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Match the design: Usage title on the left, Period + Project selects on the
right of the page header (dropping the separate labelled filter row). Give
the surface a max-width and centre it instead of the left-aligned, narrow
app-container layout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Recharts auto-scaled the y-axis to usage, so with tiny usage against a
large limit the plan-limit ceiling fell off-screen and the "usage vs
limit" comparison was lost. Fix the domain to [0, max(limit, usage)] so
the ceiling is always visible (and over-limit usage still shows above it).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The end marker's label was centred on the far-right point and half of it
overflowed the panel; right-align it so it stays within bounds.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a scenario switch backed by fixtures so the over-limit, restricted and
grace states can be shown on demand, fills in the tiles that were printing
"needs BE", and adds the notifications screen.

Restores the existing usage page and puts the prototype behind the
usage_billing_prototype flag, so nothing changes until the flag is on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
flagsmith-frontend-preview Ready Ready Preview Aug 3, 2026 2:07pm
flagsmith-frontend-staging Ready Ready Preview Aug 3, 2026 2:07pm
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Ignored Ignored Preview Aug 3, 2026 2:07pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change fixes annual billing-period date calculation and adds a regression test. It extends StatItem with optional subtext, badges, icons, and size variants, then updates related views and stories. It adds a typed usage billing prototype with fixtures, live data mapping, charts, banners, notes, notifications, billing controls, and status components. The organisation usage page now renders the prototype page.

Estimated code review effort: 5 (Critical) | ~120 minutes

✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.71%. Comparing base (10a3bb7) to head (282cd65).
⚠️ Report is 5 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #8192   +/-   ##
=======================================
  Coverage   98.71%   98.71%           
=======================================
  Files        1531     1531           
  Lines       61234    61272   +38     
=======================================
+ Hits        60446    60484   +38     
  Misses        788      788           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

The branch is never merged, so gating it behind a flag only means creating
one in Flagsmith before anyone can look at it. Check the branch out and the
prototype is there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Swaps the hand-rolled pieces for what already exists: StatItem for the tiles,
Chip for the grace status, BareButton for the state and screen switches, and
Format.shortenNumber instead of a local copy. Adds progressbar semantics to
the meter and group labels to the switches.

The tile badges go with this: StatItem has no badge slot, so the state now
reads from the meter colour, the banner and the chart. Whether StatItem should
grow one is a question for #8185.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
StatItem and Chip pulled the page away from the design: the tiles lost their
badge and sub-label and grew icon blocks, and the grace chip read as a flat
pill. Both are back to the designed layout.

Badges now follow the dot-and-label pattern from experiments/StatusBadge,
which looks like where the app is heading. That component is typed to
experiment statuses, so this is a local copy with a tone prop rather than a
change to shared code. Generalising it is a question for #8185.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
StatItem was a label, a value and an icon block, so anything richer got
hand-rolled next to it: the admin metrics cards render their sub-line in a
<small> outside the component, and the usage prototype had its own tiles.

It now carries its own card: label and optional badge on one line, the value,
then an optional sub-line. The icon is optional, since dense rows of figures
read better without one.

Call sites: the admin metrics cards drop their external <small>, Billing loses
the panel that was wrapping the limit cards, and the usage prototype uses
StatItem instead of its own tiles. Stories cover the new slots.

Lives on the prototype branch by choice, so it needs porting when this becomes
real work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the inline note under the meter, which is the only line on the page that
draws a conclusion rather than stating a number: on track, over by how much,
what it costs, what happens next. Wording follows the state, so the restricted
and grace-countdown screens explain themselves.

Adds the breakdown dimension menu (request type, project, environment, SDK)
with obviously invented project and environment names, so nobody mistakes the
demo for their own data. Live data only has request type, and says so.

Labels the dashed projection on the chart, which was an unexplained line.

Two fixes the screenshots turned up:

- the period selector read "Last 90 days" while the strip underneath said
  "Billing period 18 Jul to 17 Aug". It follows the fixture now
- rolling windows claimed a reset date. They are "Usage window" now, with no
  reset, since a rolling window never resets

Also adds the "See docs" link the notifications design has.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The y-axis top was the projected value itself, so its label had nowhere to
render and got cut off by the edge of the chart. The axis now carries 8%
headroom above the highest value, and the label sits above the endpoint
rather than inside the corner.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
You could add a notification but never get rid of one. Each row now has a
remove control next to its toggle, labelled for screen readers.

Rows above 100% claimed "You have reached your plan limit", which is wrong
once you are past it. They now read "You are over your plan limit", and only
the 100% row claims to be at it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The settings screen kept its own copy of the notifications, so removing 75%
left the "Notify 75%" marker sitting on the meter, and edits vanished on tab
switch. State lives on the page now and both screens read the same list.

Changing scenario drops the edits, so each fixture still shows its own
notifications.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@talissoncosta talissoncosta changed the title Usage dashboard prototype: show every state on fixture data [WIP]: feat(usage-dashboard): usage dashboard prototype: show every state on fixture data Aug 3, 2026
@talissoncosta
talissoncosta marked this pull request as ready for review August 3, 2026 20:56
@talissoncosta
talissoncosta requested review from a team as code owners August 3, 2026 20:56
@talissoncosta
talissoncosta requested review from khvn26 and kyle-ssg and removed request for a team August 3, 2026 20:56
@flagsmith-engineering flagsmith-engineering Bot assigned kyle-ssg and khvn26 and unassigned kyle-ssg Aug 3, 2026
@talissoncosta
talissoncosta marked this pull request as draft August 3, 2026 20:56
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Docker builds report

Image Build Status Security report
ghcr.io/flagsmith/flagsmith-e2e:pr-8192 Finished ✅ Skipped
ghcr.io/flagsmith/flagsmith-api-test:pr-8192 Finished ✅ Skipped
ghcr.io/flagsmith/flagsmith-api:pr-8192 Finished ✅ Results
ghcr.io/flagsmith/flagsmith:pr-8192 Finished ✅ Results
ghcr.io/flagsmith/flagsmith-private-cloud:pr-8192 Finished ✅ Results
ghcr.io/flagsmith/flagsmith-frontend:pr-8192 Finished ✅ Results

@coderabbitai coderabbitai 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.

Actionable comments posted: 15


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c0ad5152-842a-4e5b-882f-774d8fbcbb0b

📥 Commits

Reviewing files that changed from the base of the PR and between 10a3bb7 and 282cd65.

📒 Files selected for processing (23)
  • api/app_analytics/analytics_db_service.py
  • api/tests/unit/app_analytics/test_analytics_db_service.py
  • frontend/documentation/components/StatItem.stories.tsx
  • frontend/web/components/StatItem.scss
  • frontend/web/components/StatItem.tsx
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/GraceChip.tsx
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/UsageBadge.scss
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/UsageBadge.tsx
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/UsageBanner.tsx
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/UsageBillingPrototype.scss
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/UsageBillingPrototype.tsx
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/UsageBillingPrototypePage.tsx
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/UsageChart.tsx
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/UsageNote.tsx
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/UsageNotifications.tsx
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/fixtures.ts
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/format.ts
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/index.ts
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/types.ts
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/usePrototypeUsage.ts
  • frontend/web/components/pages/OrganisationUsagePage.tsx
  • frontend/web/components/pages/admin-dashboard/components/InstanceMetricsCards.tsx
  • frontend/web/components/pages/organisation-settings/tabs/BillingTab.tsx

Comment on lines +132 to +153
const periodStart = moment().subtract(daysElapsed - 1, 'days')
const resetsAt = periodStart.clone().add(periodDays, 'days')

return {
breakdowns: buildBreakdowns(total),
channels: { email: true, inApp: true },
grace,
graceDaysLeft,
limit,
notifications: [
{ enabled: true, percent: 75 },
{ enabled: true, percent: 100 },
],
overageCost,
period: {
daysRemaining: periodDays - daysElapsed,
isBillingPeriod,
label:
periodLabel ??
`${periodStart.format('D MMM')} to ${resetsAt.format('D MMM YYYY')}`,
// Rolling windows never reset, so they get no reset date.
resetsAt: isBillingPeriod ? resetsAt.format('D MMM YYYY') : '',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Align daysRemaining with resetsAt.

periodStart is daysElapsed - 1 days ago, so it counts today as an elapsed day. resetsAt is therefore periodDays - daysElapsed + 1 days in the future, but daysRemaining is periodDays - daysElapsed. For the healthy fixture the panel states 13 days remaining while the reset date is 14 days away. Design reviewers see both values on the same screen.

🐛 Proposed fix to make both values agree
   return {
     breakdowns: buildBreakdowns(total),
     channels: { email: true, inApp: true },
     grace,
     graceDaysLeft,
     limit,
     notifications: [
       { enabled: true, percent: 75 },
       { enabled: true, percent: 100 },
     ],
     overageCost,
     period: {
-      daysRemaining: periodDays - daysElapsed,
+      daysRemaining: periodDays - daysElapsed + 1,
       isBillingPeriod,
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const periodStart = moment().subtract(daysElapsed - 1, 'days')
const resetsAt = periodStart.clone().add(periodDays, 'days')
return {
breakdowns: buildBreakdowns(total),
channels: { email: true, inApp: true },
grace,
graceDaysLeft,
limit,
notifications: [
{ enabled: true, percent: 75 },
{ enabled: true, percent: 100 },
],
overageCost,
period: {
daysRemaining: periodDays - daysElapsed,
isBillingPeriod,
label:
periodLabel ??
`${periodStart.format('D MMM')} to ${resetsAt.format('D MMM YYYY')}`,
// Rolling windows never reset, so they get no reset date.
resetsAt: isBillingPeriod ? resetsAt.format('D MMM YYYY') : '',
const periodStart = moment().subtract(daysElapsed - 1, 'days')
const resetsAt = periodStart.clone().add(periodDays, 'days')
return {
breakdowns: buildBreakdowns(total),
channels: { email: true, inApp: true },
grace,
graceDaysLeft,
limit,
notifications: [
{ enabled: true, percent: 75 },
{ enabled: true, percent: 100 },
],
overageCost,
period: {
daysRemaining: periodDays - daysElapsed + 1,
isBillingPeriod,
label:
periodLabel ??
`${periodStart.format('D MMM')} to ${resetsAt.format('D MMM YYYY')}`,
// Rolling windows never reset, so they get no reset date.
resetsAt: isBillingPeriod ? resetsAt.format('D MMM YYYY') : '',

Comment on lines +10 to +11
export const currency = (amount: number): string =>
`$${amount.toLocaleString()}`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Format currency with Intl.NumberFormat.

toLocaleString() applies the browser locale grouping and decimal separators, but the $ prefix is hard-coded. In a non-US locale the result mixes conventions, for example $1.234,5. Fractional cents also render with a variable number of digits.

♻️ Proposed fix
-export const currency = (amount: number): string =>
-  `$${amount.toLocaleString()}`
+const CURRENCY_FORMAT = new Intl.NumberFormat('en-US', {
+  currency: 'USD',
+  maximumFractionDigits: 0,
+  style: 'currency',
+})
+
+export const currency = (amount: number): string => CURRENCY_FORMAT.format(amount)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const currency = (amount: number): string =>
`$${amount.toLocaleString()}`
const CURRENCY_FORMAT = new Intl.NumberFormat('en-US', {
currency: 'USD',
maximumFractionDigits: 0,
style: 'currency',
})
export const currency = (amount: number): string => CURRENCY_FORMAT.format(amount)

Comment on lines +41 to +42
const label =
grace === 'countdown' && daysLeft ? `${daysLeft} days left` : LABEL[grace]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Day counts read "1 days" in three messages. All three sites interpolate a day count into a hard-coded plural noun. One shared helper, for example days(count), fixes every site.

  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/GraceChip.tsx#L41-L42: pluralise the countdown label built from daysLeft.
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/UsageBanner.tsx#L29-L31: pluralise "Flag serving pauses in ... days" built from view.graceDaysLeft.
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/UsageNote.tsx#L45-L54: pluralise "Flag serving pauses in ... days" built from view.graceDaysLeft.
📍 Affects 3 files
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/GraceChip.tsx#L41-L42 (this comment)
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/UsageBanner.tsx#L29-L31
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/UsageNote.tsx#L45-L54

Comment on lines +1 to +2
import { FC, ReactNode } from 'react'
import './UsageBadge.scss'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Components under frontend/web/components/ must each live in their own folder. All six new components sit flat inside UsageBillingPrototype/, with stylesheets beside them and no barrel files. The shared root cause is one layout decision for the whole prototype folder.

  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/UsageBadge.tsx#L1-L2: create UsageBadge/UsageBadge.tsx, move UsageBadge.scss into that folder, and add UsageBadge/index.ts.
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/GraceChip.tsx#L1-L4: create GraceChip/GraceChip.tsx with GraceChip/index.ts, and import UsageBadge by folder path.
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/UsageBanner.tsx#L1-L4: create UsageBanner/UsageBanner.tsx with UsageBanner/index.ts.
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/UsageNote.tsx#L1-L9: create UsageNote/UsageNote.tsx with UsageNote/index.ts.
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/UsageChart.tsx#L1-L21: create UsageChart/UsageChart.tsx with UsageChart/index.ts.
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/UsageBillingPrototype.tsx#L1-L12: keep UsageBillingPrototype.tsx and UsageBillingPrototype.scss in the UsageBillingPrototype/ folder, and import each sibling by its new folder path.

As per coding guidelines: "Each new component must live in its own folder with a barrel index.ts, a ComponentName/ComponentName.tsx file, co-located ComponentName.scss, any subcomponents, and imports should use the component folder path rather than the inner file."

📍 Affects 6 files
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/UsageBadge.tsx#L1-L2 (this comment)
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/GraceChip.tsx#L1-L4
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/UsageBanner.tsx#L1-L4
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/UsageNote.tsx#L1-L9
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/UsageChart.tsx#L1-L21
  • frontend/web/components/organisation-settings/usage/UsageBillingPrototype/UsageBillingPrototype.tsx#L1-L12

Source: Coding guidelines

Comment on lines +124 to +128
&__pct {
font-size: 40px;
font-weight: 700;
line-height: 1;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Merge the duplicated selector blocks.

Three classes are declared twice in this file: &__pct at lines 124-128 and 169-181, &__marker-label at lines 183-191 and 206-214, and &__bar-fill at lines 193-195 and 305-308. The __bar-fill declarations sit 110 lines apart, so the base styles and the tone modifiers are easy to change independently by mistake. Merge each pair into one block.

Also applies to: 169-195, 305-308

Comment on lines +57 to +75
const toggleRow = (percent: number) =>
onChange(
rows.map((row) =>
row.percent === percent ? { ...row, enabled: !row.enabled } : row,
),
)

const removeRow = (percent: number) =>
onChange(rows.filter((row) => row.percent !== percent))

const addRow = () => {
const highest = [...rows].map((row) => row.percent).sort((a, b) => b - a)[0]
onChange(
rows.concat({
enabled: true,
percent: Math.min((highest ?? 50) + 25, 500),
}),
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Prevent duplicate percent values in addRow.

percent is the row identity for the React key, toggleRow, and removeRow. addRow clamps the new value to 500, so once a 500 row exists, every further click appends a second row at 500. React then warns about duplicate keys, and one toggle or one remove acts on both rows. Stop adding a row when the clamp cannot produce a new value.

🐛 Proposed fix
   const addRow = () => {
-    const highest = [...rows].map((row) => row.percent).sort((a, b) => b - a)[0]
-    onChange(
-      rows.concat({
-        enabled: true,
-        percent: Math.min((highest ?? 50) + 25, 500),
-      }),
-    )
+    const highest = rows.reduce((acc, row) => Math.max(acc, row.percent), 50)
+    const percent = Math.min(highest + 25, 500)
+    if (rows.some((row) => row.percent === percent)) return
+    onChange(rows.concat({ enabled: true, percent }))
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const toggleRow = (percent: number) =>
onChange(
rows.map((row) =>
row.percent === percent ? { ...row, enabled: !row.enabled } : row,
),
)
const removeRow = (percent: number) =>
onChange(rows.filter((row) => row.percent !== percent))
const addRow = () => {
const highest = [...rows].map((row) => row.percent).sort((a, b) => b - a)[0]
onChange(
rows.concat({
enabled: true,
percent: Math.min((highest ?? 50) + 25, 500),
}),
)
}
const toggleRow = (percent: number) =>
onChange(
rows.map((row) =>
row.percent === percent ? { ...row, enabled: !row.enabled } : row,
),
)
const removeRow = (percent: number) =>
onChange(rows.filter((row) => row.percent !== percent))
const addRow = () => {
const highest = rows.reduce((acc, row) => Math.max(acc, row.percent), 50)
const percent = Math.min(highest + 25, 500)
if (rows.some((row) => row.percent === percent)) return
onChange(rows.concat({ enabled: true, percent }))
}

Comment on lines +98 to +101
<Switch
checked={row.enabled}
onChange={() => toggleRow(row.percent)}
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
fd -a 'UsageNotifications\.tsx|Switch\.(tsx|ts|js|jsx)$' frontend || true

echo
echo "== target file outline =="
ast-grep outline frontend/web/components/organisation-settings/usage/UsageBillingPrototype/UsageNotifications.tsx || true

echo
echo "== target relevant lines =="
cat -n frontend/web/components/organisation-settings/usage/UsageBillingPrototype/UsageNotifications.tsx | sed -n '1,170p'

echo
echo "== Switch definitions/usages =="
rg -n "export .*Switch|const Switch|function Switch|Switch\s+|aria-label" frontend/web/components frontend/common frontend/project -g '*.{ts,tsx,js,jsx}' | head -200

Repository: Flagsmith/flagsmith

Length of output: 18475


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Switch component =="
cat -n frontend/web/components/Switch.tsx | sed -n '1,90p'

echo
echo "== nearby Switch accessible usages =="
cat -n frontend/web/components/CompareEnvironments/CompareFeatureRow.tsx | sed -n '48,72p'
cat -n frontend/web/components/modals/AuditLogWebhooks.tsx | sed -n '108,118p'

echo
echo "== repo package dependency mentions =="
rg -n '"react-rc-switch|"rc-switch|"Switch"' frontend -g 'package.json' || true

Repository: Flagsmith/flagsmith

Length of output: 3745


🌐 Web query:

rc-switch 1.9.0 Props aria-label source

💡 Result:

The rc-switch library, specifically version 1.9.0, does not explicitly list an aria-label prop in its official documentation or API properties [1][2][3]. The documented properties for rc-switch (as of version 1.9.0 and similar historical versions) include prefixCls, className, checked, defaultChecked, onChange, tabIndex, onClick, autoFocus, disabled, and loadingIcon [1][2][3]. While modern versions of the component (now under the @rc-component/switch package) have evolved and include various improvements, if you are working specifically with the legacy 1.9.0 version, you may need to apply an aria-label directly to the component if it is accepted as an arbitrary HTML attribute, or handle accessibility labels through an external wrapper or label element if the component does not natively pass through unrecognized attributes to the underlying DOM node [4]. If you require native aria-label support for a switch component, you might consider migrating to a newer version or an alternative like React Aria, which explicitly supports aria-label [5][6].

Citations:


🌐 Web query:

github react-component switch src rc-switch 1.9.0 render input id className aria-label

💡 Result:

In the rc-switch library (versions 1.9.0 and consistent throughout the library's history), the component is designed as a high-level UI primitive rather than a standard HTML <input> wrapper that exposes all native input attributes directly. Regarding the specific properties requested for rc-switch (version 1.9.0 and similar): 1. className: This property is supported and functions as intended, applying additional class names to the root node of the component [1][2][3][4]. 2. id and aria-label: These properties are not explicitly listed in the standard rc-switch API documentation for version 1.9.0 or the current versions [1][5][2][3]. Unlike standard HTML input elements, rc-switch renders as a semantic element (typically a <button> with role="switch") [5], and it does not provide a direct pass-through mechanism for all arbitrary id or aria-* attributes to an underlying input element. If you require full control over id, aria-label, or other specific input attributes for accessibility or form integration, you may need to: - Use a wrapper element to attach these attributes, although this will not apply them to the internal component logic. - Consider using an alternative library designed for full ARIA/input support if these attributes are critical for your implementation (e.g., react-aria or Headless UI which are built to expose such attributes explicitly [6][7][8]). For the specific version 1.9.0, you can verify the supported API by checking the README.md or package.json in the corresponding GitHub tag/branch, as the property list is constrained to the documented props (such as prefixCls, checked, disabled, etc.) [1][2][3].

Citations:


🌐 Web query:

github react-component switch v1.9.0 index.html children aria-label

💡 Result:

The rc-switch library (now maintained under @rc-component/switch) is an accessible React switch component commonly used within the Ant Design ecosystem [1][2]. Regarding your specific interests: Accessibility and Attributes The current version of the library explicitly supports accessibility by rendering a native button with role="switch" and aria-checked attributes [1][2]. While older versions (such as 1.9.x) primarily focused on basic functionality [3], modern versions have improved accessibility features [2]. If you require an aria-label for screen reader support—especially when the switch has no visible text—you can pass it as a standard HTML attribute to the Switch component, as it accepts standard HTML attributes [1][2][4]. Children The component provides specific slots for content via the checkedChildren and unCheckedChildren props [1][2]. These allow you to render custom React nodes (such as text or icons) that appear when the switch is in the checked or unchecked state, respectively [1][2]. Version 1.9.0 Context Version 1.9.0 was released around January 2019 [3]. It predates the recent migration to the @rc-component namespace and the introduction of more advanced semantic slots (like classNames and styles objects introduced in later major versions) [1][3][2]. For projects relying on this legacy version, you would typically use the checkedChildren and unCheckedChildren props to display content, and accessibility would generally be handled by the DOM elements rendered by the library at that time [5][3]. It is highly recommended to migrate to the latest version of @rc-component/switch to benefit from continued maintenance, bug fixes, and updated accessibility standards [1][2].

Citations:


🌐 Web query:

react-component/switch 1.9.0 source class Switch render htmlAttributesSpread

💡 Result:

The term htmlAttributesSpread is not a recognized function, method, or property within the source code or API of rc-switch (react-component/switch) version 1.9.0 [1][2][3]. In the context of React development, "html attributes spread" typically refers to the pattern of using the object spread operator (e.g., {...props}) to pass arbitrary HTML attributes to a component's underlying DOM element. In version 1.9.0 of rc-switch, the component was designed to handle specific, documented properties such as prefixCls, className, checked, defaultChecked, onChange, onClick, tabIndex, autoFocus, disabled, and loadingIcon [4][5][2][3]. There is no evidence that version 1.9.0 implemented a specific internal method named htmlAttributesSpread to handle the passing of remaining attributes [1][3]. If you are encountering this term in a codebase, it is likely either a custom helper function implemented locally by your team to manage prop spreading, or a misidentification of the standard React JSX spread syntax used to pass extraneous props down to the root DOM node.

Citations:


🌐 Web query:

"https://cdn.jsdelivr.net/npm/rc-switch@1.9.0/lib/index.js"

💡 Result:

The URL you provided, https://cdn.jsdelivr.net/npm/rc-switch@1.9.0/lib/index.js, points to a specific version (1.9.0) of a JavaScript library named rc-switch [1]. There are two distinct projects that commonly use the name "rc-switch": 1. React Component Switch: This is a widely used React UI library for a switch component [1][2][3]. It is part of the react-component ecosystem and is often used as a dependency in larger frameworks like Ant Design [3]. The version 1.9.0 of this library was published around 2019 [1]. Note that more recent versions of this package have transitioned to the scope @rc-component/switch [3]. 2. Arduino RC-Switch: This is a completely different library for Arduino and other microcontrollers (like the ESP8266 or Raspberry Pi) designed to operate 433/315MHz remote radio-controlled devices, such as power outlets [4][5][6]. Given that your link uses the jsDelivr CDN to serve a JavaScript file from the npm registry, it refers to the React Component library [1]. You should be aware that version 1.9.0 is significantly outdated [1]. If you are building a modern React application, it is recommended to use the current version of the package, which is now maintained under the @rc-component/switch namespace [3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== fetch rc-switch 1.9.0 CDN source =="
curl -fsSL https://unpkg.com/rc-switch@1.9.0/lib/index.js >/tmp/rc-switch-1.9.0.js
wc -l /tmp/rc-switch-1.9.0.js
rg -n "props|className|id|aria|children|switch" /tmp/rc-switch-1.9.0.js | head -80

echo
echo "== inspect rendering section =="
sed -n '80,180p' /tmp/rc-switch-1.9.0.js

Repository: Flagsmith/flagsmith

Length of output: 222


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== try registry tarball =="
curl -fsSL https://registry.npmjs.org/rc-switch/1.9.0:tarball -o /tmp/rc-switch-1.9.0.tgz || true
if [ -f /tmp/rc-switch-1.9.0.tgz ]; then
  tar -tzf /tmp/rc-switch-1.9.0.tgz | sed -n '1,80p'
  mkdir -p /tmp/rs-tmp
  tar -xzf /tmp/rc-switch-1.9.0.tgz -C /tmp/rs-tmp
  rg -n "props|className|id|aria|children|switch|spreadProps|objectSpread" /tmp/rs-tmp/package -g '*.js' | head -120
  echo
  echo "== inspect rendered button/label sections =="
  rg -n "className=|return |a label|children" /tmp/rs-tmp/package/lib/index.js -A6 -B4
  sed -n '1,220p' /tmp/rs-tmp/package/lib/*.js | sed -n '1,180p'
fi

echo
echo "== direct CDN text =="
curl -fsSLi https://unpkg.com/rc-switch@1.9.0/lib/index.js | sed -n '1,40p'

Repository: Flagsmith/flagsmith

Length of output: 1171


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== fetch rc-switch 1.9.0 actual files =="
curl -fsSL https://unpkg.com/rc-switch@1.9.0/lib/Switch.js >/tmp/Switch.js
wc -l /tmp/Switch.js
sed -n '1,260p' /tmp/Switch.js

echo
echo "== compare with local component wrapper =="
cat -n frontend/web/components/Switch.tsx | sed -n '1,80p'

Repository: Flagsmith/flagsmith

Length of output: 11457


Give each Switch an accessible name.

The switches at lines 98, 126, and 135 are rendered without their own label or aria-label, so screen readers announce unnamed switches. Add explicit labels and keep them descriptive, for example Notify me at ${row.percent}% of plan consumed, in-app, and email. [accessibility]

Comment on lines +2 to +3
flex: 1;
min-width: 180px;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Prevent the dashboard metric row from overflowing.

InstanceMetricsCards renders five StatItem elements in a non-wrapping row. These minimum widths require at least 996px, including its gaps. At narrower content widths, the row overflows instead of reflowing. Add wrapping in that consumer or make this minimum width layout-specific.

Comment on lines 3 to 4
import Icon, { IconName } from './icons/Icon'
import Tooltip from './Tooltip'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Replace the relative component imports.

The frontend import rule does not permit relative imports in this file. Import Icon and Tooltip through components/icons/Icon and components/Tooltip.

As per coding guidelines: frontend/**/*.{js,jsx,ts,tsx} must use common/, components/, or project/ import paths and must not use relative imports.

Source: Coding guidelines

Comment on lines +20 to +22
icon?: IconName
/** 'sm' for text values like emails, which overflow at the default size. */
size?: 'default' | 'sm'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Extract the inline union types into named types.

Define named types for these unions, then use the names at each declaration.

  • frontend/web/components/StatItem.tsx#L20-L22: define StatItemSize for 'default' | 'sm'.
  • frontend/web/components/pages/organisation-settings/tabs/BillingTab.tsx#L45-L69: define a named type for LimitItem | undefined.

As per coding guidelines: extract inline union types into named types.

📍 Affects 2 files
  • frontend/web/components/StatItem.tsx#L20-L22 (this comment)
  • frontend/web/components/pages/organisation-settings/tabs/BillingTab.tsx#L45-L69

Source: Coding guidelines

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor
✅ private-cloud · depot-ubuntu-latest-arm-16 — run #18970 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-arm-16)

passed  3 passed

Details

stats  3 tests across 3 suites
duration  1 minute, 1 second
commit  282cd65
info  🔄 Run: #18970 (attempt 1)

🗂️ Previous results
✅ private-cloud · depot-ubuntu-latest-16 — run #18970 (attempt 1)

Playwright Test Results (private-cloud - depot-ubuntu-latest-16)

passed  22 passed
skipped  1 skipped

Details

stats  23 tests across 18 suites
duration  1 minute, 27 seconds
commit  282cd65
info  🔄 Run: #18970 (attempt 1)

Skipped tests

firefox › tests/onboarding-tests.pw.ts › Onboarding › New user connects via the single-page onboarding flow @oss

✅ oss · depot-ubuntu-latest-arm-16 — run #18970 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-arm-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  38.7 seconds
commit  282cd65
info  🔄 Run: #18970 (attempt 1)

✅ oss · depot-ubuntu-latest-16 — run #18970 (attempt 1)

Playwright Test Results (oss - depot-ubuntu-latest-16)

passed  1 passed

Details

stats  1 test across 1 suite
duration  41.2 seconds
commit  282cd65
info  🔄 Run: #18970 (attempt 1)

@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Visual Regression

19 screenshots compared. See report for details.
View full report

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

Labels

api Issue related to the REST API front-end Issue related to the React Front End Dashboard

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants