Usage lists Other once: kinds of work without words of their own are added into it
Billing's remainder and every task the page has no label for each showed as its own Other row, so flagon-io's usage listed Other twice, in the breakdown and in the chart's legend.
3 files+36−30/3 viewed
| 2 | 2 | import { test } from "node:test"; | |
| 3 | 3 | ||
| 4 | 4 | import { | |
| 5 | + | foldTasks, | |
| 5 | 6 | gigabytes, | |
| 6 | 7 | shownMeters, | |
| 7 | 8 | alertText, | |
| 234 | 235 | assert.equal(gigabytes(1_500_000_000), "1.5 GB"); | |
| 235 | 236 | assert.equal(gigabytes(500_000_000), "500 MB"); | |
| 236 | 237 | }); | |
| 238 | + | ||
| 239 | + | test("kinds of work without words of their own are one Other, listed once and last", () => { | |
| 240 | + | const slices = [ | |
| 241 | + | { key: "check", micros: 5, runs: 50 }, | |
| 242 | + | { key: "review", micros: 3, runs: 5 }, | |
| 243 | + | { key: "other", micros: 1, runs: 7 }, | |
| 244 | + | { key: "implement", micros: 9, runs: 4 }, | |
| 245 | + | { key: "answer", micros: 2, runs: 11 }, | |
| 246 | + | ]; | |
| 247 | + | assert.deepEqual(foldTasks(slices), [ | |
| 248 | + | { key: "review", micros: 3, runs: 5 }, | |
| 249 | + | { key: "implement", micros: 9, runs: 4 }, | |
| 250 | + | { key: "other", micros: 8, runs: 68 }, | |
| 251 | + | ]); | |
| 252 | + | assert.deepEqual(foldTasks([{ key: "plan", micros: 1, runs: 1 }]), [{ key: "plan", micros: 1, runs: 1 }]); | |
| 253 | + | }); |
| 270 | 270 | return USAGE_TASKS[key] ?? USAGE_TASKS.other!; | |
| 271 | 271 | } | |
| 272 | 272 | ||
| 273 | + | /** | |
| 274 | + | * Usage by kind of work with every kind that has no words of its own | |
| 275 | + | * added into one Other, last, so Other is listed once. | |
| 276 | + | */ | |
| 277 | + | export function foldTasks<T extends { key: string; micros: number; runs: number }>(slices: T[]): T[] { | |
| 278 | + | const known = slices.filter((slice) => slice.key !== "other" && slice.key in USAGE_TASKS); | |
| 279 | + | const rest = slices.filter((slice) => !known.includes(slice)); | |
| 280 | + | if (rest.length === 0) return known; | |
| 281 | + | const other = rest.reduce( | |
| 282 | + | (sum, slice) => ({ ...sum, micros: sum.micros + slice.micros, runs: sum.runs + slice.runs }), | |
| 283 | + | { ...rest[0]!, key: "other", micros: 0, runs: 0 }, | |
| 284 | + | ); | |
| 285 | + | return [...known, other]; | |
| 286 | + | } | |
| 287 | + | ||
| 273 | 288 | /** The workspace's month at a glance, for the Usage card on its overview. */ | |
| 274 | 289 | export type UsageGlance = { | |
| 275 | 290 | /** |
| 4 | 4 | import { MICROS_PER_DOLLAR, type UsageSlice } from "@g1t/contracts"; | |
| 5 | 5 | ||
| 6 | 6 | import type { Route } from "./+types/usage"; | |
| 7 | − | import { usageTask } from "../../lib/billing"; | |
| 7 | + | import { foldTasks, usageTask } from "../../lib/billing"; | |
| 8 | 8 | import { page } from "../../lib/meta"; | |
| 9 | 9 | import { ButtonLink } from "../../components/ui"; | |
| 10 | 10 | import { billing } from "../../lib/services.server"; | |
| 181 | 181 | ||
| 182 | 182 | export default function UsagePage({ loaderData, params }: Route.ComponentProps) { | |
| 183 | 183 | const { period, since, usage, account } = loaderData; | |
| 184 | + | const byTask = foldTasks(usage.byTask); | |
| 184 | 185 | const base = `/${params.owner}`; | |
| 185 | 186 | const days = Math.max(1, Math.ceil((Date.now() - new Date(since).getTime()) / 86_400_000)); | |
| 186 | 187 | // While g1t is free nothing is charged, so usage is measured at cost | |
| 257 | 258 | <div className="flex flex-wrap items-baseline justify-between gap-3"> | |
| 258 | 259 | <h3 className="text-sm font-medium">{usage.free ? "Usage per day" : "Spend per day"}</h3> | |
| 259 | 260 | <ul className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted"> | |
| 260 | − | {usage.byTask.map((slice) => ( | |
| 261 | + | {byTask.map((slice) => ( | |
| 261 | 262 | <li key={slice.key} className="flex items-center gap-1.5"> | |
| 262 | 263 | <span className="size-2 rounded-sm" style={{ background: task(slice.key).color }} /> | |
| 263 | 264 | {task(slice.key).label} | |
| 273 | 274 | <div className="grid gap-4 lg:grid-cols-2"> | |
| 274 | 275 | <Breakdown | |
| 275 | 276 | title="By kind of work" | |
| 276 | − | slices={usage.byTask} | |
| 277 | + | slices={byTask} | |
| 277 | 278 | total={total} | |
| 278 | 279 | label={(key) => task(key).label} | |
| 279 | 280 | color={(key) => task(key).color} |