g1t/apps/sudo/app/components/costs-header.tsx
Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| Costs: Cloudflare's subscriptions read from Cloudflare each day, the estimate only until then; sudo's costs split into Costs & margin and Bill & pricing | 1 | /** |
| 2 | * What both costs pages open with: the title, the run button, the two | |
| 3 | * tabs, the range, and what needs attention. | |
| 4 | */ | |
| 5 | import { Link } from "react-router"; | |
| 6 | ||
| 7 | import type { CostsReport } from "@g1t/contracts"; | |
| 8 | ||
| 9 | import { Button, Notice, PageHeader } from "~/components/ui"; | |
| 10 | ||
| 11 | export const RANGES = [7, 30, 90]; | |
| 12 | ||
| 13 | export function chip(active: boolean) { | |
| 14 | return `inline-flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs whitespace-nowrap transition-colors ${ | |
| 15 | active ? "border-merged/50 bg-merged/10 text-merged" : "border-line text-muted hover:border-line-strong hover:text-fg" | |
| 16 | }`; | |
| 17 | } | |
| 18 | ||
| 19 | /** A costs page's address with its range (and, on Costs & margin, its product). */ | |
| 20 | export function costsHref(page: "costs" | "bill", range: number, bucket: string | null = null) { | |
| 21 | const params = new URLSearchParams(); | |
| 22 | if (range !== 30) params.set("days", String(range)); | |
| 23 | if (bucket && page === "costs") params.set("product", bucket); | |
| 24 | const query = params.toString(); | |
| 25 | const path = page === "costs" ? "/costs" : "/costs/bill"; | |
| 26 | return query ? `${path}?${query}` : path; | |
| 27 | } | |
| 28 | ||
| 29 | const TABS = [ | |
| 30 | { page: "costs", title: "Costs & margin" }, | |
| 31 | { page: "bill", title: "Bill & pricing" }, | |
| 32 | ] as const; | |
| 33 | ||
| 34 | export function CostsHeader({ | |
| 35 | page, | |
| 36 | range, | |
| 37 | report, | |
| 38 | done, | |
| 39 | runError, | |
| 40 | description, | |
| 41 | }: { | |
| 42 | page: "costs" | "bill"; | |
| 43 | range: number; | |
| 44 | report: CostsReport | null; | |
| 45 | done: string | null; | |
| 46 | runError: string | null; | |
| 47 | description: string; | |
| 48 | }) { | |
| 49 | // Margin under the floor at the top of Costs & margin; drift and leaks | |
| 50 | // at the top of Bill & pricing; workspaces on Reach out. | |
| 51 | const alerts = report?.alerts ?? []; | |
| 52 | const here = alerts.filter((a) => (page === "costs" ? a.kind === "overall" || a.kind === "margin" : a.kind === "drift" || a.kind === "leak")); | |
| 53 | const drift = alerts.filter((a) => a.kind === "drift" || a.kind === "leak").length; | |
| 54 | const workspaces = alerts.filter((a) => a.kind === "workspace").length; | |
| 55 | const margin = alerts.filter((a) => a.kind === "overall" || a.kind === "margin").length; | |
| 56 | return ( | |
| 57 | <> | |
| 58 | <PageHeader title={TABS.find((t) => t.page === page)!.title} description={description} actions={<RunButton />} /> | |
| 59 | <nav aria-label="Costs" className="mt-5 flex flex-wrap gap-x-5 gap-y-2 border-b border-line"> | |
| 60 | {TABS.map((tab) => ( | |
| 61 | <Link | |
| 62 | key={tab.page} | |
| 63 | to={costsHref(tab.page, range)} | |
| 64 | aria-current={tab.page === page ? "page" : undefined} | |
| 65 | className={`-mb-px border-b-2 pb-2 text-sm ${tab.page === page ? "border-merged text-fg" : "border-transparent text-muted hover:text-fg"}`} | |
| 66 | > | |
| 67 | {tab.title} | |
| 68 | </Link> | |
| 69 | ))} | |
| 70 | </nav> | |
| 71 | <nav aria-label="Range" className="mt-4 flex flex-wrap gap-2"> | |
| 72 | {RANGES.map((days) => ( | |
| 73 | <Link key={days} to={costsHref(page, days)} aria-current={days === range ? "page" : undefined} className={chip(days === range)}> | |
| 74 | Last {days} days | |
| 75 | </Link> | |
| 76 | ))} | |
| 77 | </nav> | |
| 78 | <div className="mt-5 space-y-3 empty:hidden"> | |
| 79 | {done && <Notice tone="ok">{done}</Notice>} | |
| 80 | {runError && <Notice tone="error">{runError}</Notice>} | |
| 81 | {report && !report.configured && ( | |
| 82 | <Notice tone="warn"> | |
| 83 | Billing cannot read Cloudflare's bill: set the <code className="font-mono">CLOUDFLARE_BILLING_TOKEN</code> secret on g1t-billing (Account: | |
| 84 | Billing Read and Account Analytics Read). Until then the figures are g1t's own, and Cloudflare's cost shows as nothing. | |
| 85 | </Notice> | |
| 86 | )} | |
| 87 | {here.map((alert) => ( | |
| 88 | <Notice key={alert.id} tone={alert.kind === "drift" ? "warn" : "error"}> | |
| 89 | {alert.detail} <span className="text-faint">Since {alert.since}.</span> | |
| 90 | </Notice> | |
| 91 | ))} | |
| 92 | {page === "costs" && (drift > 0 || workspaces > 0) && ( | |
| 93 | <p className="text-xs text-muted"> | |
| 94 | Also open:{" "} | |
| 95 | {drift > 0 && ( | |
| 96 | <Link to={costsHref("bill", range) + "#drift"} className="underline underline-offset-2"> | |
| 97 | {drift} on Bill & pricing | |
| 98 | </Link> | |
| 99 | )} | |
| 100 | {drift > 0 && workspaces > 0 && ", "} | |
| 101 | {workspaces > 0 && ( | |
| 102 | <Link to="/reach-out?kind=cost_over_revenue" className="underline underline-offset-2"> | |
| 103 | {workspaces} workspace{workspaces === 1 ? "" : "s"} on Reach out | |
| 104 | </Link> | |
| 105 | )} | |
| 106 | . | |
| 107 | </p> | |
| 108 | )} | |
| 109 | {page === "bill" && margin > 0 && ( | |
| 110 | <p className="text-xs text-muted"> | |
| 111 | Also open:{" "} | |
| 112 | <Link to={costsHref("costs", range)} className="underline underline-offset-2"> | |
| 113 | margin under the floor on Costs & margin | |
| 114 | </Link> | |
| 115 | . | |
| 116 | </p> | |
| 117 | )} | |
| 118 | </div> | |
| 119 | </> | |
| 120 | ); | |
| 121 | } | |
| 122 | ||
| 123 | /** | |
| 124 | * The button that runs the nightly analysis now. sudo runs no JavaScript, | |
| 125 | * so app.css's run-button rules say it is running while the page waits. | |
| 126 | */ | |
| 127 | function RunButton() { | |
| 128 | return ( | |
| 129 | <form method="post" className="flex items-center gap-3"> | |
| 130 | <input type="hidden" name="intent" value="run" /> | |
| 131 | <Button variant="quiet" type="submit" className="run-button"> | |
| 132 | <span className="run-idle">Run the analysis now</span> | |
| 133 | <span className="run-busy"> | |
| 134 | <span className="run-spinner" aria-hidden="true" /> | |
| 135 | Running the analysis… | |
| 136 | </span> | |
| 137 | </Button> | |
| 138 | <span className="run-note text-xs text-muted">Reading the bill and reconciling 31 days; about a minute.</span> | |
| 139 | </form> | |
| 140 | ); | |
| 141 | } |