Skip to content

Commit a803a14

Browse files
authored
Merge pull request #21 from OpenSourceAGI/claude/fix-instance-launch-7QkuT
2 parents 5e226ae + d089b7e commit a803a14

File tree

3 files changed

+239
-42
lines changed

3 files changed

+239
-42
lines changed

apps/Cloud-Computer-Control-Panel/components/dashboard/manager-list.tsx

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,13 @@ export function ManagerList({ credentials }: ManagerListProps) {
7474
useEffect(() => {
7575
loadManagers()
7676
loadAllRegionsInstances()
77+
78+
// Auto-refresh instances every 10 seconds
79+
const interval = setInterval(() => {
80+
loadAllRegionsInstances()
81+
}, 10000)
82+
83+
return () => clearInterval(interval)
7784
}, [])
7885

7986
const loadManagers = () => {
@@ -378,6 +385,19 @@ export function ManagerList({ credentials }: ManagerListProps) {
378385
const handleInstanceAction = async (instance: EC2Instance, action: string) => {
379386
setActionLoading({ instanceId: instance.instanceId, action })
380387

388+
// Optimistically update the state in the UI
389+
const optimisticState =
390+
action === "start" ? "pending" :
391+
action === "stop" ? "stopping" :
392+
action === "reboot" ? "stopping" :
393+
action === "terminate" ? "shutting-down" : instance.state
394+
395+
setEc2Instances((prev) =>
396+
prev.map((inst) =>
397+
inst.instanceId === instance.instanceId ? { ...inst, state: optimisticState } : inst
398+
)
399+
)
400+
381401
try {
382402
const headers: Record<string, string> = {
383403
"Content-Type": "application/json",
@@ -403,21 +423,33 @@ export function ManagerList({ credentials }: ManagerListProps) {
403423
throw new Error(error.message || `Failed to ${action} instance`)
404424
}
405425

426+
const actionText =
427+
action === "terminate" ? "Terminated" :
428+
action === "reboot" ? "Rebooting" :
429+
action === "start" ? "Starting" :
430+
"Stopping"
431+
406432
toast({
407-
title: `Instance ${action === "terminate" ? "Terminated" : action === "reboot" ? "Rebooting" : action === "start" ? "Started" : "Stopped"}`,
408-
description: `Instance ${instance.instanceId} action completed`,
433+
title: `Instance ${actionText}`,
434+
description: `${instance.instanceId} - Action initiated successfully`,
409435
})
410436

411-
// Refresh instances after action
437+
// Immediately refresh to get the actual state from AWS
412438
setTimeout(() => {
413439
loadAllRegionsInstances()
414-
}, 2000)
440+
}, 1000)
415441
} catch (error) {
416442
toast({
417443
title: "Action Failed",
418444
description: error instanceof Error ? error.message : "Unknown error",
419445
variant: "destructive",
420446
})
447+
// Revert the optimistic update on error
448+
setEc2Instances((prev) =>
449+
prev.map((inst) =>
450+
inst.instanceId === instance.instanceId ? { ...inst, state: instance.state } : inst
451+
)
452+
)
421453
} finally {
422454
setActionLoading(null)
423455
}
@@ -645,7 +677,17 @@ export function ManagerList({ credentials }: ManagerListProps) {
645677
</Badge>
646678
)}
647679
<Badge variant="outline" className={getStateColor(instance.state)}>
648-
{instance.state}
680+
{isLoading ? (
681+
<>
682+
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
683+
{actionLoading?.action === "start" && "starting..."}
684+
{actionLoading?.action === "stop" && "stopping..."}
685+
{actionLoading?.action === "reboot" && "rebooting..."}
686+
{actionLoading?.action === "terminate" && "terminating..."}
687+
</>
688+
) : (
689+
instance.state
690+
)}
649691
</Badge>
650692
{dokploy && (
651693
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">

apps/Cloud-Computer-Control-Panel/components/instance/create-manager.tsx

Lines changed: 117 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ import { Checkbox } from "@/components/ui/checkbox"
1212
import { Badge } from "@/components/ui/badge"
1313
import { Alert, AlertDescription } from "@/components/ui/alert"
1414
import { Slider } from "@/components/ui/slider"
15-
import { Rocket, CheckCircle, HardDrive, Cpu, MapPin } from "lucide-react"
15+
import { Rocket, CheckCircle, HardDrive, Cpu, MapPin, Loader2 } from "lucide-react"
1616
import { useToast } from "@/hooks/use-toast"
1717
import { Textarea } from "@/components/ui/textarea"
1818
import { DockerImageSearch } from "@/components/search/docker-image-search"
1919
import { GitHubRepoSearch } from "@/components/search/github-repo-search"
20+
import { storeSSHKey } from "@/lib/ssh-key-utils"
2021

2122
const INSTANCE_TYPES = [
2223
{ value: "t3.micro", label: "t3.micro", vcpu: 2, ram: 1, cost: 7.59 },
@@ -54,6 +55,7 @@ const DEV_TOOLS = ["git", "docker", "nodejs", "python3", "nginx"]
5455

5556
export function CreateManager({ credentials, onSuccess }: { credentials: any; onSuccess: () => void }) {
5657
const { toast } = useToast()
58+
const [isLaunching, setIsLaunching] = useState(false)
5759
const [formData, setFormData] = useState({
5860
instanceName: "",
5961
region: credentials.region || "us-east-1",
@@ -71,36 +73,116 @@ export function CreateManager({ credentials, onSuccess }: { credentials: any; on
7173
dokployApiKey: "",
7274
})
7375

74-
const handleSubmit = (e: React.FormEvent) => {
76+
const handleSubmit = async (e: React.FormEvent) => {
7577
e.preventDefault()
7678

79+
setIsLaunching(true)
80+
7781
const sanitizedKeyName = formData.keyName && formData.keyName.trim() !== "" ? formData.keyName : ""
7882

83+
const sanitizedConfig = {
84+
...formData,
85+
keyName: sanitizedKeyName,
86+
createdAt: new Date().toISOString(),
87+
}
88+
7989
const newManager = {
8090
managerId: `mgr-${Date.now()}`,
81-
config: {
82-
...formData,
83-
keyName: sanitizedKeyName,
84-
createdAt: new Date().toISOString(),
85-
},
91+
config: sanitizedConfig,
8692
status: {
87-
state: "not-launched",
93+
state: "launching",
8894
instanceId: null,
8995
},
9096
costEstimate: calculateCost(),
9197
}
9298

99+
// Save the manager config to localStorage first
93100
const existing = localStorage.getItem("ec2Managers")
94101
const managers = existing ? JSON.parse(existing) : []
95102
managers.push(newManager)
96103
localStorage.setItem("ec2Managers", JSON.stringify(managers))
97104

98-
toast({
99-
title: "Manager Created",
100-
description: `${formData.instanceName} is ready to launch`,
101-
})
105+
try {
106+
// Launch the instance immediately
107+
const response = await fetch("/api/servers/create", {
108+
method: "POST",
109+
headers: { "Content-Type": "application/json" },
110+
body: JSON.stringify({
111+
accessKeyId: credentials.accessKeyId,
112+
secretAccessKey: credentials.secretAccessKey,
113+
region: sanitizedConfig.region || credentials.region,
114+
config: sanitizedConfig,
115+
}),
116+
})
117+
118+
if (!response.ok) {
119+
const error = await response.json()
120+
throw new Error(error.message || `HTTP ${response.status}`)
121+
}
102122

103-
onSuccess()
123+
const result = await response.json()
124+
125+
// Store SSH key if provided
126+
if (result.sshKey) {
127+
storeSSHKey(result.sshKey.keyName, {
128+
privateKey: result.sshKey.privateKey,
129+
publicKey: result.sshKey.publicKey,
130+
fingerprint: result.sshKey.fingerprint,
131+
})
132+
}
133+
134+
// Update the manager with the launched instance details
135+
const updatedManagers = managers.map((mgr: any) =>
136+
mgr.managerId === newManager.managerId
137+
? {
138+
...mgr,
139+
config: {
140+
...sanitizedConfig,
141+
keyName: result.sshKey?.keyName || sanitizedConfig.keyName,
142+
},
143+
status: {
144+
state: "pending",
145+
instanceId: result.instanceId,
146+
publicIp: result.elasticIp,
147+
allocationId: result.allocationId,
148+
},
149+
}
150+
: mgr
151+
)
152+
localStorage.setItem("ec2Managers", JSON.stringify(updatedManagers))
153+
154+
toast({
155+
title: "Instance Launched Successfully!",
156+
description: `${formData.instanceName} is now starting with IP ${result.elasticIp || "pending"}`,
157+
})
158+
159+
// Only call onSuccess after successful launch
160+
onSuccess()
161+
} catch (error) {
162+
console.error("Launch error:", error)
163+
164+
// Update manager status to failed
165+
const updatedManagers = managers.map((mgr: any) =>
166+
mgr.managerId === newManager.managerId
167+
? {
168+
...mgr,
169+
status: {
170+
state: "launch-failed",
171+
instanceId: null,
172+
},
173+
}
174+
: mgr
175+
)
176+
localStorage.setItem("ec2Managers", JSON.stringify(updatedManagers))
177+
178+
toast({
179+
title: "Launch Failed",
180+
description: error instanceof Error ? error.message : "Unknown error",
181+
variant: "destructive",
182+
})
183+
} finally {
184+
setIsLaunching(false)
185+
}
104186
}
105187

106188
const calculateCost = () => {
@@ -187,6 +269,16 @@ export function CreateManager({ credentials, onSuccess }: { credentials: any; on
187269

188270
return (
189271
<form onSubmit={handleSubmit}>
272+
{isLaunching && (
273+
<Alert className="mb-6 border-blue-500 bg-blue-50 dark:bg-blue-950/20">
274+
<Loader2 className="h-4 w-4 animate-spin text-blue-600" />
275+
<AlertDescription className="text-blue-900 dark:text-blue-100">
276+
<strong>Launching your instance...</strong>
277+
<br />
278+
This may take a few minutes. Please wait while we set up your EC2 instance with Dokploy.
279+
</AlertDescription>
280+
</Alert>
281+
)}
190282
<div className="grid gap-6 md:grid-cols-2">
191283
{/* Basic Configuration */}
192284
<Card>
@@ -311,9 +403,18 @@ export function CreateManager({ credentials, onSuccess }: { credentials: any; on
311403
</div>
312404
</CardContent>
313405
<CardFooter className="border-t pt-6">
314-
<Button type="submit" size="lg" className="w-full">
315-
<Rocket className="h-4 w-4 mr-2" />
316-
Create Manager
406+
<Button type="submit" size="lg" className="w-full" disabled={isLaunching}>
407+
{isLaunching ? (
408+
<>
409+
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
410+
Launching Instance...
411+
</>
412+
) : (
413+
<>
414+
<Rocket className="h-4 w-4 mr-2" />
415+
Launch Instance
416+
</>
417+
)}
317418
</Button>
318419
</CardFooter>
319420
</Card>

0 commit comments

Comments
 (0)