Commit

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.

syntaqxcommitted Parent5916a7eBrowse files
3 files+36−30/3 viewed
+17−0
22 import { test } from "node:test";
33
44 import {
5+ foldTasks,
56 gigabytes,
67 shownMeters,
78 alertText,
234235 assert.equal(gigabytes(1_500_000_000), "1.5 GB");
235236 assert.equal(gigabytes(500_000_000), "500 MB");
236237 });
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+});
+15−0
270270 return USAGE_TASKS[key] ?? USAGE_TASKS.other!;
271271 }
272272
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+
273288 /** The workspace's month at a glance, for the Usage card on its overview. */
274289 export type UsageGlance = {
275290 /**
+4−3
44 import { MICROS_PER_DOLLAR, type UsageSlice } from "@g1t/contracts";
55
66 import type { Route } from "./+types/usage";
7−import { usageTask } from "../../lib/billing";
7+import { foldTasks, usageTask } from "../../lib/billing";
88 import { page } from "../../lib/meta";
99 import { ButtonLink } from "../../components/ui";
1010 import { billing } from "../../lib/services.server";
181181
182182 export default function UsagePage({ loaderData, params }: Route.ComponentProps) {
183183 const { period, since, usage, account } = loaderData;
184+ const byTask = foldTasks(usage.byTask);
184185 const base = `/${params.owner}`;
185186 const days = Math.max(1, Math.ceil((Date.now() - new Date(since).getTime()) / 86_400_000));
186187 // While g1t is free nothing is charged, so usage is measured at cost
257258 <div className="flex flex-wrap items-baseline justify-between gap-3">
258259 <h3 className="text-sm font-medium">{usage.free ? "Usage per day" : "Spend per day"}</h3>
259260 <ul className="flex flex-wrap gap-x-4 gap-y-1 text-xs text-muted">
260− {usage.byTask.map((slice) => (
261+ {byTask.map((slice) => (
261262 <li key={slice.key} className="flex items-center gap-1.5">
262263 <span className="size-2 rounded-sm" style={{ background: task(slice.key).color }} />
263264 {task(slice.key).label}
273274 <div className="grid gap-4 lg:grid-cols-2">
274275 <Breakdown
275276 title="By kind of work"
276− slices={usage.byTask}
277+ slices={byTask}
277278 total={total}
278279 label={(key) => task(key).label}
279280 color={(key) => task(key).color}