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.

106 linesCodeBlameRaw
1/**
2 * Install requests: for an owner, everything members asked the workspace
3 * to add, with the way to add it, mark it added or turn it down; for
4 * anyone else, their own and how each was answered. Every Marketplace form
5 * posts here: asking, answering, and installing, switching off or removing
6 * an extension.
7 */
8import { Link, data } from "react-router";
9
10import { connectorsFor } from "@g1t/contracts/connectors";
11
12import type { Route } from "./+types/requests";
13import { RequestRow, SectionHead } from "../../../components/marketplace";
14import { EmptyState } from "../../../components/ui";
15import { Card } from "../../../components/ui/card";
16import { addPath, marketplaceForm, marketplacePath, openRequests } from "../../../lib/marketplace";
17import { workspaceAgents } from "../../../lib/services.server";
18import { assertSameOrigin, requireUser, roleIn } from "../../../lib/session.server";
19import { useMarketplace } from "./layout";
20
21export async function action({ params, context, request }: Route.ActionArgs) {
22 assertSameOrigin(request);
23 const viewer = requireUser(context, request);
24 const slug = params.owner.toLowerCase();
25 if (!roleIn(viewer, slug)) throw data(null, { status: 404 });
26 const asked = marketplaceForm(await request.formData());
27 if (!asked) return data({ error: "Nothing to do." }, { status: 400 });
28 const call = (): Promise<{ ok: true } | { ok: false; error: { code: string; message: string } }> => {
29 switch (asked.intent) {
30 case "request":
31 return workspaceAgents.requestInstall(slug, viewer, asked.listing, asked.note);
32 case "resolve":
33 return workspaceAgents.resolveInstallRequest(slug, viewer, asked.id, asked.status);
34 case "install":
35 return workspaceAgents.installExtension(slug, viewer, asked.extension);
36 case "switch":
37 return workspaceAgents.setExtensionEnabled(slug, viewer, asked.listing, asked.enabled);
38 case "uninstall":
39 return workspaceAgents.uninstallExtension(slug, viewer, asked.listing);
40 }
41 };
42 const result = await call().catch(() => null);
43 if (!result) return data({ error: "The request didn't go through. Try again in a moment." }, { status: 503 });
44 if (!result.ok) return data({ error: result.error.message }, { status: result.error.code === "conflict" ? 409 : 422 });
45 return { error: null };
46}
47
48export default function MarketplaceRequests() {
49 const { slug, owner, requests } = useMarketplace();
50 if (!requests) {
51 return <EmptyState title="Requests can't be shown right now">The agents service didn't answer. Reload in a minute.</EmptyState>;
52 }
53 const views = connectorsFor("workspace");
54 const waiting = openRequests(requests.requests);
55 const answered = requests.requests.filter((request) => request.status !== "open");
56 if (requests.requests.length === 0) {
57 return owner ? (
58 <EmptyState title="No requests">When a member asks for an extension or an integration, it shows here, and you're notified.</EmptyState>
59 ) : (
60 <EmptyState title="You haven't asked for anything">
61 Find an extension or an integration and choose <b>Request</b>. The workspace's owners are notified, and you hear back when one answers.
62 </EmptyState>
63 );
64 }
65 return (
66 <div className="space-y-10">
67 <section aria-labelledby="waiting">
68 <SectionHead id="waiting" title={owner ? "Waiting on an owner" : "Waiting"} aside={`${waiting.length}`}>
69 {owner
70 ? "Installing an extension answers every request for it. After connecting an integration, mark its requests added."
71 : "Your requests the workspace's owners haven't answered yet."}
72 </SectionHead>
73 {waiting.length === 0 ? (
74 <Card asChild tone="plain" className="border-dashed px-4 py-6 text-center text-sm text-muted">
75 <p>Nothing is waiting.</p>
76 </Card>
77 ) : (
78 <Card asChild divided className="overflow-hidden">
79 <ul>
80 {waiting.map((request) => (
81 <RequestRow key={request.id} request={request} slug={slug} owner={owner} addTo={owner ? addPath(request, slug, views) : null} />
82 ))}
83 </ul>
84 </Card>
85 )}
86 </section>
87 {answered.length > 0 && (
88 <section aria-labelledby="answered">
89 <SectionHead id="answered" title="Answered" aside="Last 30 days" />
90 <Card asChild divided className="overflow-hidden">
91 <ul>
92 {answered.map((request) => (
93 <RequestRow key={request.id} request={request} slug={slug} owner={owner} addTo={null} />
94 ))}
95 </ul>
96 </Card>
97 </section>
98 )}
99 {!owner && (
100 <p className="text-sm text-muted">
101 Looking for something else? <Link to={marketplacePath(slug)} className="text-accent hover:underline">Browse the Marketplace</Link>.
102 </p>
103 )}
104 </div>
105 );
106}