Skip to content

g1t/apps/web/app/routes/workspace/usage.tsx

146 lines5,631 bytesCodeBlame

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.

Usage, Billing settings and prepaid AI credit; fixes from the UX audit1import { Suspense } from "react";
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens2import { Await, Link, data } from "react-router";
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request3
Usage, Billing settings and prepaid AI credit; fixes from the UX audit4import { PRODUCTS, type UsageReport } from "@g1t/contracts";
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request5
6import type { Route } from "./+types/usage";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit7import { Breakdown, IncludedAndCredit, UsageChart, UsageFilterBar, type UsageFilters, UsageSkeleton } from "../../components/usage";
8import { columns, defaultGrain, type Grain, type GroupBy, resolveRange } from "../../lib/usage";
Social cards for every page: og.g1t.sh9import { page } from "../../lib/meta";
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request10import { billing } from "../../lib/services.server";
Usage, Billing settings and prepaid AI credit; fixes from the UX audit11import { getViewer, roleIn } from "../../lib/session.server";
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request12
Usage, Billing settings and prepaid AI credit; fixes from the UX audit13/** Where the usage API is documented. */
14export const USAGE_API = "https://docs.g1t.sh/reference/api/#usage";
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request15
Social cards for every page: og.g1t.sh16export function meta({ params, ...args }: Route.MetaArgs) {
17 return page(args, { title: `Usage · ${params.owner} · g1t` });
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request18}
19
Usage, Billing settings and prepaid AI credit; fixes from the UX audit20/** The page's filters from its address: a filtered page is a link. */
21export function readFilters(url: URL, fixedProject?: string): UsageFilters {
22 const q = url.searchParams;
23 const { period, from, until } = resolveRange(q.get("period"), { from: q.get("from"), until: q.get("until") });
24 const known = new Set<string>(PRODUCTS.map((p) => p.key));
25 const grain = q.get("grain");
26 const group = q.get("group");
27 return {
28 period,
29 from,
30 until,
31 products: q.getAll("product").filter((p) => known.has(p)),
32 projects: fixedProject ? [fixedProject] : q.getAll("project").filter((p) => /^[a-z0-9._-]+\/[a-z0-9._-]+$/i.test(p)).map((p) => p.toLowerCase()),
33 group: group === "project" && !fixedProject ? "project" : group === "day" ? "day" : "product",
34 grain: grain === "day" || grain === "week" || grain === "month" ? grain : defaultGrain(from, until),
35 cumulative: q.get("cumulative") === "1",
36 };
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request37}
38
Usage, Billing settings and prepaid AI credit; fixes from the UX audit39/** The address with one filter changed. */
40export function hrefWith(url: string, change: Record<string, string | null>): string {
41 const next = new URL(url);
42 for (const [key, value] of Object.entries(change)) {
43 if (value == null) next.searchParams.delete(key);
44 else next.searchParams.set(key, value);
45 }
46 return `${next.pathname}?${next.searchParams.toString()}`;
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request47}
48
Usage, Billing settings and prepaid AI credit; fixes from the UX audit49export async function loader({ params, context, request }: Route.LoaderArgs) {
50 const viewer = getViewer(context);
51 const role = roleIn(viewer, params.owner);
52 if (!role) throw data(null, { status: 404 });
53 const slug = params.owner.toLowerCase();
54 const url = new URL(request.url);
55 const filters = readFilters(url);
56 // Streamed: the page's frame and filters come at once, each region's
57 // skeleton holds its place, and the report fills them in.
58 const report = billing
59 .usageReport(slug, viewer, { from: filters.from, until: filters.until, products: filters.products, projects: filters.projects })
60 .then((result) => (result.ok ? { report: result.value, error: null } : { report: null, error: result.error.message }));
61 return { slug, owner: role === "owner", filters, url: url.toString(), report };
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request62}
63
Usage, Billing settings and prepaid AI credit; fixes from the UX audit64export function UsageView({
65 slug,
66 owner,
67 filters,
68 url,
69 report,
70 error,
71 fixedProject,
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request72}: {
Usage, Billing settings and prepaid AI credit; fixes from the UX audit73 slug: string;
74 owner: boolean;
75 filters: UsageFilters;
76 url: string;
77 report: UsageReport | null;
78 error: string | null;
79 fixedProject?: string;
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request80}) {
Usage, Billing settings and prepaid AI credit; fixes from the UX audit81 const bar = (
82 <UsageFilterBar
83 filters={filters}
84 products={[...PRODUCTS]}
85 projects={report?.projects ?? filters.projects}
86 report={report}
87 apiHref={USAGE_API}
88 fixedProject={fixedProject}
89 />
90 );
91 if (!report) {
92 return (
93 <div className="space-y-6">
94 {bar}
95 <p className="rounded-lg border border-danger/40 bg-danger/5 px-4 py-2.5 text-sm">{error ?? "Usage could not be read. Try again in a minute."}</p>
96 </div>
97 );
98 }
99 const cols = columns(report.days, report.from, report.until, filters.grain as Grain, filters.cumulative);
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request100 return (
Usage, Billing settings and prepaid AI credit; fixes from the UX audit101 <div className="space-y-6">
102 {bar}
103 {!fixedProject && <IncludedAndCredit report={report} billingHref={`/${slug}/-/billing`} owner={owner} />}
104 <UsageChart
105 columns={cols}
106 products={report.products}
107 grain={filters.grain}
108 cumulative={filters.cumulative}
109 grainHref={(grain) => hrefWith(url, { grain })}
110 cumulativeHref={hrefWith(url, { cumulative: filters.cumulative ? null : "1" })}
111 />
112 <Breakdown report={report} group={filters.group as GroupBy} columns={cols} projectHref={fixedProject ? undefined : (project) => `/${project}/usage`} />
113 <p className="text-xs text-faint">
114 Every amount is usage at price: what was charged, plus what included usage, credit or a discount paid for it. Storage, git
115 operations, scans and search are metered through the month and charged when it closes.
Merge the AI Gateway: Anthropic's Messages API on a workspace's tokens116 {!fixedProject && (
117 <>
118 {" "}
119 Each request your own code sent through the AI Gateway is listed on{" "}
120 <Link to={`/${slug}/-/gateway`} className="hover:text-fg hover:underline">
121 AI Gateway
122 </Link>
123 .
124 </>
125 )}
Usage, Billing settings and prepaid AI credit; fixes from the UX audit126 </p>
127 </div>
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request128 );
129}
130
Usage, Billing settings and prepaid AI credit; fixes from the UX audit131export default function UsagePage({ loaderData }: Route.ComponentProps) {
132 const { slug, owner, filters, url, report } = loaderData;
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request133 return (
Usage, Billing settings and prepaid AI credit; fixes from the UX audit134 <Suspense
135 fallback={
136 <UsageSkeleton>
137 <UsageFilterBar filters={filters} products={[...PRODUCTS]} projects={filters.projects} report={null} apiHref={USAGE_API} />
138 </UsageSkeleton>
139 }
140 >
141 <Await resolve={report}>
142 {(loaded) => <UsageView slug={slug} owner={owner} filters={filters} url={url} report={loaded.report} error={loaded.error} />}
143 </Await>
144 </Suspense>
Usage, like a hosting provider's: what agents cost, per day, task, repository and pull request145 );
146}

This file's history is long; its oldest lines are credited to the oldest commit read.