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.

g1t.sh
153 linesCodeBlameRaw
1/**
2 * Integrations in the Marketplace, by who builds them: every connector is
3 * g1t's own (Official), so that section holds the ones connected, the ones
4 * a workspace can connect today, any this g1t can't connect (with why),
5 * those each person connects for themselves, and the ones the catalog
6 * lists as coming; Verified, Community and Internal are sections too,
7 * empty and saying so. Search, and filters by tier and availability, kept
8 * in the address. Each workspace one is set up on its own page under
9 * Workspace → Integrations; owners connect, anyone else asks.
10 */
11import { Search } from "lucide-react";
12import { useState } from "react";
13import { Link, useSearchParams } from "react-router";
14
15import { CONNECTORS, connectorView, connectorsFor } from "@g1t/contracts/connectors";
16
17import type { Route } from "./+types/integrations";
18import { IntegrationCard, IntegrationRow, ListingFilterBar, ListingLegend, SectionHead, TierBadge, TierEmpty, filterCounts } from "../../../components/marketplace";
19import { type IntegrationListing, comingListings, integrationListings, integrationMatches, passes, personalListings, readFilters, tiersShown } from "../../../lib/marketplace";
20import { loadConnected } from "../../../lib/marketplace.server";
21import { requireUser, roleIn } from "../../../lib/session.server";
22import { useMarketplace } from "./layout";
23
24export async function loader({ params, context, request }: Route.LoaderArgs) {
25 const viewer = requireUser(context, request);
26 const slug = params.owner.toLowerCase();
27 if (!roleIn(viewer, slug)) throw new Response(null, { status: 404 });
28 return await loadConnected(slug, viewer);
29}
30
31const HEADINGS = { official: "From g1t", verified: "From verified publishers", community: "From the community", internal: "Built in this workspace" } as const;
32
33export default function MarketplaceIntegrations({ loaderData }: Route.ComponentProps) {
34 const { slug, owner, username, requests } = useMarketplace();
35 const [query, setQuery] = useState("");
36 const [params] = useSearchParams();
37 const filters = readFilters(params);
38 const views = connectorsFor("workspace");
39 const personalViews = CONNECTORS.map((c) => connectorView(c, "personal")).filter((view) => view != null);
40 const workspace = integrationListings(views, loaderData.connected, requests?.requests ?? [], username, slug, loaderData.unavailable);
41 // What each person connects for themselves, today; and what is coming, for either.
42 const yours = personalListings(personalViews, slug, loaderData.unavailable);
43 const coming = comingListings(
44 views,
45 personalViews.filter((view) => !CONNECTORS.find((c) => c.id === view.id)?.scopes.includes("workspace")),
46 slug,
47 );
48 const everything = [...workspace, ...yours, ...coming];
49 const wanted = query.trim().toLowerCase();
50 const keep = (listing: IntegrationListing) => integrationMatches(listing, query) && passes(listing, filters);
51 // A search or an availability filter can empty a tier; the tier filter alone only picks one.
52 const filtered = wanted !== "" || filters.availability !== "all";
53 // A plain function, not a component made each render: that would remount the cards (and close a request's dialog).
54 const grid = (items: IntegrationListing[]) => (
55 <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
56 {items.map((listing) => (
57 <IntegrationCard key={`${listing.scope}:${listing.ref}`} listing={listing} slug={slug} owner={owner} />
58 ))}
59 </div>
60 );
61 return (
62 <div className="space-y-10">
63 <div className="space-y-4">
64 <div className="flex flex-wrap items-center justify-between gap-3">
65 <label className="flex h-9 w-full max-w-sm items-center gap-2 rounded-lg border border-line bg-surface px-2.5 text-sm focus-within:border-line-strong">
66 <Search size={15} className="shrink-0 text-faint" />
67 <input
68 value={query}
69 onChange={(event) => setQuery(event.target.value)}
70 placeholder="Find an integration"
71 aria-label="Find an integration"
72 autoComplete="off"
73 className="min-w-0 grow bg-transparent outline-none placeholder:text-faint"
74 />
75 </label>
76 <Link to={`/${slug}/-/integrations`} className="text-[0.8125rem] text-muted hover:text-fg">
77 Workspace integrations settings
78 </Link>
79 </div>
80 <ListingFilterBar kind="integration" filters={filters} counts={filterCounts(everything)} />
81 </div>
82
83 {tiersShown(filters).map((tier) => {
84 const connected = workspace.filter((l) => l.tier === tier && l.availability === "added" && keep(l));
85 const available = workspace.filter((l) => l.tier === tier && l.availability === "available" && keep(l));
86 const blocked = workspace.filter((l) => l.tier === tier && l.availability === "unavailable" && keep(l));
87 const own = yours.filter((l) => l.tier === tier && keep(l));
88 const soon = coming.filter((l) => l.tier === tier && keep(l));
89 const count = connected.length + available.length + blocked.length + own.length + soon.length;
90 return (
91 <section key={tier} aria-labelledby={`tier-${tier}`} className="space-y-8">
92 <SectionHead
93 id={`tier-${tier}`}
94 title={
95 <>
96 <TierBadge tier={tier} />
97 {HEADINGS[tier]}
98 </>
99 }
100 aside={`${count}`}
101 >
102 {tier === "official" && count > 0 ? "Every connector in the catalog is built and supported by g1t. Each says what it lets agents do." : null}
103 </SectionHead>
104 {count === 0 && <TierEmpty tier={tier} kind="integration" filtered={filtered} />}
105 {connected.length > 0 && (
106 <div>
107 <SectionHead sub id={`${tier}-connected`} title="Connected" aside={`${connected.length}`} />
108 {grid(connected)}
109 </div>
110 )}
111 {available.length > 0 && (
112 <div>
113 <SectionHead sub id={`${tier}-available`} title="Available to connect" aside={`${available.length}`}>
114 Connected once for the whole workspace by an owner.
115 </SectionHead>
116 {grid(available)}
117 </div>
118 )}
119 {blocked.length > 0 && (
120 <div>
121 <SectionHead sub id={`${tier}-unavailable`} title="Not available here" aside={`${blocked.length}`}>
122 This g1t lacks something these need. Each says what.
123 </SectionHead>
124 {grid(blocked)}
125 </div>
126 )}
127 {own.length > 0 && (
128 <div>
129 <SectionHead sub id={`${tier}-yours`} title="Connected by each person" aside={`${own.length}`}>
130 Each person connects these for themselves, and agents use them only when that person asks.
131 </SectionHead>
132 {grid(own)}
133 </div>
134 )}
135 {soon.length > 0 && (
136 <div>
137 <SectionHead sub id={`${tier}-soon`} title="Soon" aside={`${soon.length}`}>
138 Planned, not built. Nobody can connect these yet.
139 </SectionHead>
140 <ul className="grid gap-2 sm:grid-cols-2 lg:grid-cols-3">
141 {soon.map((listing) => (
142 <IntegrationRow key={listing.ref} listing={listing} />
143 ))}
144 </ul>
145 </div>
146 )}
147 </section>
148 );
149 })}
150 <ListingLegend kind="integration" />
151 </div>
152 );
153}