Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

149 linesCodeBlameRaw
1import { ArrowRight, ChevronDown, CreditCard } from "lucide-react";
2import { useState } from "react";
3import { Link } from "react-router";
4
5import { ButtonLink } from "./ui";
6import { Button } from "./ui/button";
7import { Card } from "./ui/card";
8import { type UsageGlance, share, usageTask } from "../lib/billing";
9import { money, wholeDollars } from "../lib/money";
10
11/** Line items shown before Show more. */
12const FIRST_LINES = 3;
13
14/** What the card says the workspace is on, beside its spend. */
15const STANDING: Record<UsageGlance["kind"], string> = {
16 beta: "Free for now",
17 comped: "100% discount",
18 plan: "g1t plan",
19 trial: "Trial",
20 forge: "Free",
21};
22
23/**
24 * A thin meter of `used` against `of`. The trial warns near its end; the
25 * plan's included usage running out is ordinary, as on-demand takes over.
26 */
27function Bar({ used, of, warns }: { used: number; of: number; warns: boolean }) {
28 const part = share(used, of);
29 const tone = !warns ? "bg-success" : part >= 1 ? "bg-danger" : part >= 0.75 ? "bg-warn" : "bg-success";
30 return (
31 <span className="mt-1.5 block h-1 overflow-hidden rounded-full bg-line" role="presentation">
32 <span className={`block h-full rounded-full ${tone}`} style={{ width: `${Math.max(part * 100, part > 0 ? 2 : 0)}%` }} />
33 </span>
34 );
35}
36
37/**
38 * The workspace's month on its overview: what it has spent, what pays
39 * first and how much of it is left, what was charged past that, and what
40 * it went on, with the way to Billing.
41 */
42export function UsageCard({ slug, glance, owner }: { slug: string; glance: UsageGlance; owner: boolean }) {
43 const [all, setAll] = useState(false);
44 const lines = all ? glance.lines : glance.lines.slice(0, FIRST_LINES);
45 const more = glance.lines.length - FIRST_LINES;
46 return (
47 <Card asChild>
48 <section aria-labelledby="usage-card">
49 <div className="p-5 pb-4">
50 <div className="flex items-baseline justify-between gap-3">
51 <h2 id="usage-card" className="font-medium">
52 <Link to={`/${slug}/-/usage`} className="hover:underline">
53 Usage
54 </Link>
55 </h2>
56 <span className="text-xs text-faint">This month</span>
57 </div>
58 <p className="mt-3 flex items-baseline justify-between gap-3">
59 <span className="text-2xl font-semibold tracking-tight tabular-nums">{money(glance.spentMicros)}</span>
60 <span className={`rounded-full px-2 py-0.5 text-xs ${glance.kind === "forge" ? "border border-line text-muted" : "bg-accent/15 text-accent"}`}>
61 {STANDING[glance.kind]}
62 </span>
63 </p>
64 <p className="mt-1 text-xs text-faint">
65 {glance.kind === "beta"
66 ? "At cost. Nothing is charged while g1t is being built out."
67 : glance.kind === "comped"
68 ? "At price, with a 100% discount: nothing is charged to this workspace."
69 : glance.kind === "forge"
70 ? "The forge is free. Workflows and deployments need the plan or the trial."
71 : "Charged so far, from the 1st."}
72 </p>
73
74 {glance.credit && (
75 <div className="mt-4 text-sm">
76 <div className="flex justify-between gap-3">
77 <span className="text-muted">{glance.credit.label}</span>
78 <span className="tabular-nums">
79 {money(glance.credit.usedMicros)}
80 <span className="text-faint"> / {money(glance.credit.ofMicros)}</span>
81 </span>
82 </div>
83 <Bar used={glance.credit.usedMicros} of={glance.credit.ofMicros} warns={glance.kind === "trial"} />
84 </div>
85 )}
86 {glance.onDemand && (
87 <div className="mt-3 flex justify-between gap-3 text-sm">
88 <span className="text-muted">On-demand charges</span>
89 <span className="tabular-nums">
90 {money(glance.onDemand.micros)}
91 {glance.onDemand.limitMicros != null && <span className="text-faint"> / {wholeDollars(glance.onDemand.limitMicros)}</span>}
92 </span>
93 </div>
94 )}
95 </div>
96
97 {glance.lines.length > 0 && (
98 <div className="border-t border-line px-5 py-3">
99 <ul className="space-y-2 text-sm">
100 {lines.map((line) => (
101 <li key={line.key} className="flex items-center gap-2">
102 <span className="size-2 shrink-0 rounded-sm" style={{ background: usageTask(line.key).color }} />
103 <span className="min-w-0 grow truncate text-muted">{line.label}</span>
104 <span className="shrink-0 font-mono text-xs tabular-nums">{money(line.micros)}</span>
105 </li>
106 ))}
107 </ul>
108 {more > 0 && (
109 <Button
110 type="button"
111 onClick={() => setAll((open) => !open)}
112 aria-expanded={all}
113 variant="link"
114 size="inline"
115 className="mt-2 gap-1 text-xs text-muted hover:text-fg font-normal"
116 >
117 <ChevronDown size={13} className={`transition-transform ${all ? "rotate-180" : ""}`} />
118 {all ? "Show less" : `Show ${more} more`}
119 </Button>
120 )}
121 </div>
122 )}
123
124 <div className="flex flex-wrap items-center justify-between gap-2 border-t border-line px-5 py-3">
125 <Link to={`/${slug}/-/usage`} className="inline-flex items-center gap-1 text-xs text-muted hover:text-fg">
126 Usage, run by run <ArrowRight size={12} />
127 </Link>
128 {glance.kind === "forge" || glance.kind === "trial" ? (
129 owner ? (
130 <ButtonLink to={`/${slug}/-/billing#plan`} variant="accent">
131 Start the plan
132 </ButtonLink>
133 ) : (
134 <ButtonLink to={`/${slug}/-/billing`} variant="outline">
135 <CreditCard size={14} />
136 Billing
137 </ButtonLink>
138 )
139 ) : (
140 <ButtonLink to={`/${slug}/-/billing`} variant="outline">
141 <CreditCard size={14} />
142 Billing
143 </ButtonLink>
144 )}
145 </div>
146 </section>
147 </Card>
148 );
149}