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.

185 linesCodeBlameRaw
1/**
2 * One integration's page: who builds it, whether this workspace can
3 * connect it (or has), and exactly what it lets agents and people do
4 * today and what is still Soon, from the connector catalog
5 * (@g1t/contracts/connectors) for each way it is connected: for the
6 * whole workspace, and for each person.
7 */
8import { ArrowLeft, Clock, ShieldCheck, Users } from "lucide-react";
9import { Link, data } from "react-router";
10
11import { connectorById, connectorView } from "@g1t/contracts/connectors";
12
13import type { Route } from "./+types/integration";
14import { ConnectorMark } from "../../../components/connectors";
15import { AvailabilityBadge, IntegrationAction, ListingFacts } from "../../../components/marketplace";
16import { cn } from "../../../lib/cn";
17import { type IntegrationUse, TIERS, categoryTitle, comingListings, integrationListings, integrationUses, marketplacePath, personalListings } from "../../../lib/marketplace";
18import { loadConnected } from "../../../lib/marketplace.server";
19import { page } from "../../../lib/meta";
20import { requireUser, roleIn } from "../../../lib/session.server";
21import { useMarketplace } from "./layout";
22import { Card } from "../../../components/ui/card";
23
24export function meta({ params, ...args }: Route.MetaArgs) {
25 const connector = connectorById(params.integration);
26 return page(args, { title: `${connector?.name ?? "Integration"} · Marketplace · ${params.owner} · g1t` });
27}
28
29export async function loader({ params, context, request }: Route.LoaderArgs) {
30 const viewer = requireUser(context, request);
31 const slug = params.owner.toLowerCase();
32 if (!roleIn(viewer, slug)) throw data(null, { status: 404 });
33 if (!connectorById(params.integration)) throw data(null, { status: 404 });
34 return await loadConnected(slug, viewer);
35}
36
37export default function MarketplaceIntegration({ params, loaderData }: Route.ComponentProps) {
38 const { slug, owner, username, requests } = useMarketplace();
39 const connector = connectorById(params.integration)!;
40 const workspaceView = connectorView(connector, "workspace");
41 const personalView = connectorView(connector, "personal");
42 // The listing this page acts on: the workspace's, or each person's when it is only theirs.
43 const listing = workspaceView
44 ? workspaceView.status === "available"
45 ? integrationListings([workspaceView], loaderData.connected, requests?.requests ?? [], username, slug, loaderData.unavailable)[0]!
46 : comingListings([workspaceView], [], slug)[0]!
47 : personalView!.status === "available" && personalView!.href
48 ? personalListings([personalView!], slug, loaderData.unavailable)[0]!
49 : comingListings([], [personalView!], slug)[0]!;
50 const uses = integrationUses(connector);
51 const tier = TIERS[listing.tier];
52 return (
53 <div>
54 <Link to={marketplacePath(slug, "integrations")} className="inline-flex items-center gap-1.5 text-sm text-muted hover:text-fg">
55 <ArrowLeft size={14} />
56 All integrations
57 </Link>
58 <header className="mt-5 flex flex-wrap items-center gap-4">
59 <span className={cn("shrink-0", (listing.availability === "soon" || listing.availability === "unavailable") && "opacity-60 grayscale")}>
60 <ConnectorMark view={listing.view} size={64} />
61 </span>
62 <div className="min-w-0 grow basis-60">
63 <h2 className="text-xl font-semibold tracking-tight">{connector.name}</h2>
64 <ListingFacts tier={listing.tier} publisher={listing.publisher} category={categoryTitle(connector.category)}>
65 <AvailabilityBadge
66 availability={listing.availability}
67 kind="integration"
68 why={listing.why}
69 hint={listing.availability === "available" && !owner && listing.scope === "workspace" ? "An owner connects it for everyone. Ask one with Request." : undefined}
70 />
71 </ListingFacts>
72 </div>
73 <IntegrationAction listing={listing} slug={slug} owner={owner} className="h-9 px-4 text-sm" />
74 </header>
75 {listing.availability === "unavailable" && listing.why && (
76 <p className="mt-5 max-w-3xl rounded-lg border border-dashed border-line-strong px-4 py-3 text-sm text-muted">
77 <span className="font-medium text-fg-soft">Not available here.</span> {listing.why}
78 </p>
79 )}
80 {listing.availability === "soon" && (
81 <p className="mt-5 max-w-3xl rounded-lg border border-dashed border-line-strong px-4 py-3 text-sm text-muted">
82 Planned, not built yet: nobody can connect it. This page shows what it will do.
83 </p>
84 )}
85 <p className="mt-6 max-w-3xl text-[0.9375rem] leading-relaxed text-fg-soft">{listing.view.description}</p>
86
87 <div className="mt-8 grid gap-8 lg:grid-cols-[minmax(0,1fr)_20rem]">
88 <section aria-labelledby="does" className="min-w-0">
89 <h3 id="does" className="text-sm font-semibold">
90 What it lets agents and people do
91 </h3>
92 <div className="mt-3 grid gap-3 sm:grid-cols-2">
93 <div>
94 <h4 className="mb-2 flex items-center gap-1.5 text-xs font-medium text-success">
95 <ShieldCheck size={13} />
96 {listing.availability === "unavailable" ? "Once it can be connected here" : "Today"}
97 </h4>
98 {uses.today.length === 0 ? (
99 <Card asChild tone="plain" className="border-dashed px-4 py-4 text-sm text-muted">
100 <p>Nothing yet. It can't be connected until it is built.</p>
101 </Card>
102 ) : (
103 <div className="space-y-3">
104 {uses.today.map((use) => (
105 <Use key={use.scope} use={use} said={listing.view.description} />
106 ))}
107 </div>
108 )}
109 </div>
110 <div>
111 <h4 className="mb-2 flex items-center gap-1.5 text-xs font-medium text-muted">
112 <Clock size={13} />
113 Soon
114 </h4>
115 {uses.soon.length === 0 ? (
116 <Card asChild tone="plain" className="border-dashed px-4 py-4 text-sm text-muted">
117 <p>Nothing more is planned for it yet.</p>
118 </Card>
119 ) : (
120 <div className="space-y-3">
121 {uses.soon.map((use) => (
122 <Use key={use.scope} use={use} said={listing.view.description} soon />
123 ))}
124 </div>
125 )}
126 </div>
127 </div>
128 </section>
129
130 <aside className="text-sm">
131 <Card asChild className="space-y-3 p-4">
132 <dl>
133 <div>
134 <dt className="text-xs text-faint">Publisher</dt>
135 <dd className="mt-0.5">
136 {listing.publisher} · {tier.label}
137 <span className="block text-xs text-muted">{tier.about}</span>
138 </dd>
139 </div>
140 <div>
141 <dt className="flex items-center gap-1.5 text-xs text-faint">
142 <Users size={12} />
143 Who connects it
144 </dt>
145 <dd className="mt-0.5">
146 {connector.scopes.includes("workspace") && connector.scopes.includes("personal")
147 ? "An owner, once for the whole workspace; and each person, for their own account."
148 : connector.scopes.includes("workspace")
149 ? "An owner, once for the whole workspace."
150 : "Each person, for their own account. Agents use it only when that person asks."}
151 </dd>
152 </div>
153 {listing.connected && (
154 <div>
155 <dt className="text-xs text-faint">Connected</dt>
156 <dd className="mt-0.5 break-words">{listing.connected.detail}</dd>
157 {listing.connected.problem && <dd className="mt-1 text-warn">{listing.connected.problem}</dd>}
158 </div>
159 )}
160 </dl>
161 </Card>
162 </aside>
163 </div>
164 </div>
165 );
166}
167
168/** One way it is connected: who connects it, what it does that way, and its capabilities. */
169function Use({ use, said, soon = false }: { use: IntegrationUse; said: string; soon?: boolean }) {
170 return (
171 <div className={cn("rounded-xl border p-4", soon ? "border-dashed border-line-strong/70" : "border-line bg-surface")}>
172 <p className="text-xs text-faint">{use.who}</p>
173 {use.description !== said && <p className={cn("mt-1 text-sm", soon ? "text-muted" : "text-fg-soft")}>{use.description}</p>}
174 {use.capabilities.length > 0 && (
175 <ul className="mt-2.5 flex flex-wrap gap-1.5" aria-label={soon ? "What it will do" : "What it does"}>
176 {use.capabilities.map((capability) => (
177 <li key={capability} className={cn("rounded-full px-2 py-px text-[0.6875rem]", soon ? "border border-dashed border-line text-faint" : "bg-raised text-fg-soft")}>
178 {capability}
179 </li>
180 ))}
181 </ul>
182 )}
183 </div>
184 );
185}