AuroraMeter.Components (Aurora Meter v1.0.0)

View Source

Drop-in HEEx components for showing live usage and credit spend. Compiled only when Phoenix.Component is available (the LiveView deps are optional).

They are unstyled by design (BEM-style aurora-meter__*, aurora-spend-chart__* and aurora-credit-summary__* classes) so they inherit your app's look; the Pro dashboard ships a styled version. Nothing here declares a colour: the charts paint with currentColor, so they take the text colour your design system already set. There is no JavaScript: the chart is inline SVG and the tooltips are <title> elements. Pair with AuroraMeter.LiveView.subscribe/1 for live usage updates.

Money components render dollars through AuroraMeter.Credits.Money, never raw micro-dollars:

<.spend_chart points={AuroraMeter.Credits.spend_history(@org, days: 30)} />
<.credit_summary summary={AuroraMeter.Credits.summary(@org)} />

Summary

Functions

Renders the credit balance and burn from AuroraMeter.Credits.summary/1 as a description list.

Renders a spend-per-bucket bar chart from AuroraMeter.Credits.spend_history/2.

Renders a labelled usage bar for one feature.

Renders a usage meter for every feature in the tenant's plan.

Functions

credit_summary(assigns)

Renders the credit balance and burn from AuroraMeter.Credits.summary/1 as a description list.

Held, promotional and granted figures only appear when they are non-zero. daily_burn and runway_days render as "not yet" when they are nil. There is no honest number to show for a tenant that has not spent anything, and this component will not invent one.

Examples

<.credit_summary summary={AuroraMeter.Credits.summary(@org)} />

Attributes

  • summary (:map) (required)
  • Global attributes are accepted.

spend_chart(assigns)

Renders a spend-per-bucket bar chart from AuroraMeter.Credits.spend_history/2.

Every bucket in points gets a bar, including the empty ones: a zero-spend day is a baseline, never a gap. Buckets where credit was granted carry a marker above the bar when show_grants is set. Amounts in the axis labels and in the <title> tooltips are dollars, formatted with AuroraMeter.Credits.Money.format/2.

Examples

<.spend_chart points={AuroraMeter.Credits.spend_history(@org, days: 30)} />

<.spend_chart
  points={AuroraMeter.Credits.spend_history(@org, bucket: :month, days: 365)}
  label="Spend per month"
  height={160}
/>

Attributes

  • points (:list) (required)
  • height (:integer) - Defaults to 120.
  • label (:string) - Defaults to "Spend per day".
  • show_grants (:boolean) - Defaults to true.
  • Global attributes are accepted.

usage_meter(assigns)

Renders a labelled usage bar for one feature.

Hard caps show used / limit; metered features show used / included and flag overage; counters show the bare count with no bar (they have no denominator, ADR 0006); boolean features show whether they are enabled; integer features show their plan value.

Pass quota in a LiveView, tenant and feature anywhere else

# Live: updates when usage moves.
<.usage_meter quota={@quota} />

# Snapshot: reads the current value once, at render time.
<.usage_meter tenant={@org} feature={:ai_generations} />

The two forms are not interchangeable inside a LiveView, and the reason is change tracking rather than taste. LiveView re-renders a function component only when the assigns passed to it have changed. The tenant form's assigns are the tenant and the feature name, and neither of those moves when usage does, so a socket that is subscribed correctly, receives the broadcast and re-renders would keep showing the figure read at the first render. That is not a bar that lags; it is a bar that never moves again, and nothing on the page says so.

The quota form has no such gap, because the number is the assign: when usage changes the map changes, and change tracking re-renders for the ordinary reason. Fetch it with AuroraMeter.quota/2 in mount/3 and again wherever you handle the usage message:

def mount(_params, session, socket) do
  org = MyApp.Accounts.org_for_session!(session)
  if connected?(socket), do: AuroraMeter.LiveView.subscribe(org)
  {:ok, assign(socket, org: org, quota: AuroraMeter.quota(org, :ai_generations))}
end

def handle_info({:aurora_meter, :usage, _payload} = message, socket) do
  {:noreply, AuroraMeter.LiveView.assign_quota(message, socket, :quota, :ai_generations)}
end

AuroraMeter.LiveView.assign_quota/4 is that second line; it drops a message for another tenant and re-reads the quota for the socket's own.

Exactly one of quota, or tenant and feature, is required. Passing neither, or both, raises rather than guessing which one the caller meant.

Attributes

  • quota (:map) - A map from AuroraMeter.quota/2. The live form: prefer it in a LiveView. Defaults to nil.
  • tenant (:any) - Read the quota at render time. A snapshot. Defaults to nil.
  • feature (:atom) - Required with tenant. Defaults to nil.
  • label (:string) - Defaults to nil.
  • Global attributes are accepted.

usage_summary(assigns)

Renders a usage meter for every feature in the tenant's plan.

quotas and tenant divide exactly as they do in usage_meter/1, for the same change-tracking reason: pass quotas={@quotas} in a LiveView so the meters move, tenant={@org} anywhere a single snapshot is what is wanted. AuroraMeter.LiveView.quotas/1 builds the list and AuroraMeter.LiveView.assign_quotas/2 refreshes it from a usage message.

<.usage_summary quotas={@quotas} />
<.usage_summary tenant={@org} />

Attributes

  • quotas (:list) - A list of maps from AuroraMeter.quota/2. The live form. Defaults to nil.
  • tenant (:any) - Read every feature's quota at render time. Defaults to nil.
  • Global attributes are accepted.