blob: cf4b586528b056c3927778bcfc25ad508f62726d [file] [log] [blame]
gio69148322025-06-19 23:16:12 +04001import { useProjectId, useGithubService, useStateStore, useGeminiService } from "@/lib/state";
giod0026612025-05-08 13:00:36 +00002import { Form, FormControl, FormField, FormItem, FormMessage } from "./components/ui/form";
3import { Input } from "./components/ui/input";
4import { useForm } from "react-hook-form";
5import { z } from "zod";
6import { zodResolver } from "@hookform/resolvers/zod";
7import { Button } from "./components/ui/button";
8import { useToast } from "@/hooks/use-toast";
gio02f1cad2025-05-13 11:51:55 +00009import { CircleCheck, CircleX } from "lucide-react";
giod0026612025-05-08 13:00:36 +000010import { useState, useCallback } from "react";
gio7f98e772025-05-07 11:00:14 +000011
gio69148322025-06-19 23:16:12 +040012const githubSchema = z.object({
giod0026612025-05-08 13:00:36 +000013 githubToken: z.string().min(1, "GitHub token is required"),
gio7f98e772025-05-07 11:00:14 +000014});
15
gio69148322025-06-19 23:16:12 +040016const geminiSchema = z.object({
17 geminiApiKey: z.string().min(1, "Gemini API token is required"),
18});
19
gio7f98e772025-05-07 11:00:14 +000020export function Integrations() {
giod0026612025-05-08 13:00:36 +000021 const { toast } = useToast();
22 const store = useStateStore();
23 const projectId = useProjectId();
gio69148322025-06-19 23:16:12 +040024 const [isEditingGithub, setIsEditingGithub] = useState(false);
25 const [isEditingGemini, setIsEditingGemini] = useState(false);
giod0026612025-05-08 13:00:36 +000026 const githubService = useGithubService();
gio69148322025-06-19 23:16:12 +040027 const geminiService = useGeminiService();
giod0026612025-05-08 13:00:36 +000028 const [isSaving, setIsSaving] = useState(false);
gio7f98e772025-05-07 11:00:14 +000029
gio69148322025-06-19 23:16:12 +040030 const githubForm = useForm<z.infer<typeof githubSchema>>({
31 resolver: zodResolver(githubSchema),
giod0026612025-05-08 13:00:36 +000032 mode: "onChange",
33 });
gio7f98e772025-05-07 11:00:14 +000034
gio69148322025-06-19 23:16:12 +040035 const geminiForm = useForm<z.infer<typeof geminiSchema>>({
36 resolver: zodResolver(geminiSchema),
37 mode: "onChange",
38 });
39
40 const onGithubSubmit = useCallback(
41 async (data: z.infer<typeof githubSchema>) => {
giod0026612025-05-08 13:00:36 +000042 if (!projectId) return;
gio7f98e772025-05-07 11:00:14 +000043
giod0026612025-05-08 13:00:36 +000044 setIsSaving(true);
gio7f98e772025-05-07 11:00:14 +000045
giod0026612025-05-08 13:00:36 +000046 try {
47 const response = await fetch(`/api/project/${projectId}/github-token`, {
48 method: "POST",
49 headers: {
50 "Content-Type": "application/json",
51 },
52 body: JSON.stringify({ githubToken: data.githubToken }),
53 });
gio7f98e772025-05-07 11:00:14 +000054
giod0026612025-05-08 13:00:36 +000055 if (!response.ok) {
56 throw new Error("Failed to save GitHub token");
57 }
gio7f98e772025-05-07 11:00:14 +000058
giod0026612025-05-08 13:00:36 +000059 await store.refreshEnv();
gio69148322025-06-19 23:16:12 +040060 setIsEditingGithub(false);
61 githubForm.reset();
giod0026612025-05-08 13:00:36 +000062 toast({
63 title: "GitHub token saved successfully",
64 });
65 } catch (error) {
66 toast({
67 variant: "destructive",
68 title: "Failed to save GitHub token",
69 description: error instanceof Error ? error.message : "Unknown error",
70 });
71 } finally {
72 setIsSaving(false);
73 }
74 },
gio69148322025-06-19 23:16:12 +040075 [projectId, store, githubForm, toast, setIsEditingGithub, setIsSaving],
giod0026612025-05-08 13:00:36 +000076 );
gio7f98e772025-05-07 11:00:14 +000077
gio69148322025-06-19 23:16:12 +040078 const onGeminiSubmit = useCallback(
79 async (data: z.infer<typeof geminiSchema>) => {
80 if (!projectId) return;
81 setIsSaving(true);
82 try {
83 const response = await fetch(`/api/project/${projectId}/gemini-token`, {
84 method: "POST",
85 headers: {
86 "Content-Type": "application/json",
87 },
88 body: JSON.stringify({ geminiApiKey: data.geminiApiKey }),
89 });
90 if (!response.ok) {
91 throw new Error("Failed to save Gemini token");
92 }
93 await store.refreshEnv();
94 setIsEditingGemini(false);
95 geminiForm.reset();
96 toast({
97 title: "Gemini token saved successfully",
98 });
99 } catch (error) {
100 toast({
101 variant: "destructive",
102 title: "Failed to save Gemini token",
103 description: error instanceof Error ? error.message : "Unknown error",
104 });
105 } finally {
106 setIsSaving(false);
107 }
108 },
109 [projectId, store, geminiForm, toast, setIsEditingGemini, setIsSaving],
110 );
111
112 const handleCancelGithub = () => {
113 setIsEditingGithub(false);
114 githubForm.reset();
115 };
116
117 const handleCancelGemini = () => {
118 setIsEditingGemini(false);
119 geminiForm.reset();
giod0026612025-05-08 13:00:36 +0000120 };
gio7f98e772025-05-07 11:00:14 +0000121
giod0026612025-05-08 13:00:36 +0000122 return (
gio02f1cad2025-05-13 11:51:55 +0000123 <div className="px-4 py-1">
124 <div className="flex flex-col gap-1">
125 <div className="flex flex-row items-center gap-1">
126 {githubService ? <CircleCheck /> : <CircleX />}
127 <div>Github</div>
giod0026612025-05-08 13:00:36 +0000128 </div>
gio3a921b82025-05-10 07:36:09 +0000129
gio69148322025-06-19 23:16:12 +0400130 {!!githubService && !isEditingGithub && (
131 <Button variant="outline" className="w-fit" onClick={() => setIsEditingGithub(true)}>
gio3a921b82025-05-10 07:36:09 +0000132 Update Access Token
133 </Button>
134 )}
135
gio69148322025-06-19 23:16:12 +0400136 {(!githubService || isEditingGithub) && (
gio02f1cad2025-05-13 11:51:55 +0000137 <div className="flex flex-row items-center gap-1 text-sm">
gio33046722025-05-16 14:49:55 +0000138 <div>
139 Follow the link to generate new PAT:{" "}
140 <a href="https://github.com/settings/personal-access-tokens" target="_blank">
141 https://github.com/settings/personal-access-tokens
142 </a>
143 <br />
144 Grant following <b>Repository</b> permissions:
145 <ul>
146 <li>
147 <b>Contents</b> - Read-only access so dodo can clone the repository
148 </li>
149 <li>
150 <b>Metadata</b> - Read-only access so dodo can search for repositories
151 </li>
152 <li>
gioeb148c82025-05-19 16:17:22 +0000153 <b>Webhooks</b> - Read and write access so dodo can register webhooks with Github
154 and receive updates
155 </li>
156 <li>
gio33046722025-05-16 14:49:55 +0000157 <b>Administration</b> - Read and write access so dodo automatically add deploy keys
158 to repositories
159 </li>
160 </ul>
161 </div>
gio02f1cad2025-05-13 11:51:55 +0000162 </div>
163 )}
gio69148322025-06-19 23:16:12 +0400164 {(!githubService || isEditingGithub) && (
165 <Form {...githubForm}>
166 <form className="space-y-2" onSubmit={githubForm.handleSubmit(onGithubSubmit)}>
gio3a921b82025-05-10 07:36:09 +0000167 <FormField
gio69148322025-06-19 23:16:12 +0400168 control={githubForm.control}
gio3a921b82025-05-10 07:36:09 +0000169 name="githubToken"
170 render={({ field }) => (
171 <FormItem>
172 <FormControl>
173 <Input
174 type="password"
gio02f1cad2025-05-13 11:51:55 +0000175 placeholder="Personal Access Token"
176 className="w-1/4"
gio3a921b82025-05-10 07:36:09 +0000177 {...field}
178 />
179 </FormControl>
180 <FormMessage />
181 </FormItem>
182 )}
183 />
gio02f1cad2025-05-13 11:51:55 +0000184 <div className="flex flex-row items-center gap-1">
gio3a921b82025-05-10 07:36:09 +0000185 <Button type="submit" disabled={isSaving}>
186 {isSaving ? "Saving..." : "Save"}
187 </Button>
188 {!!githubService && (
gio69148322025-06-19 23:16:12 +0400189 <Button
190 type="button"
191 variant="outline"
192 onClick={handleCancelGithub}
193 disabled={isSaving}
194 >
195 Cancel
196 </Button>
197 )}
198 </div>
199 </form>
200 </Form>
201 )}
202 </div>
203 <div className="flex flex-col gap-1">
204 <div className="flex flex-row items-center gap-1">
205 {geminiService ? <CircleCheck /> : <CircleX />}
206 <div>Gemini</div>
207 </div>
208
209 {!!geminiService && !isEditingGemini && (
210 <Button variant="outline" className="w-fit" onClick={() => setIsEditingGemini(true)}>
211 Update API Key
212 </Button>
213 )}
214
215 {(!geminiService || isEditingGemini) && (
216 <div className="flex flex-row items-center gap-1 text-sm">
217 <div>
218 Follow the link to generate new API Key:{" "}
219 <a href="https://aistudio.google.com/app/apikey" target="_blank">
220 https://aistudio.google.com/app/apikey
221 </a>
222 </div>
223 </div>
224 )}
225 {(!geminiService || isEditingGemini) && (
226 <Form {...geminiForm}>
227 <form className="space-y-2" onSubmit={geminiForm.handleSubmit(onGeminiSubmit)}>
228 <FormField
229 control={geminiForm.control}
230 name="geminiApiKey"
231 render={({ field }) => (
232 <FormItem>
233 <FormControl>
234 <Input
235 type="password"
236 placeholder="Gemini API Token"
237 className="w-1/4"
238 {...field}
239 />
240 </FormControl>
241 <FormMessage />
242 </FormItem>
243 )}
244 />
245 <div className="flex flex-row items-center gap-1">
246 <Button type="submit" disabled={isSaving}>
247 {isSaving ? "Saving..." : "Save"}
248 </Button>
249 {!!geminiService && (
250 <Button
251 type="button"
252 variant="outline"
253 onClick={handleCancelGemini}
254 disabled={isSaving}
255 >
gio3a921b82025-05-10 07:36:09 +0000256 Cancel
257 </Button>
258 )}
259 </div>
260 </form>
261 </Form>
262 )}
giod0026612025-05-08 13:00:36 +0000263 </div>
264 </div>
265 );
266}