Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
636390a
spike(usage): prototype billing-aligned usage view to scope BE work
talissoncosta Jul 27, 2026
20c827c
spike(usage): build full S1 (healthy) design as prototype
talissoncosta Jul 27, 2026
b9c0c4b
spike(usage): use semantic colour tokens for dark-mode support
talissoncosta Jul 27, 2026
2e6c914
fix(usage): correct current-billing-period range for annual plans
talissoncosta Jul 27, 2026
ff0b745
spike(usage): drop legacy By Endpoint/SDK nav, keep project/env filters
talissoncosta Jul 27, 2026
e4c9894
spike(usage): drop environment filter, keep project only
talissoncosta Jul 27, 2026
8be59da
spike(usage): move period/project filters into header, center the page
talissoncosta Jul 27, 2026
baa55aa
spike(usage): add top padding above the Usage header
talissoncosta Jul 27, 2026
b18f235
spike(usage): pin chart y-axis to the plan limit
talissoncosta Jul 27, 2026
cca71c1
spike(usage): keep the 100% notify label inside the panel
talissoncosta Jul 27, 2026
eeac13f
spike(usage): cover every state with fixture data (#8184)
talissoncosta Jul 31, 2026
71e9e1c
spike(usage): drop the flag, the prototype is the page on this branch
talissoncosta Aug 3, 2026
db1d6c0
spike(usage): build the prototype on the design system
talissoncosta Aug 3, 2026
9accc85
spike(usage): revert to the designed tiles, badge follows experiments
talissoncosta Aug 3, 2026
2e2b4e3
feat(ds): give StatItem the tile design, with badge and sub slots
talissoncosta Aug 3, 2026
44ba60f
feat(billing): put the plan header on StatItem too
talissoncosta Aug 3, 2026
af5cd21
spike(usage): close the gaps against the v0.2 designs
talissoncosta Aug 3, 2026
dae918b
spike(usage): stop the projected label being clipped
talissoncosta Aug 3, 2026
9c01b88
spike(usage): let notifications be removed, fix the row copy
talissoncosta Aug 3, 2026
282cd65
spike(usage): make notification edits reach the meter
talissoncosta Aug 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/app_analytics/analytics_db_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ def _get_start_date_and_stop_date_for_subscribed_organisation(
raise NotFound("No billing periods found for this organisation.")

month_delta = relativedelta(now, starts_at).months
month_delta += relativedelta(now, starts_at).years * 12
date_start = relativedelta(months=month_delta) + starts_at
return date_start, now

Expand Down
33 changes: 33 additions & 0 deletions api/tests/unit/app_analytics/test_analytics_db_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,39 @@ def test_get_usage_data__current_billing_period__passes_correct_date_range(
)


@pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00")
def test_get_usage_data__current_billing_period_annual_plan__passes_correct_date_range(
mocker: MockerFixture,
settings: SettingsWrapper,
organisation: Organisation,
cache: OrganisationSubscriptionInformationCache,
) -> None:
# Given
# A billing term that started more than 12 months ago (annual plan): the
# months-only delta used to drop the years component and land a year early.
settings.USE_POSTGRES_FOR_ANALYTICS = True
cache.current_billing_term_starts_at = datetime(
2021, 12, 30, 9, 9, 47, 325132, tzinfo=UTC
)
cache.save()
mocked_get_usage_data_from_local_db = mocker.patch(
"app_analytics.analytics_db_service.get_usage_data_from_local_db", autospec=True
)

# When
get_usage_data(organisation, period=CURRENT_BILLING_PERIOD)

# Then the current period start is this month, not a year ago.
mocked_get_usage_data_from_local_db.assert_called_once_with(
organisation=organisation,
environment_id=None,
project_id=None,
date_start=datetime(2022, 12, 30, 9, 9, 47, 325132, tzinfo=UTC),
date_stop=datetime(2023, 1, 19, 9, 9, 47, 325132, tzinfo=UTC),
labels_filter=None,
)


@pytest.mark.freeze_time("2023-01-19T09:09:47.325132+00:00")
def test_get_usage_data__previous_billing_period__passes_correct_date_range(
mocker: MockerFixture,
Expand Down
29 changes: 29 additions & 0 deletions frontend/documentation/components/StatItem.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Meta, StoryObj } from 'storybook'
import StatItem from 'components/StatItem'
import StatusBadge from 'components/experiments/StatusBadge'

const meta: Meta<typeof StatItem> = {
component: StatItem,
Expand Down Expand Up @@ -56,3 +57,31 @@ export const StringValue: Story = {
value: 'Scale-Up',
},
}

export const WithSub: Story = {
args: {
icon: 'bar-chart',
label: 'Total API Calls',
sub: 'of 2M plan limit',
value: 1240000,
},
}

export const WithBadge: Story = {
args: {
badge: <StatusBadge status='running' />,
icon: 'flask',
label: 'Experiment',
sub: 'started 12 days ago',
value: 'Checkout v2',
},
}

// The icon is optional: dense rows of figures often read better without one.
export const WithoutIcon: Story = {
args: {
label: '% of plan consumed',
sub: 'this billing period',
value: '62%',
},
}
56 changes: 56 additions & 0 deletions frontend/web/components/StatItem.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
.stat-item {
flex: 1;
min-width: 180px;
Comment on lines +2 to +3

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.

padding: 16px;
border: 1px solid var(--color-border-default);
border-radius: var(--radius-md);
background: var(--color-surface-default);

&__head {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 8px;
}

&__icon {
flex-shrink: 0;
}

&__label {
font-size: 12px;
color: var(--color-text-secondary);
}

// Pushes the badge to the right of the label, whatever the label's length.
&__badge {
margin-left: auto;
}

&__value {
font-weight: var(--font-weight-bold);
line-height: 1.1;
// Long text values (emails, ids) wrap instead of pushing the card wide.
overflow-wrap: anywhere;

&--default {
font-size: 28px;
}

&--sm {
font-size: 16px;
}
}

&__limit {
font-size: 12px;
font-weight: var(--font-weight-regular);
color: var(--color-text-secondary);
}

&__sub {
margin-top: 2px;
font-size: 11px;
color: var(--color-text-secondary);
}
}
88 changes: 51 additions & 37 deletions frontend/web/components/StatItem.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import React, { FC, KeyboardEvent } from 'react'
import React, { FC, KeyboardEvent, ReactNode } from 'react'
import { colorIconDefault } from 'common/theme/tokens'
import Icon, { IconName } from './icons/Icon'
import Tooltip from './Tooltip'
Comment on lines 3 to 4

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

import './StatItem.scss'

type VisibilityToggleProps = {
colour: string
Expand All @@ -10,9 +11,15 @@ type VisibilityToggleProps = {
}

export type StatItemProps = {
icon: IconName
label: string
value: string | number
/** Qualifier under the value, e.g. "of 2M plan limit". */
sub?: ReactNode
/** State on the right of the label, e.g. a status badge. */
badge?: ReactNode
icon?: IconName
/** 'sm' for text values like emails, which overflow at the default size. */
size?: 'default' | 'sm'
Comment on lines +20 to +22

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

// Optional: for displaying limits (e.g., "1,000 / 10,000")
limit?: number | null
// Optional: hover tooltip on the label
Expand All @@ -22,9 +29,12 @@ export type StatItemProps = {
}

const StatItem: FC<StatItemProps> = ({
badge,
icon,
label,
limit,
size = 'default',
sub,
tooltip,
value,
visibilityToggle,
Expand All @@ -40,45 +50,49 @@ const StatItem: FC<StatItemProps> = ({
}

return (
<div className='d-flex flex-row align-items-start gap-2'>
<div className='plan-icon flex-shrink-0'>
<Icon name={icon} width={32} fill={colorIconDefault} />
</div>
<div>
<p className='fs-small lh-sm mb-0'>
<div className='stat-item'>
<div className='stat-item__head'>
{icon && (
<Icon
name={icon}
width={16}
fill={colorIconDefault}
className='stat-item__icon'
/>
)}
<span className='stat-item__label'>
{tooltip ? <Tooltip title={label}>{tooltip}</Tooltip> : label}
</p>
<h4 className='mb-0'>
{formattedValue}
{limit !== null && limit !== undefined && (
<span className='text-muted fs-small fw-normal'>
{' '}
/ {formatNumber(limit)}
</span>
)}
</h4>
{visibilityToggle && (
</span>
{badge && <span className='stat-item__badge'>{badge}</span>}
</div>
<div className={`stat-item__value stat-item__value--${size}`}>
{formattedValue}
{limit !== null && limit !== undefined && (
<span className='stat-item__limit'> / {formatNumber(limit)}</span>
)}
</div>
{sub && <div className='stat-item__sub'>{sub}</div>}
{visibilityToggle && (
<div
role='checkbox'
aria-checked={visibilityToggle.isVisible}
aria-label={`Toggle ${label} visibility`}
tabIndex={0}
className='cursor-pointer d-flex align-items-center gap-2 mt-2'
onClick={visibilityToggle.onToggle}
onKeyDown={handleKeyDown}
>
<div
role='checkbox'
aria-checked={visibilityToggle.isVisible}
aria-label={`Toggle ${label} visibility`}
tabIndex={0}
className='cursor-pointer d-flex align-items-center gap-2 mt-1'
onClick={visibilityToggle.onToggle}
onKeyDown={handleKeyDown}
className='visibility-checkbox'
style={{ backgroundColor: visibilityToggle.colour }}
>
<div
className='visibility-checkbox'
style={{ backgroundColor: visibilityToggle.colour }}
>
{visibilityToggle.isVisible && (
<Icon name='checkmark' width={10} fill='white' />
)}
</div>
<span className='text-muted fs-small'>Visible</span>
{visibilityToggle.isVisible && (
<Icon name='checkmark' width={10} fill='white' />
)}
</div>
)}
</div>
<span className='text-muted fs-small'>Visible</span>
</div>
)}
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { FC } from 'react'
import Tooltip from 'components/Tooltip'
import UsageBadge, { BadgeTone } from './UsageBadge'
import { GraceState } from './types'

type GraceChipProps = {
grace: GraceState
daysLeft?: number
}

const TONE: Record<GraceState, BadgeTone> = {
available: 'success',
countdown: 'warning',
covering: 'info',
restricted: 'danger',
used: 'danger',
}

const LABEL: Record<GraceState, string> = {
available: 'Grace period: available',
countdown: 'Grace period: ending',
covering: 'Grace period: covering this period',
restricted: 'Restricted',
used: 'Grace period: used',
}

const EXPLANATION: Record<GraceState, string> = {
available:
'Your first month over the limit is covered. We never cut off your API without warning.',
countdown:
'You are over your limit. Flag serving pauses when the grace window ends, unless usage drops back under.',
covering:
'You are over your limit, but this month is covered by your grace period, so there is no overage charge.',
restricted:
'The grace window has passed. Flag serving and admin access are paused, but this page stays readable.',
used: 'Your grace period has already been used, so usage above the limit is charged as overage.',
}

/** PROTOTYPE (#8184). Grace period status, per the "Grace period states" design. */
const GraceChip: FC<GraceChipProps> = ({ daysLeft, grace }) => {
const label =
grace === 'countdown' && daysLeft ? `${daysLeft} days left` : LABEL[grace]
Comment on lines +41 to +42

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


return (
<Tooltip title={<UsageBadge tone={TONE[grace]}>{label}</UsageBadge>}>
{EXPLANATION[grace]}
</Tooltip>
)
}

export default GraceChip
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Mirrors experiments/StatusBadge so the two read as one pattern.
.usage-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 10px;
border-radius: var(--radius-full);
font-size: 11px;
font-weight: var(--font-weight-medium);
white-space: nowrap;

&__dot {
width: 6px;
height: 6px;
border-radius: var(--radius-full);
}

&--success {
background: var(--color-surface-success);
color: var(--color-text-success);

.usage-badge__dot {
background: var(--color-text-success);
}
}

&--warning {
background: var(--color-surface-warning);
color: var(--color-text-warning);

.usage-badge__dot {
background: var(--color-text-warning);
}
}

&--danger {
background: var(--color-surface-danger);
color: var(--color-text-danger);

.usage-badge__dot {
background: var(--color-text-danger);
}
}

&--info {
background: var(--color-surface-info);
color: var(--color-text-info);

.usage-badge__dot {
background: var(--color-text-info);
}
}

&--neutral {
background: var(--color-surface-muted);
color: var(--color-text-secondary);

.usage-badge__dot {
background: var(--color-text-secondary);
}
}
}
Loading
Loading