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

144 lines6,591 bytesCodeBlame
1import { Plus } from "lucide-react";
2import { Outlet, data, useLocation } from "react-router";
3
4import type { Route } from "./+types/layout";
5import { page } from "../../lib/meta";
6import { Avatar, ButtonLink, Pill } from "../../components/ui";
7import { WelcomeBanner } from "../../components/welcome";
8import { clearWelcome, welcomes } from "../../lib/invites";
9import { notFound } from "../../lib/not-found.server";
10import { redirectIfRenamed } from "../../lib/renamed.server";
11import { rememberWorkspace } from "../../lib/workspace-choice";
12import { identity } from "../../lib/services.server";
13import { getViewer, roleIn } from "../../lib/session.server";
14
15export function meta({ loaderData, params, ...args }: Route.MetaArgs) {
16 return page(args, { title: `${loaderData?.workspace.name ?? params.owner} · g1t` });
17}
18
19export async function loader({ params, context, request }: Route.LoaderArgs) {
20 const workspace = await identity.getWorkspace(params.owner);
21 if (!workspace) {
22 // A workspace's old name, after a rename: its pages are at the new one.
23 await redirectIfRenamed(request, params.owner);
24 throw notFound("workspace");
25 }
26 const role = roleIn(getViewer(context), workspace.slug);
27 // Opening one of your workspaces makes it the one you are in.
28 if (!role) return { workspace, role, welcome: false };
29 const secure = new URL(request.url).protocol === "https:";
30 const headers = new Headers({ "Set-Cookie": rememberWorkspace(workspace.slug, secure) });
31 // Just joined with an invite: welcomed once (routes/invite.tsx).
32 const welcome = welcomes(request.headers.get("cookie"), workspace.slug);
33 if (welcome) headers.append("Set-Cookie", clearWelcome(secure));
34 return data({ workspace, role, welcome }, { headers });
35}
36
37/** A workspace's own pages, each with its title and what it is for. */
38const PAGES: Record<string, { title: string; about: string }> = {
39 settings: { title: "General", about: "The workspace's name, icon, address and description, and deleting it." },
40 people: {
41 title: "Members",
42 about: "Members create repositories and have the base permission on each one. Owners are Admins on every repository, and also manage members, tokens, billing and integrations.",
43 },
44 repositories: {
45 title: "Repositories",
46 about: "Every repository in the workspace: who can see it, whether it is archived, and the ones deleted recently.",
47 },
48 tokens: {
49 title: "Access tokens",
50 about: "Tokens that belong to the workspace, not a person: for CI, integrations and agents that work for the whole team.",
51 },
52 usage: { title: "Usage", about: "What the workspace's agents cost, run by run, by repository, pull request and model." },
53 billing: { title: "Billing and plans", about: "The g1t plan, the trial, your spend limit and caps, prepaying, and every charge." },
54 agents: {
55 title: "Agent fleet",
56 about: "Every agent at work across the workspace's projects: what each holds, what it is doing now, and what it has cost.",
57 },
58 memory: {
59 title: "Workspace memory",
60 about: "What holds across all of the workspace's projects, given to every agent in every one of them, beside each project's own memory.",
61 },
62 context: {
63 title: "Context",
64 about: "Everything the workspace builds and runs, and what it knows: a catalog built from its projects, memory with a review queue, one search over all of it, and a scorecard for each project.",
65 },
66 audit: {
67 title: "Audit log",
68 about: "Every action agents took with their run credentials, and every change people and tokens made: by whom, on whose behalf, to what, and whether it was allowed.",
69 },
70 guardrails: {
71 title: "Guardrails",
72 about: "What agents may reach, run, spend and take in every project's sandboxes. Each project can override these under its settings.",
73 },
74 webhooks: {
75 title: "Webhooks",
76 about: "Every repository's events, sent to your own addresses as they happen. A repository can also have its own, under its settings.",
77 },
78 secrets: {
79 title: "Secrets and variables",
80 about: "Shared with every repository, or the ones you link: read by workflows, deployments, or both. A repository's own row of the same key wins.",
81 },
82 integrations: {
83 title: "Integrations",
84 about: "Model providers, alerts and trackers. Secrets are sealed when saved, and agents never see them.",
85 },
86};
87
88export default function WorkspaceLayout({ loaderData }: Route.ComponentProps) {
89 const { workspace, role, welcome } = loaderData;
90 // The sidebar finds the workspace's pages, for everyone, so its pages
91 // need a title, not the workspace's whole header again.
92 const page = PAGES[useLocation().pathname.split("/-/")[1]?.split("/")[0] ?? ""];
93 if (page) {
94 return (
95 <div className="mx-auto max-w-5xl px-4 py-10 sm:px-8">
96 <header className="mb-8 border-b border-line pb-6">
97 <h1 className="text-2xl font-semibold tracking-tight">{page.title}</h1>
98 <p className="mt-1.5 text-sm text-muted">{page.about}</p>
99 </header>
100 <Outlet />
101 </div>
102 );
103 }
104 return (
105 <>
106 {/* The workspace's own header band. */}
107 <div className="border-b border-line bg-surface/60">
108 <div className="mx-auto max-w-6xl px-4 pt-8 pb-8">
109 {welcome && (
110 <div className="mb-6">
111 <WelcomeBanner title={`You're in ${workspace.name}`}>
112 You joined as a {role}. Its projects, issues and agents are all here.
113 </WelcomeBanner>
114 </div>
115 )}
116 <div className="flex flex-wrap items-center gap-4">
117 <Avatar name={workspace.slug} image={workspace.avatar} size={52} square />
118 <div className="min-w-0 grow">
119 <div className="flex flex-wrap items-center gap-2.5">
120 <h1 className="truncate text-2xl font-semibold tracking-tight">
121 {workspace.name}
122 </h1>
123 {role && <Pill>{role}</Pill>}
124 </div>
125 <p className="font-mono text-sm text-muted">g1t.sh/{workspace.slug}</p>
126 </div>
127 {role && (
128 <ButtonLink to={`/new?workspace=${workspace.slug}`}>
129 <Plus size={15} />
130 New project
131 </ButtonLink>
132 )}
133 </div>
134 {workspace.description && (
135 <p className="mt-4 max-w-2xl text-sm text-muted">{workspace.description}</p>
136 )}
137 </div>
138 </div>
139 <div className="mx-auto max-w-6xl px-4 py-8">
140 <Outlet />
141 </div>
142 </>
143 );
144}