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.

162 linesCodeBlameRaw
1/**
2 * Which frame a page is drawn in.
3 *
4 * - `standalone`: signing in, signing up and choosing a workspace. The
5 * page alone, centred, with the logo and a quiet row of links at the
6 * foot: no header, no sidebar, no rail.
7 * - `app`: someone signed in, everywhere else. The rail, the mode's
8 * sidebar and the page in its panel, always about their current
9 * workspace, even on public pages such as Explore, a profile or another
10 * workspace's public project.
11 * - `public`: a visitor who is not signed in (or has not confirmed their
12 * address yet), on every other page: a plain header with the logo,
13 * Explore, Docs and signing in, the page at full width, and the footer.
14 */
15
16import type { Abilities, Capability } from "@g1t/contracts";
17
18export type Frame = "standalone" | "public" | "app";
19
20/** Pages that are drawn on their own, whoever opens them. */
21const STANDALONE = new Set([
22 "/login",
23 "/login/two-factor",
24 "/register",
25 "/logout",
26 "/verify",
27 "/confirm-email",
28 "/forgot",
29 "/reset",
30 "/device",
31 "/oauth/authorize",
32 "/workspaces/new",
33 "/integrations/github/setup",
34]);
35
36/** Marketing pages: the public header and footer, even for someone signed in. */
37const MARKETING = new Set(["/download", "/features"]);
38
39/** The page's path without a trailing slash or a click's `.data`. */
40function framePath(pathname: string): string {
41 const path = pathname.replace(/\.data$/, "");
42 return path.length > 1 ? path.replace(/\/+$/, "") : path;
43}
44
45/**
46 * The frame for `pathname`. `viewer` says whether someone is signed in and
47 * confirmed (`signedIn`), and whether they belong to a workspace yet: until
48 * they do, answering an invitation is part of getting started too.
49 */
50export function frameOf(pathname: string, viewer: { signedIn: boolean; workspace: boolean }): Frame {
51 const path = framePath(pathname);
52 if (STANDALONE.has(path)) return "standalone";
53 // Marketing pages are the site's front, whoever reads them: never inside the app's rail and sidebar.
54 if (MARKETING.has(path)) return "public";
55 // An invite link, signing in with GitHub, and a workspace's single sign-on are the front door.
56 if (path.startsWith("/invite/") || path === "/auth/github" || path.startsWith("/auth/github/") || path.startsWith("/auth/sso/")) {
57 return "standalone";
58 }
59 if (path === "/invitations" && viewer.signedIn && !viewer.workspace) return "standalone";
60 return viewer.signedIn ? "app" : "public";
61}
62
63/** The project pages the sidebar lists, by key, for a member or not. */
64export type ProjectPage =
65 | "code"
66 | "work"
67 | "pulls"
68 | "actions"
69 | "deployments"
70 | "observability"
71 | "security"
72 | "insights"
73 | "secrets"
74 | "environments"
75 | "domains"
76 | "runners"
77 | "webhooks"
78 | "storage"
79 | "daemons"
80 | "settings";
81
82/**
83 * What a page needs beyond a role on the repository, once what the viewer
84 * may do is known: Security's findings are for people who can push.
85 */
86export const PROJECT_OPERATIONS = ["secrets", "environments", "domains", "runners", "webhooks"] as const;
87
88export const PAGE_NEEDS: Partial<Record<ProjectPage, Capability>> = {
89 security: "push",
90 secrets: "manage_integrations", environments: "manage_integrations", domains: "manage_integrations",
91 runners: "manage_integrations", webhooks: "manage_integrations",
92};
93
94/**
95 * A project's menu, in order. Security and Settings are for people with a
96 * role on its repository, and a page in `PAGE_NEEDS` only for those whose
97 * role has its capability (`can`, once the repository's page has loaded);
98 * everyone who can see the project sees the rest.
99 */
100export function projectPages(member: boolean, can?: Partial<Abilities>): ProjectPage[] {
101 const pages: (ProjectPage | false)[] = [
102 "code",
103 "work",
104 "pulls",
105 "actions",
106 // A public repository's deployments and build logs are for anyone, as its code is.
107 "deployments",
108 // Logs and requests are not: they can hold what only the people running it should read.
109 member && "observability",
110 member && "security",
111 "insights",
112 ...PROJECT_OPERATIONS.map((page) => member && page),
113 member && "storage",
114 member && "daemons",
115 member && "settings",
116 ];
117 return pages.filter(
118 (page): page is ProjectPage =>
119 page !== false && (!can || !PAGE_NEEDS[page] || Boolean(can[PAGE_NEEDS[page]!])),
120 );
121}
122
123/**
124 * A project's pages as the phone's strip of tabs under its name names them
125 * (routes/repo/layout.tsx): the sidebar's words, and the paths under the
126 * project at which each is the current one.
127 */
128export const PROJECT_PAGE_LINKS: Record<ProjectPage, { label: string; path: string; also: string[]; soon?: boolean; outside?: boolean }> = {
129 code: { label: "Code", path: "code", also: ["tree", "blob", "commits", "commit", "branches", "tags", "releases", "compare"] },
130 // The workspace's Work list filtered to the project (`outside`): work items belong to the workspace.
131 work: { label: "Work", path: "work", also: [], outside: true },
132 pulls: { label: "Pull requests", path: "pulls", also: ["pull", "queue", "sessions"] },
133 actions: { label: "Workflows", path: "actions", also: [] },
134 deployments: { label: "Deployments", path: "deployments", also: ["settings/deployments"] },
135 observability: { label: "Observability", path: "observability", also: [] },
136 security: { label: "Security", path: "security", also: [] },
137 insights: { label: "Insights", path: "contributors", also: ["activity", "stargazers"] },
138 secrets: { label: "Environment variables", path: "settings/secrets", also: [] },
139 environments: { label: "Environments", path: "settings/environments", also: [] },
140 domains: { label: "Domains", path: "settings/domains", also: [] },
141 runners: { label: "Runners", path: "settings/runners", also: [] },
142 webhooks: { label: "Webhooks", path: "settings/webhooks", also: [] },
143 storage: { label: "Storage", path: "soon/storage", also: [], soon: true },
144 daemons: { label: "Daemons", path: "soon/daemons", also: [], soon: true },
145 settings: { label: "Settings", path: "settings", also: [] },
146};
147
148/**
149 * Which of a project's pages `rest` (the path under the project, no
150 * leading slash) is on: "overview" for the project itself. `soon` says
151 * which page a roadmap page (`soon/<key>`) sits under.
152 */
153export function projectPageAt(rest: string, soon: Record<string, ProjectPage> = {}): ProjectPage | "overview" | null {
154 const path = rest.replace(/^\/+|\/+$/g, "");
155 if (path === "") return "overview";
156 const soonKey = /^soon\/([^/]+)/.exec(path)?.[1];
157 if (soonKey && soon[soonKey]) return soon[soonKey]!;
158 for (const [page, link] of Object.entries(PROJECT_PAGE_LINKS) as [ProjectPage, (typeof PROJECT_PAGE_LINKS)[ProjectPage]][]) {
159 if ([link.path, ...link.also].some((prefix) => path === prefix || path.startsWith(`${prefix}/`))) return page;
160 }
161 return null;
162}