6.1 KB214 lines
Blame
1"use client";
2
3import { useState, useRef } from "react";
4
5interface GitImportFormProps {
6 owner: string;
7 repo: string;
8}
9
10type ImportState = "idle" | "importing" | "done" | "error";
11
12export function GitImportForm({ owner, repo }: GitImportFormProps) {
13 const [url, setUrl] = useState("");
14 const [state, setState] = useState<ImportState>("idle");
15 const [currentStep, setCurrentStep] = useState("");
16 const [logs, setLogs] = useState<string[]>([]);
17 const [error, setError] = useState("");
18 const logRef = useRef<HTMLPreElement>(null);
19
20 const handleImport = async () => {
21 if (!url.trim()) return;
22 setState("importing");
23 setCurrentStep("Starting import...");
24 setLogs([]);
25 setError("");
26
27 const token =
28 typeof window !== "undefined"
29 ? localStorage.getItem("grove_hub_token")
30 : null;
31
32 try {
33 const res = await fetch(`/api/repos/${owner}/${repo}/import`, {
34 method: "POST",
35 headers: {
36 "Content-Type": "application/json",
37 ...(token ? { Authorization: `Bearer ${token}` } : {}),
38 },
39 body: JSON.stringify({ url: url.trim() }),
40 });
41
42 if (!res.ok || !res.body) {
43 const body = await res.json().catch(() => ({}));
44 setError(body.error ?? `HTTP ${res.status}`);
45 setState("error");
46 return;
47 }
48
49 const reader = res.body.getReader();
50 const decoder = new TextDecoder();
51 let buffer = "";
52
53 while (true) {
54 const { done, value } = await reader.read();
55 if (done) break;
56
57 buffer += decoder.decode(value, { stream: true });
58 const lines = buffer.split("\n");
59 buffer = lines.pop() ?? "";
60
61 let event = "";
62 for (const line of lines) {
63 if (line.startsWith("event: ")) {
64 event = line.slice(7);
65 } else if (line.startsWith("data: ")) {
66 const data = JSON.parse(line.slice(6));
67 if (event === "progress") {
68 setCurrentStep(data.message);
69 } else if (event === "log") {
70 setLogs((prev) => {
71 const next = [...prev, data.line];
72 // Auto-scroll
73 requestAnimationFrame(() => {
74 if (logRef.current) {
75 logRef.current.scrollTop = logRef.current.scrollHeight;
76 }
77 });
78 return next;
79 });
80 } else if (event === "done") {
81 setState("done");
82 } else if (event === "error") {
83 setError(data.message);
84 setState("error");
85 }
86 }
87 }
88 }
89
90 // If stream ended without explicit done/error
91 if (state === "importing") {
92 setState("done");
93 }
94 } catch (err: any) {
95 setError(err.message ?? "Connection failed");
96 setState("error");
97 }
98 };
99
100 if (state === "done") {
101 return (
102 <div
103 className="p-3"
104 style={{
105 backgroundColor: "var(--bg-inset)",
106 border: "1px solid var(--border-subtle)",
107 }}
108 >
109 <p className="text-sm" style={{ color: "var(--text-secondary)" }}>
110 Import complete
111 </p>
112 <p className="text-xs mt-1" style={{ color: "var(--text-faint)" }}>
113 Repository imported successfully. Reloading...
114 </p>
115 {/* Reload so the RSC page re-fetches and shows the tree */}
116 <script
117 dangerouslySetInnerHTML={{
118 __html: `setTimeout(()=>location.reload(),1500)`,
119 }}
120 />
121 </div>
122 );
123 }
124
125 return (
126 <div
127 className="p-3"
128 style={{
129 backgroundColor: "var(--bg-inset)",
130 border: "1px solid var(--border-subtle)",
131 }}
132 >
133 <p className="text-xs mb-1" style={{ color: "var(--text-secondary)" }}>
134 Import from Git
135 </p>
136 <p className="text-xs mb-2" style={{ color: "var(--text-faint)" }}>
137 Clone an existing Git repository with full history.
138 </p>
139
140 <div className="flex gap-2">
141 <input
142 type="text"
143 value={url}
144 onChange={(e) => setUrl(e.target.value)}
145 onKeyDown={(e) => {
146 if (e.key === "Enter" && state === "idle") handleImport();
147 }}
148 placeholder="https://github.com/owner/repo.git"
149 disabled={state === "importing"}
150 className="flex-1 text-xs px-2 py-1.5"
151 style={{
152 backgroundColor: "var(--bg-card)",
153 border: "1px solid var(--border-subtle)",
154 color: "var(--text-primary)",
155 outline: "none",
156 font: "inherit",
157 }}
158 />
159 <button
160 onClick={handleImport}
161 disabled={state === "importing" || !url.trim()}
162 className="text-xs px-3 py-1.5 shrink-0"
163 style={{
164 backgroundColor:
165 state === "importing" || !url.trim()
166 ? "var(--bg-card)"
167 : "var(--accent)",
168 color:
169 state === "importing" || !url.trim()
170 ? "var(--text-faint)"
171 : "var(--accent-text)",
172 border: "1px solid var(--border-subtle)",
173 cursor:
174 state === "importing" || !url.trim()
175 ? "default"
176 : "pointer",
177 font: "inherit",
178 }}
179 >
180 {state === "importing" ? "Importing..." : "Import"}
181 </button>
182 </div>
183
184 {error && (
185 <p className="text-xs mt-2" style={{ color: "var(--text-danger, #e55)" }}>
186 {error}
187 </p>
188 )}
189
190 {state === "importing" && (
191 <div className="mt-3">
192 <p className="text-xs mb-1" style={{ color: "var(--text-muted)" }}>
193 {currentStep}
194 </p>
195 {logs.length > 0 && (
196 <pre
197 ref={logRef}
198 className="text-xs overflow-auto"
199 style={{
200 color: "var(--text-faint)",
201 maxHeight: 200,
202 whiteSpace: "pre-wrap",
203 wordBreak: "break-all",
204 }}
205 >
206 {logs.join("\n")}
207 </pre>
208 )}
209 </div>
210 )}
211 </div>
212 );
213}
214