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.

88 linesCodeBlameRaw
1import { Link } from "react-router";
2
3import { connectorsFor } from "@g1t/contracts/connectors";
4
5import type { Route } from "./+types/integrations";
6import { ConnectorDirectory } from "../../components/connectors";
7import type { ConnectedState } from "../../lib/connectors";
8import { githubSignIn } from "../../lib/github.server";
9import { page } from "../../lib/meta";
10import { identity } from "../../lib/services.server";
11import { requireUser } from "../../lib/session.server";
12import { Unavailable } from "../../components/boundary";
13import { isFailed, part } from "../../lib/partial";
14
15export function meta(args: Route.MetaArgs) {
16 return page(args, { title: "Integrations · Settings · g1t" });
17}
18
19/**
20 * Your own integrations: the connector directory's personal side, with
21 * what you have connected (GitHub sign-in, applications on the MCP server).
22 * Each is still set up on its own settings page.
23 */
24export async function loader({ request, context }: Route.LoaderArgs) {
25 const user = requireUser(context, request);
26 // What is connected marks the directory's tiles; one that could not be
27 // read is said above it, so a tile is never shown unconnected when it
28 // may well be connected.
29 const [github, grants] = await Promise.all([
30 part(githubSignIn.account(user), "Your GitHub link"),
31 part(identity.listOAuthGrants(user), "Applications on the MCP server"),
32 ]);
33 const unread = [github, grants].filter(isFailed).map((failed) => failed.what);
34 const connected: Record<string, ConnectedState> = {};
35 if (!isFailed(github) && github?.account) {
36 connected.github = {
37 detail: `Linked to @${github.account.login}`,
38 problem: github.account.authorized ? null : "Access ended: link it again to import repositories.",
39 manage: null,
40 };
41 }
42 if (!isFailed(grants) && grants.length > 0) {
43 connected.mcp = {
44 detail: grants.length === 1 ? grants[0]!.clientName : `${grants.length} applications`,
45 problem: null,
46 manage: null,
47 };
48 }
49 const workspaces = (user.workspaces ?? []).map((membership) => ({ slug: membership.slug, name: membership.name ?? membership.slug }));
50 return { connected, workspaces, unread };
51}
52
53export default function PersonalIntegrations({ loaderData }: Route.ComponentProps) {
54 const { connected, workspaces, unread } = loaderData;
55 return (
56 <div>
57 {unread.length > 0 && (
58 <div className="mb-4 space-y-1">
59 {unread.map((what) => (
60 <Unavailable key={what} what={what} compact />
61 ))}
62 </div>
63 )}
64 <ConnectorDirectory
65 scope="personal"
66 views={connectorsFor("personal")}
67 connected={connected}
68 workspace={null}
69 canManage
70 nav="chips"
71 />
72 {workspaces.length > 0 && (
73 <p className="mt-10 border-t border-line pt-5 text-sm text-muted">
74 Tools the whole team shares are connected for each workspace:{" "}
75 {workspaces.map((workspace, index) => (
76 <span key={workspace.slug}>
77 {index > 0 && ", "}
78 <Link to={`/${workspace.slug}/-/integrations`} className="text-accent hover:underline">
79 {workspace.name}
80 </Link>
81 </span>
82 ))}
83 .
84 </p>
85 )}
86 </div>
87 );
88}