Usage dashboard
NewThe Agents screen of a team’s coding-agent app: a heatmap of every agent’s fullest plan window, a table of usage bars and lines, and a details panel.
usage-dashboard/usage-dashboard.tsxSubscription usage
Fullest share of each plan window per half hour
Fullest plan window, every agent
| Agent | Runs on | Usage | Last 24h | Details |
|---|---|---|---|---|
| Ember CLI | Self-hosted | 5-hour5-hour window 0%WeeklyWeekly window 18% | ||
| Ember CLI | Cloud | 5-hour5-hour window 17% | ||
| Ember CLI | Cloud | 5-hour5-hour window 27% | ||
| Ember CLI | Cloud | 5-hour5-hour window 4% | ||
| Ember CLI | Cloud | 5-hour5-hour window 0% | ||
| Ember CLI | Cloud | 5-hour5-hour window 13%WeeklyWeekly window 78% | ||
| Ember CLI | Cloud | HTTP 429 | ||
| Knot CLI | Cloud | 5-hour5-hour window 9%WeeklyWeekly window 22% | ||
| Knot CLI | Cloud | 5-hour5-hour window 6% | ||
| Knot CLI | Cloud | 5-hour5-hour window 41%WeeklyWeekly window 64% | ||
| Knot CLI | Cloud | 5-hour5-hour window 3% |
The live component, numbered part by part. Every measurement is read from it as it is drawn. Point at a row to outline its part, or at a part to find its row.
When to use
- A team’s pool of seats, keys or agents, each with a quota that resets on a window, where the question is who is close to the limit and when.
- One reading per item per time slot, best read as a grid: a heatmap shows quiet hours, bursts and hits at the limit across every row at a glance.
- A list that pairs the overview with the current numbers: bars for the windows now, a small line for the trend, and a details panel per row.
- An app screen with its own navigation: the sidebar, the chats and the workspace switcher come with it, and fold away when the data needs the room.
When not to use
- Billing or spend in money. Use a cost dashboard with totals, a period picker and invoices; shares of a window say nothing about price.
- A single agent or key. Show its own page with its windows and resource charts, as the details panel does, without the heatmap.
- Readings finer than half an hour, or live streams. Use a time-series chart with zoom; the grid merges cells, it never adds them.
- Hundreds of rows. The heatmap stops reading past a screenful; group by team or plan, or rank the fullest and page the rest.
- Alerting. A row at the limit only turns orange; send limit warnings as notifications, not as a screen someone has to watch.
You can add this component with the shadcn CLI or by hand:
Into components/usage-dashboard of any React project, with no setup:
npx shadcn@latest add https://brunodelorence.com/r/usage-dashboard.json- Copy the component files
Copy these files into one folder of your project, keeping their relative imports together, or download them as a ZIP:
- Import it
As shown under Usage, below.
Either way it needs React 19 and a bundler that supports CSS Modules, such as Next.js or Vite. It has a dark colour scheme: it follows your page’s color-scheme, light unless the page sets one, or its colorScheme prop, with nothing to configure. Browsers without light-dark() show it light.
Usage
import { UsageDashboard } from './usage-dashboard';export function AgentsPage() { return ( <UsageDashboard workspace="Kestrel Labs" user={{ name: 'Noa Castell' }} defaultRange="7d" onAgentOpen={(agent) => console.log('Opened', agent.name)} autoplay={false} /> );}Each state below is a live instance, locked so you can study it.
The pieces it is built from, each with its measurements read live.
Every coding agent of the workspace is signed in to a plan with a 5-hour and sometimes a weekly usage window. A heatmap shows each agent’s fullest window over the last day, week or month, violet by share and orange at the limit, with a tooltip on every cell; the table under it shows the windows as bars and the last readings as a line. A row opens the agent’s details beside it: windows, runtime details and resource charts. A sidebar with a workspace switcher, sections and chats with their tickets folds away. Light and dark.
- The heatmap is an ARIA grid of 49 cells a row (48 half hours and the current one, or 3½ or 15 hours for the week and the month). A cell’s colour is color-mix(in oklab) between two tokens by its reading, so one custom property per cell draws every shade in both schemes; a reading at the limit is orange and a missing one a flat grey.
- The cells stretch to the card. When they would get narrower than 12.5 px the row is merged into fewer columns, each the fullest of the half hours it covers (the current cell stays alone), and the axis keeps only the labels that still fit, so a narrow card keeps readable squares instead of scrolling.
- Hovering or focusing a cell places one tooltip from the cell’s box (below it in the first rows, above it further down) and lights the agent’s name; the arrow keys, Home and End walk the grid and Enter opens the agent. Hovering a legend swatch fades every cell outside that band.
- 24h / 7d / 30d switch the readings, the axis and the table’s line together. The last day is written out by hand; the week and the month are a seeded random walk per agent, so they are the same on every load.
- Each usage bar is a full-width fill slid by translateX to its share, never narrower than a dot, so 4% still shows and a new value glides. The lines and the resource charts are SVG paths in a box of their own units, stretched to their slot with a stroke that does not scale; a missing reading breaks the line.
- A row opens the details panel: beside the main column from 1100 px, over it with a scrim below. Its menu restarts, copies the ID of or removes the agent, with Undo in a toast; Add agent opens a modal dialog, and the new agent waits for its sign-in with an empty row in the heatmap. Agents ⌄ sorts both lists and the toolbar’s field (or ⌘K) filters them.
- The window bar’s button folds the sidebar away: it slides out by transform while the toolbar and the content slide by its width with the Web Animations API. Under 720 px it is a drawer behind a menu button; the table drops its runtime, host and line columns when the main column is narrow, and on a phone the usage goes under each name.
- While on screen and untouched, a demo pointer hovers three cells and the At limit band, switches to the week and back, opens an agent and its resources over six hours, starts adding an agent and folds the sidebar. Any real input hands it over; it comes back after 9 s of quiet.
- Every colour is a light-dark() token on the root: a near-black window with a cooler sidebar and cards a shade darker than the page by night, a pale grey sidebar and white cards by day, the same violet a shade deeper.
| Prop | Type | Default | Description |
|---|---|---|---|
agents | Agent[] | a team of eleven | The agents listed: name, runtime, host, version, 5-hour and weekly usage (0–100), an error or a pending sign-in, and the runtime details the panel shows. |
defaultRange | '24h' | '7d' | '30d' | '24h' | The heatmap’s range at first. |
workspace | string | 'Kestrel Labs' | The workspace’s name in the switcher. |
user | { name: string } | { name: 'Noa Castell' } | Who is signed in, at the foot of the sidebar. |
onAgentOpen | (agent: Agent) => void | — | Called when an agent’s details open. |
onAddAgent | (agent: Agent) => void | — | Called with the new agent when the Add agent dialog is sent. |
defaultState | UsageDashboardState | — | Opens in this moment, held still: range, selected (an agent’s id, its details open), cell ({ agent, index }, a heatmap cell with its tooltip), collapsed (the sidebar folded away) and adding (the Add agent dialog open). Turns the demo off. |
autoplay | boolean | true | While on screen and untouched, a demo pointer shows the main interactions. Any real input stops it. |
colorScheme | 'light' | 'dark' | 'system' | — | Light or dark colours, or the reader’s; without it, the page’s color-scheme. |
className | string | — | Extra class for the root. |
The complete source behind this demo. Open source under the MIT license, © 2026 Bruno Delorence: copy it, change it, ship it.
// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';/* * Usage dashboard: the Agents screen of a team's coding-agent app, where every agent is signed in * to a plan with a 5-hour (and sometimes a weekly) usage window. An app window with a sidebar * (workspace switcher, search, sections, pinned and shared items, chats with their tickets and pull * requests, the signed-in person), and a main column: a heatmap of the fullest plan window of * every agent over the last day, week or month, and a table of agents with their windows as bars * and their last readings as a line. A row opens the agent's details in a side panel. * * - The heatmap is a grid: hovering or focusing a cell shows its tooltip (agent, span, reading), * the arrow keys walk it and Enter opens that agent. Its cells stretch to the card; when they * would get narrower than 12.5 px they merge into fewer columns (the fullest wins), so a narrow card keeps * readable squares. Hovering a legend swatch lights only the cells of that band. * - 24h / 7d / 30d switch the readings (49 cells: half hours, 3½ hours or 15 hours), the axis and * the table's line. Agents ⌄ sorts both lists, the toolbar's field filters them. * - A row opens the details panel: plan windows, runtime details and resource charts with their * own 1h / 6h / 24h, and a menu to restart, copy the ID of or remove the agent (with undo). * Add agent opens a dialog; the new agent waits for its sign-in, with no readings yet. * - The sidebar folds away (and slides back) from the window bar; on a phone it is a drawer. * - While on screen and untouched, a demo pointer hovers cells and the legend, switches the range, * opens an agent and its resource range, starts adding an agent and folds the sidebar. Any real/* Usage dashboard (usage-dashboard.tsx): a near-black app window, its sidebar a shade cooler than the main column, cards a shade darker than the page they sit on, violet for usage and a burnt orange for an agent at its limit; by day a pale grey sidebar, a white page and the same violet, a shade deeper. Colours are the tokens below, light-dark(light, dark) where the two differ: the component takes the color-scheme it inherits from the page (light unless the page says otherwise), or the one its colorScheme prop sets (data-color-scheme). Sizes are those of the app at its own scale: a 267 px sidebar, 28 px rows in it, a 38 px window bar, 14 px text. Layout comes from the component's own width (container queries) and the main column's (the table's columns): the details panel beside the main column from 1100 px and over it below, the sidebar a drawer under 720 px. Memory. The sidebar folds and the panel, menus, tooltip and dialog come in by transform and opacity; the heatmap's cells are plain boxes with a colour, so a range switch only repaints. */.root { --frame-edge: #d9d9de; --sidebar: #f3f3f5; --page: #fbfbfc; --card: #fff; --card-edge: #e6e6ea; --line: #ececf0;// Copyright (c) 2026 Bruno Delorence. MIT License./* * The small charts: a row's last-day line (a sparkline with gaps where nothing was read) and the * details panel's resource charts (a line, two areas and a filled bar under a dashed limit). All * are SVG drawn in a box of their own units and stretched to their slot with a stroke that does * not scale, so they stay crisp at any width. They are decorative: their figures are in the text * beside them. */import styles from './usage-dashboard.module.css';const W = 134;const H = 30;/** The path of `values` (0–1, null for a gap) across a W × H box, `pad` units off top and bottom. */function line(values: (number | null)[], width = W, height = H, pad = 2) { const step = width / Math.max(1, values.length - 1); let d = ''; let open = false; values.forEach((value, index) => { if (value === null) { open = false; return;// Copyright (c) 2026 Bruno Delorence. MIT License./* * The dashboard's made-up data: a workspace of coding agents, each signed in to a plan with a * 5-hour and sometimes a weekly usage window, and what each window read every half hour. * * A reading is the share of the agent's fullest plan window, 0 to 1 (1 is at the limit), or null * when the agent sent nothing. The last day is written out by hand, one character a cell: '.' no * reading, 'X' at the limit, and 0–9 then a–j for twentieths. The week and the month are worked out * from a seeded random walk per agent, so they are the same on every load. */export type Range = '24h' | '7d' | '30d';export type Runtime = 'ember' | 'knot';export type Host = 'cloud' | 'self';export type Agent = { id: string; name: string; runtime: Runtime; host: Host; version: string; /** Share of the 5-hour window used, 0–100. */ fiveHour: number;// Copyright (c) 2026 Bruno Delorence. MIT License./* * The dashboard's icons, drawn on a 24-unit grid with round 1.7-unit strokes. They are decorative * (aria-hidden); their size comes from the stylesheet, their colour from currentColor. The two * runtime marks are filled shapes in their own colours. */import type { ReactNode } from 'react';function Icon({ children, className }: { children: ReactNode; className?: string }) { return ( <svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true" focusable="false" > {children}// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import type { CSSProperties, Ref } from 'react';/* * The demo's pointer, and the steps that move it. A demo is a script of awaited steps: wait, or * glide the pointer somewhere. Stopping the demo (the reader takes over, or it scrolls away) ends * the step in progress at once: its promise rejects, which ends the script right there, and the * pointer fades out. * * Each glide is a curved flight played by the Web Animations API, so nothing runs frame by frame. * The points of the flight are handed to whoever wants to move along with it. The arrow and the hand are HTML boxes * that swap by their visibility: a change of style inside an SVG would lay the SVG out again. */export type Point = { x: number; y: number };/** A glide's path and timing, for an animation that plays along with it. */export type Flight = { points: Point[]; timing: KeyframeAnimationOptions };export type Demo = { /** Resolves after `ms`. */// Copyright (c) 2026 Bruno Delorence. MIT License.'use client';import { useCallback, useEffect, useRef, useState } from 'react';/* * When a demo may play on its own: while part of it is on screen and the tab is visible, never * under reduced motion, and not while the reader is using it. `take()` hands it to the reader; it * takes back over `idle` ms after their last input. */export function useAutoplay<T extends HTMLElement>(idle = 6000) { const ref = useRef<T>(null); const [visible, setVisible] = useState(false); const [reduced, setReduced] = useState(true); const [taken, setTaken] = useState(false); const timer = useRef(0); useEffect(() => { const element = ref.current; if (!element) return; let onScreen = false; const update = () => setVisible(onScreen && !document.hidden); const observer = new IntersectionObserver(- Landmarks and names: the sidebar is a navigation with aria-current on its section, the page a main with a heading, the details panel a complementary region named after the agent. The window bar’s toggle is a disclosure for the sidebar (a drawer on a phone); Pinned, Shared, Chats and chats with threads are disclosures too.
- The heatmap is a grid with one tab stop: each row has its agent as a row header, each cell is named with its span and reading (“Tomas (6.25x), 06:53 PM – 07:23 PM: At limit”), the arrows, Home and End move and Enter opens the agent. The legend’s swatches are toggle buttons.
- The ranges are radio groups (arrows move). The workspace, sort, help and agent menus close on Escape or a press elsewhere. Add agent is a modal dialog that traps the focus, closes on Escape and gives the focus back to its button; its choices are real radio buttons.
- Each table row opens through its agent’s name, a button stretched over the row with aria-expanded; the usage figures carry their window’s name for screen readers. Range and sort changes, removals, restarts and copies are announced in a polite live region.
- The lines and the resource charts are drawn for the eye; their figures are beside them. Under reduced motion nothing slides and the demo never plays.