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.

132 linesCodeBlameRaw
1/**
2 * A project's pages and the tabs within them, in one place: the sidebar
3 * lists the pages, and a page with more than one view shows them as tabs
4 * across its top, the way GitHub and Vercel do. Soon tabs come from the
5 * roadmap and open its page for them, under the same tabs.
6 */
7import type { Abilities, Capability } from "@g1t/contracts";
8
9import { type RoadmapItem, roadmapIn } from "./roadmap.ts";
10
11export type Tab = {
12 label: string;
13 /** Under the project, without a leading slash; "" is its overview. */
14 path: string;
15 /** Other paths under which this tab is the current one. */
16 also?: string[];
17 soon?: boolean;
18 about?: string;
19 /** Members only. */
20 members?: boolean;
21 /** Current on its own path only, not on the paths under it. */
22 exact?: boolean;
23 /** Only for viewers whose role has this capability, once it is known. */
24 needs?: Capability;
25};
26
27export type Section = {
28 key: RoadmapItem["section"] | "Workflows" | "Settings";
29 tabs: Tab[];
30};
31
32function soon(section: RoadmapItem["section"]): Tab[] {
33 return roadmapIn(section).map((item) => ({ label: item.title, path: `soon/${item.key}`, soon: true, about: item.summary }));
34}
35
36/**
37 * The project's pages, built on first use rather than when the module
38 * loads: the soon tabs read the roadmap, and a bundle that splits the two
39 * into chunks that import each other could otherwise read it before it is
40 * there (a page that throws as it loads, so nothing on it works).
41 */
42let built: Section[] | null = null;
43export function sections(): Section[] {
44 return (built ??= [
45 {
46 key: "Code",
47 tabs: [
48 { label: "Files", path: "code", also: ["tree", "blob"] },
49 { label: "Commits", path: "commits", also: ["commit"] },
50 { label: "Branches", path: "branches" },
51 { label: "Tags", path: "tags" },
52 { label: "Releases", path: "releases" },
53 { label: "Compare", path: "compare" },
54 // No Docs tab: code is not docs. Docs is a workspace mode, filtered by project there.
55 ],
56 },
57 {
58 key: "Deployments",
59 tabs: [
60 { label: "Deployments", path: "deployments" },
61 { label: "Build configuration", path: "settings/deployments", members: true, needs: "manage_integrations" },
62 ...soon("Deployments"),
63 ],
64 },
65 {
66 key: "Observability",
67 tabs: [
68 { label: "Overview", path: "observability", exact: true, members: true },
69 { label: "Requests", path: "observability/requests", members: true },
70 { label: "Logs", path: "observability/logs", members: true },
71 { label: "Traces", path: "observability/traces", members: true },
72 { label: "Errors", path: "observability/errors", members: true },
73 { label: "Metrics", path: "observability/metrics", members: true },
74 { label: "Settings", path: "observability/settings", members: true, needs: "manage_integrations" },
75 ...soon("Observability"),
76 ],
77 },
78 {
79 key: "Security",
80 tabs: [
81 { label: "Overview", path: "security", members: true, needs: "security_alerts", exact: true },
82 { label: "Secret scanning", path: "security/secret-scanning", members: true, needs: "security_alerts" },
83 { label: "Code scanning", path: "security/code-scanning", members: true, needs: "security_alerts", also: ["security/pulls"] },
84 { label: "Vulnerabilities", path: "security/vulnerabilities", members: true, needs: "security_alerts" },
85 { label: "Dependency graph", path: "security/dependency-graph", members: true, needs: "security_alerts" },
86 { label: "Dependency updates", path: "security/dependency-updates", members: true, needs: "security_alerts" },
87 { label: "Advisories", path: "security/advisories", members: true, needs: "security_alerts", also: ["security/reports"] },
88 { label: "Settings", path: "security/settings", members: true, needs: "security_alerts" },
89 ...soon("Security"),
90 ],
91 },
92 {
93 key: "Insights",
94 tabs: [
95 { label: "Contributors", path: "contributors" },
96 { label: "Activity", path: "activity" },
97 { label: "Stargazers", path: "stargazers" },
98 ...soon("Insights"),
99 ],
100 },
101 ]);
102}
103
104/** Pull requests and the merge queue share a page's tabs too. */
105export const PULLS: Section = {
106 key: "Code",
107 tabs: [
108 { label: "Pull requests", path: "pulls", also: ["pull"] },
109 { label: "Merge queue", path: "queue" },
110 ],
111};
112
113function matches(rest: string, tab: Tab): boolean {
114 return [tab.path, ...(tab.also ?? [])].some((path) => path !== "" && (rest === path || rest.startsWith(`${path}/`)));
115}
116
117/**
118 * The tabs for the page at `rest` (the path under the project), if it
119 * belongs to a page with more than one view.
120 */
121export function tabsFor(rest: string, member: boolean, can?: Partial<Abilities>): Tab[] | null {
122 for (const section of [...sections(), PULLS]) {
123 const visible = section.tabs.filter((tab) => (member || !tab.members) && (!tab.needs || !can || Boolean(can[tab.needs])));
124 if (visible.length > 1 && visible.some((tab) => matches(rest, tab))) return visible;
125 }
126 return null;
127}
128
129/** The first page of a section, for its sidebar link: a built one if any. */
130export function entryOf(key: Section["key"]): Tab | undefined {
131 return sections().find((section) => section.key === key)?.tabs[0];
132}