UsagePanel
Per-project consumption grid with plan gating and metering-lag honesty.
UsagePanel is the workspace’s usage tab: a grid of MetricCards over the v2 per-project consumption metrics — compute as CU-hours, storage and egress as bytes — under a mono header with the billing period — or a DateRangePicker in the filter slot when the range should be the user’s call. Cards enter one at a time in metric order. A plan-gated metric keeps its card frame but holds its voice: mono “available on a paid plan” with a quiet upgrade pill — an invitation, not a wall. And when metering trails real time, the footer says exactly how far, because the numbers shouldn’t pretend to be live.
usage
metered through 21:40 UTC · ~15m behind
Install
npx shadcn@latest add https://ui.neon.com/r/usage-panel.json
Usage
import { UsagePanel } from "@/components/neon-ui/usage-panel";
<UsagePanel
metrics={[
{
id: "compute_unit_seconds",
label: "Compute",
value: 22.3,
unit: "CU-hrs",
},
{
id: "storage_bytes",
label: "Storage",
value: 3_380_000_000,
format: "bytes",
},
{
id: "egress_bytes",
label: "Egress",
value: 731_000_000,
format: "bytes",
},
{
id: "branch_compute",
label: "Branch compute",
value: 0,
gated: plan === "free",
},
]}
period="Jul 1 – Jul 18"
meteredThrough="metered through 21:40 UTC · ~15m behind"
onUpgrade={() => openUpgrade()}
/>;
Each metric accepts the full MetricCard surface: delta, comparisonLabel, trend, format, unit.
Wiring to Neon
One consumption call fills every card. Sum the billing period’s timeframes into a metric each:
import { createNeonClient } from "@/lib/neon-client";
const neon = createNeonClient(process.env.NEON_API_KEY);
const { data } = await neon.consumption
.perProject({
from: periodStart.toISOString(),
to: now.toISOString(),
granularity: "daily",
project_ids: [projectId],
})
.all();
const timeframes =
data?.[0]?.periods.flatMap((period) => period.consumption) ?? [];
const computeSeconds = timeframes.reduce(
(sum, frame) => sum + frame.compute_time_seconds,
0
);
// storage: synthetic_storage_size_bytes, writes: written_data_bytes
Compute arrives in seconds, so divide by 3600 for CU-hours; storage and writes are bytes, which format: "bytes" renders. Storage is a point-in-time size rather than a total, so read the newest timeframe instead of summing. Network egress is not in this response: use neon.consumption.perProjectV2 with the public_network_transfer_bytes metric when you need an egress card. Neon meters on a delay, so state it in meteredThrough rather than letting a low number read as a bug. Gate a metric with gated when the account’s plan does not include it.
States
- Gated —
gated: trueswaps the value for the plan gate inside the same frame;onUpgraderenders the pill. - Metering lag —
meteredThroughrenders the mono footer notice. - Loading —
isLoadingskeletons every card through MetricCard. - Error —
errorrenders the house error line above the grid while cards hold their last values.
Props
metricsUsageMetric[]
Display order; id, label, value plus the MetricCard surface and an optional gated flag.
UsageMetric[]period?string
Billing period readout, e.g. "Jul 1 – Jul 18".
stringfilter?ReactNode
Header filter slot, e.g. a DateRangePicker; takes the period readout's place.
ReactNodemeteredThrough?string
Metering-lag notice rendered as a quiet mono footer.
stringonUpgrade?() => void
Renders the upgrade action on gated metrics.
() => voidisLoading?boolean
booleanfalseerror?string | null
Panel-level failure rendered above the grid.
string | nullfooter?ReactNode
Extra content after the lag notice, e.g. an invoices link.
ReactNodeAccessibility
- The panel is a
sectionwith a heading; the grid is plain flow content — cards carry their own labels. - Gated cards state the constraint in text; the upgrade action is a real button.
- The error line is
role="alert"; loading states ride MetricCard’saria-busy. - Entrance stagger is static under
prefers-reduced-motion.