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.

81 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 scope="personal" views={connectorsFor("personal")} connected={connected} workspace={null} canManage nav="chips" />
65 {workspaces.length > 0 && (
66 <p className="mt-10 border-t border-line pt-5 text-sm text-muted">
67 Tools the whole team shares are connected for each workspace:{" "}
68 {workspaces.map((workspace, index) => (
69 <span key={workspace.slug}>
70 {index > 0 && ", "}
71 <Link to={`/${workspace.slug}/-/integrations`} className="text-accent hover:underline">
72 {workspace.name}
73 </Link>
74 </span>
75 ))}
76 .
77 </p>
78 )}
79 </div>
80 );
81}