flagon-io/g1t

public

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

g1t/apps/sudo/app/routes/new-enterprise.tsx

110 lines5,369 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.

Billing accounts, terms and enterprises; g1t is no longer free1import { ArrowLeft, Building2 } from "lucide-react";
2import { data, Link, redirect } from "react-router";
3
4import type { Route } from "./+types/new-enterprise";
5import { Avatar, Button, Field, Input, Notice, Section, Textarea } from "~/components/ui";
6import { fields, parseSlugList, text } from "~/lib/forms";
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace7import { admin, identity } from "~/lib/services.server";
Billing accounts, terms and enterprises; g1t is no longer free8import { requireStaff } from "~/lib/staff";
9
10export const meta: Route.MetaFunction = () => [{ title: "New enterprise · sudo" }, { name: "robots", content: "noindex, nofollow" }];
11
12export async function loader({ context }: Route.LoaderArgs) {
13 requireStaff(context);
14 return null;
15}
16
17type ActionData =
18 | { error: string; values: Record<string, string> }
19 | { review: { name: string; workspaces: string[]; values: Record<string, string> } };
20
21export async function action({ request, context }: Route.ActionArgs) {
22 const staff = requireStaff(context);
23 const form = await request.formData();
24 const values = fields(form, "name", "workspaces");
25 const fail = (error: string) => data<ActionData>({ error, values }, { status: 422 });
26
27 const name = values.name.replace(/\s+/g, " ");
28 if (!name) return fail("Give the enterprise a name.");
29 if (name.length > 100) return fail("Keep the name under 100 characters.");
30 const workspaces = parseSlugList(values.workspaces);
31 if (!workspaces.ok) return fail(workspaces.error);
32 if (workspaces.value.length === 0) return fail("Name at least one workspace for it to pay for.");
33 if (workspaces.value.length > 100) return fail("At most 100 workspaces at once.");
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace34 const found = await Promise.all(workspaces.value.map((slug) => identity.workspace(slug)));
35 const unknown = workspaces.value.filter((_, index) => !found[index]);
36 if (unknown.length > 0) return fail(`No workspace called ${unknown.join(", ")}.`);
Billing accounts, terms and enterprises; g1t is no longer free37
38 // Moving workspaces onto it changes who pays for them: confirm first.
39 if (text(form, "confirm") !== "yes") {
40 return { review: { name, workspaces: workspaces.value, values } } satisfies ActionData;
41 }
42 const result = await admin.createEnterprise(name, workspaces.value, staff.email);
43 if (!result.ok) return fail(result.error.message);
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace44 return redirect(`/enterprises/${encodeURIComponent(result.value.id)}?done=created#top`);
Billing accounts, terms and enterprises; g1t is no longer free45}
46
47export default function NewEnterprise({ actionData }: Route.ComponentProps) {
48 const result = actionData as ActionData | undefined;
49 const review = result && "review" in result ? result.review : null;
50 const error = result && "error" in result ? result : null;
51 const values = review?.values ?? error?.values;
52
53 return (
54 <main className="mx-auto max-w-2xl px-4 py-8 sm:py-10">
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace55 <Link to="/enterprises" className="inline-flex items-center gap-1.5 text-sm text-muted hover:text-fg">
Billing accounts, terms and enterprises; g1t is no longer free56 <ArrowLeft size={14} />
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace57 Enterprises
Billing accounts, terms and enterprises; g1t is no longer free58 </Link>
59 <h1 className="mt-4 text-2xl font-semibold tracking-tight">New enterprise</h1>
60 <p className="mt-1 text-sm text-muted">
Say what g1t does, not whom it is like61 One customer that pays for several workspaces: one bill, one limit, one set of terms. Set its terms
Billing accounts, terms and enterprises; g1t is no longer free62 once it exists.
63 </p>
64
65 {review && (
66 <section id="review" className="mt-6 scroll-mt-20 rounded-lg border border-merged/40 bg-merged/5 p-4 sm:p-5">
67 <h2 className="font-semibold tracking-tight">Create {review.name}?</h2>
68 <p className="mt-1 text-sm text-muted">
Billing on Stripe's pages, month-end charges, warnings; sudo by workspace69 These workspaces will be billed to it from now on, under its limit and terms instead of their own:
Billing accounts, terms and enterprises; g1t is no longer free70 </p>
71 <ul className="mt-3 flex flex-wrap gap-2">
72 {review.workspaces.map((slug) => (
73 <li key={slug} className="inline-flex items-center gap-1.5 rounded-md border border-line bg-bg px-2 py-1 font-mono text-xs">
74 <Avatar name={slug} size={14} />
75 {slug}
76 </li>
77 ))}
78 </ul>
79 <form method="post" action="/enterprises/new" className="mt-4 flex flex-wrap items-center gap-2">
80 <input type="hidden" name="name" value={review.values.name} />
81 <input type="hidden" name="workspaces" value={review.values.workspaces} />
82 <input type="hidden" name="confirm" value="yes" />
83 <Button type="submit" variant="lavender">
84 <Building2 size={14} />
85 Create enterprise
86 </Button>
87 <Link to="/enterprises/new" className="px-2 text-sm text-muted hover:text-fg">
88 Cancel
89 </Link>
90 </form>
91 </section>
92 )}
93
94 <Section title="Enterprise" className="mt-6">
95 <form method="post" action="/enterprises/new#review" className="space-y-4">
96 {error && <Notice tone="error">{error.error}</Notice>}
97 <Field label="Name" hint="As it should appear on the bill.">
98 <Input name="name" required maxLength={100} placeholder="Acme Corporation" defaultValue={values?.name ?? ""} />
99 </Field>
100 <Field label="Workspaces" hint="Slugs, separated by commas or one per line.">
101 <Textarea name="workspaces" required rows={4} placeholder={"acme\nacme-labs"} defaultValue={values?.workspaces ?? ""} className="font-mono" />
102 </Field>
103 <div className="flex justify-end">
104 <Button type="submit">Review</Button>
105 </div>
106 </form>
107 </Section>
108 </main>
109 );
110}