blob: fbb63e4426dd65978c7299fcb0f9e201afbcc88f [file] [log] [blame]
giod0026612025-05-08 13:00:36 +00001import { NodeRect } from "./node-rect";
gioa71316d2025-05-24 09:41:36 +04002import {
3 GithubNode,
4 nodeIsConnectable,
5 nodeLabel,
gioa71316d2025-05-24 09:41:36 +04006 useStateStore,
7 useGithubService,
gioa71316d2025-05-24 09:41:36 +04008 useGithubRepositories,
9 useGithubRepositoriesLoading,
10 useGithubRepositoriesError,
11 useFetchGithubRepositories,
gio8e74dc02025-06-13 10:19:26 +000012 AppNode,
gioa71316d2025-05-24 09:41:36 +040013} from "@/lib/state";
gio8e74dc02025-06-13 10:19:26 +000014import { useEffect, useMemo, useState } from "react";
gio5f2f1002025-03-20 18:38:48 +040015import { z } from "zod";
giod0026612025-05-08 13:00:36 +000016import { DeepPartial, EventType, useForm } from "react-hook-form";
17import { zodResolver } from "@hookform/resolvers/zod";
18import { Form, FormControl, FormField, FormItem, FormMessage } from "./ui/form";
gio5f2f1002025-03-20 18:38:48 +040019import { Handle, Position } from "@xyflow/react";
giod0026612025-05-08 13:00:36 +000020import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
giod0026612025-05-08 13:00:36 +000021import { useProjectId } from "@/lib/state";
22import { Alert, AlertDescription } from "./ui/alert";
gioa71316d2025-05-24 09:41:36 +040023import { AlertCircle, LoaderCircle, RefreshCw } from "lucide-react";
24import { Button } from "./ui/button";
gio3fb133d2025-06-13 07:20:24 +000025import { NodeDetailsProps } from "@/lib/types";
gio8e74dc02025-06-13 10:19:26 +000026import { ImportModal } from "./import-modal";
gio5f2f1002025-03-20 18:38:48 +040027
28export function NodeGithub(node: GithubNode) {
giod0026612025-05-08 13:00:36 +000029 const { id, selected } = node;
30 const isConnectable = useMemo(() => nodeIsConnectable(node, "repository"), [node]);
31 return (
gio69148322025-06-19 23:16:12 +040032 <NodeRect id={id} selected={selected} node={node} state={node.data.state}>
giod0026612025-05-08 13:00:36 +000033 <div style={{ padding: "10px 20px" }}>
34 {nodeLabel(node)}
35 <Handle
36 id="repository"
37 type={"source"}
38 position={Position.Right}
39 isConnectableStart={isConnectable}
40 isConnectableEnd={isConnectable}
41 isConnectable={isConnectable}
42 />
43 </div>
44 </NodeRect>
45 );
gio5f2f1002025-03-20 18:38:48 +040046}
47
48const schema = z.object({
giod0026612025-05-08 13:00:36 +000049 repositoryId: z.number().optional(),
gio5f2f1002025-03-20 18:38:48 +040050});
51
gio3fb133d2025-06-13 07:20:24 +000052export function NodeGithubDetails({ node, disabled }: NodeDetailsProps<GithubNode>) {
gio08acd3a2025-06-12 12:15:30 +000053 const { id, data } = node;
giod0026612025-05-08 13:00:36 +000054 const store = useStateStore();
55 const projectId = useProjectId();
giod0026612025-05-08 13:00:36 +000056 const githubService = useGithubService();
gio7f98e772025-05-07 11:00:14 +000057
gioa71316d2025-05-24 09:41:36 +040058 const storeRepos = useGithubRepositories();
59 const isLoadingRepos = useGithubRepositoriesLoading();
60 const repoError = useGithubRepositoriesError();
61 const fetchStoreRepositories = useFetchGithubRepositories();
62
gio8e74dc02025-06-13 10:19:26 +000063 const [showImportModal, setShowImportModal] = useState(false);
gioa71316d2025-05-24 09:41:36 +040064
giod0026612025-05-08 13:00:36 +000065 const form = useForm<z.infer<typeof schema>>({
66 resolver: zodResolver(schema),
67 mode: "onChange",
68 defaultValues: {
69 repositoryId: data.repository?.id,
70 },
71 });
gio7f98e772025-05-07 11:00:14 +000072
giod0026612025-05-08 13:00:36 +000073 useEffect(() => {
gioa71316d2025-05-24 09:41:36 +040074 form.reset({ repositoryId: data.repository?.id });
75 }, [data.repository?.id, form]);
76
77 useEffect(() => {
giod0026612025-05-08 13:00:36 +000078 const sub = form.watch(
79 (
80 value: DeepPartial<z.infer<typeof schema>>,
81 { name, type }: { name?: keyof z.infer<typeof schema> | undefined; type?: EventType | undefined },
82 ) => {
83 if (type !== "change") {
84 return;
85 }
86 switch (name) {
87 case "repositoryId":
88 if (value.repositoryId) {
gio3d0bf032025-06-05 06:57:26 +000089 const repo = storeRepos.find((r) => r.id === value.repositoryId);
gio8e74dc02025-06-13 10:19:26 +000090 if (!repo) {
91 return;
giod0026612025-05-08 13:00:36 +000092 }
gio8e74dc02025-06-13 10:19:26 +000093 store.setNodes(
94 store.nodes.map((n): AppNode => {
95 if (n.type === "github" && n.id === id) {
96 return {
97 ...n,
98 data: {
99 ...n.data,
100 repository: {
101 id: repo.id,
102 sshURL: repo.ssh_url,
103 fullName: repo.full_name,
104 },
105 },
106 };
107 } else if (n.type === "app" && n.data.repository?.repoNodeId === id) {
108 return {
109 ...n,
110 data: { ...n.data, repository: { id: repo.id, repoNodeId: id } },
111 };
112 } else {
113 return n;
114 }
115 }),
116 );
giod0026612025-05-08 13:00:36 +0000117 }
118 break;
119 }
120 },
121 );
122 return () => sub.unsubscribe();
gio3d0bf032025-06-05 06:57:26 +0000123 }, [form, store, id, storeRepos]);
gio7f98e772025-05-07 11:00:14 +0000124
giod0026612025-05-08 13:00:36 +0000125 return (
126 <>
127 <Form {...form}>
128 <form className="space-y-2">
129 <FormField
130 control={form.control}
131 name="repositoryId"
132 render={({ field }) => (
133 <FormItem>
gioa71316d2025-05-24 09:41:36 +0400134 <div className="flex items-center gap-2 w-full">
135 <div className="flex-grow">
136 <Select
137 onValueChange={(value) => field.onChange(Number(value))}
138 value={field.value?.toString()}
139 disabled={isLoadingRepos || !projectId || !githubService || disabled}
140 >
141 <FormControl>
142 <SelectTrigger>
143 <SelectValue
144 placeholder={
145 githubService
146 ? isLoadingRepos
147 ? "Loading..."
gio3d0bf032025-06-05 06:57:26 +0000148 : storeRepos.length === 0
gioa71316d2025-05-24 09:41:36 +0400149 ? "No repositories found"
150 : "Select a repository"
151 : "GitHub not configured"
152 }
153 />
154 </SelectTrigger>
155 </FormControl>
156 <SelectContent>
gio3d0bf032025-06-05 06:57:26 +0000157 {storeRepos.map((repo) => (
gioa71316d2025-05-24 09:41:36 +0400158 <SelectItem key={repo.id} value={repo.id.toString()}>
159 {repo.full_name}
160 {repo.description && ` - ${repo.description}`}
161 </SelectItem>
162 ))}
163 </SelectContent>
164 </Select>
165 </div>
166 {isLoadingRepos && (
167 <Button variant="ghost" size="icon" disabled>
168 <LoaderCircle className="h-5 w-5 animate-spin text-muted-foreground" />
169 </Button>
170 )}
171 {!isLoadingRepos && githubService && (
172 <Button
173 variant="ghost"
174 size="icon"
175 onClick={fetchStoreRepositories}
176 disabled={disabled}
177 aria-label="Refresh repositories"
178 >
179 <RefreshCw className="h-5 w-5 text-muted-foreground" />
180 </Button>
181 )}
182 </div>
giod0026612025-05-08 13:00:36 +0000183 <FormMessage />
gioa71316d2025-05-24 09:41:36 +0400184 {repoError && <p className="text-sm text-red-500 mt-1">{repoError}</p>}
giod0026612025-05-08 13:00:36 +0000185 {!githubService && (
186 <Alert variant="destructive" className="mt-2">
187 <AlertCircle className="h-4 w-4" />
188 <AlertDescription>
gio818da4e2025-05-12 14:45:35 +0000189 Please configure Github Personal Access Token in the Integrations tab.
giod0026612025-05-08 13:00:36 +0000190 </AlertDescription>
191 </Alert>
192 )}
193 </FormItem>
194 )}
195 />
196 </form>
197 </Form>
gio8e74dc02025-06-13 10:19:26 +0000198 <Button
199 disabled={!data.repository?.sshURL || !githubService || disabled}
200 onClick={() => setShowImportModal(true)}
201 >
gioa71316d2025-05-24 09:41:36 +0400202 Scan for services
203 </Button>
gio8e74dc02025-06-13 10:19:26 +0000204 <ImportModal
205 open={showImportModal}
206 onOpenChange={setShowImportModal}
207 initialRepositoryId={data.repository?.id}
208 />
giod0026612025-05-08 13:00:36 +0000209 </>
210 );
211}