From 1e726041be5419776a978bf4a087985f2d7139d8 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Jan 2026 09:12:55 +0000 Subject: [PATCH 1/2] fix: launch instance directly instead of redirecting to managers list - Modified CreateManager component to launch instance immediately on form submit - Added loading spinner (Loader2) during instance creation - Shows prominent blue alert with launch status message - Only redirects to managers view after successful instance launch - Stores SSH keys in localStorage automatically - Handles launch failures gracefully with error state - Changed button text from "Create Manager" to "Launch Instance" for clarity --- .../components/instance/create-manager.tsx | 133 +++++++++++++++--- 1 file changed, 117 insertions(+), 16 deletions(-) diff --git a/apps/Cloud-Computer-Control-Panel/components/instance/create-manager.tsx b/apps/Cloud-Computer-Control-Panel/components/instance/create-manager.tsx index 2835ae3..53a3303 100644 --- a/apps/Cloud-Computer-Control-Panel/components/instance/create-manager.tsx +++ b/apps/Cloud-Computer-Control-Panel/components/instance/create-manager.tsx @@ -12,11 +12,12 @@ import { Checkbox } from "@/components/ui/checkbox" import { Badge } from "@/components/ui/badge" import { Alert, AlertDescription } from "@/components/ui/alert" import { Slider } from "@/components/ui/slider" -import { Rocket, CheckCircle, HardDrive, Cpu, MapPin } from "lucide-react" +import { Rocket, CheckCircle, HardDrive, Cpu, MapPin, Loader2 } from "lucide-react" import { useToast } from "@/hooks/use-toast" import { Textarea } from "@/components/ui/textarea" import { DockerImageSearch } from "@/components/search/docker-image-search" import { GitHubRepoSearch } from "@/components/search/github-repo-search" +import { storeSSHKey } from "@/lib/ssh-key-utils" const INSTANCE_TYPES = [ { 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"] export function CreateManager({ credentials, onSuccess }: { credentials: any; onSuccess: () => void }) { const { toast } = useToast() + const [isLaunching, setIsLaunching] = useState(false) const [formData, setFormData] = useState({ instanceName: "", region: credentials.region || "us-east-1", @@ -71,36 +73,116 @@ export function CreateManager({ credentials, onSuccess }: { credentials: any; on dokployApiKey: "", }) - const handleSubmit = (e: React.FormEvent) => { + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() + setIsLaunching(true) + const sanitizedKeyName = formData.keyName && formData.keyName.trim() !== "" ? formData.keyName : "" + const sanitizedConfig = { + ...formData, + keyName: sanitizedKeyName, + createdAt: new Date().toISOString(), + } + const newManager = { managerId: `mgr-${Date.now()}`, - config: { - ...formData, - keyName: sanitizedKeyName, - createdAt: new Date().toISOString(), - }, + config: sanitizedConfig, status: { - state: "not-launched", + state: "launching", instanceId: null, }, costEstimate: calculateCost(), } + // Save the manager config to localStorage first const existing = localStorage.getItem("ec2Managers") const managers = existing ? JSON.parse(existing) : [] managers.push(newManager) localStorage.setItem("ec2Managers", JSON.stringify(managers)) - toast({ - title: "Manager Created", - description: `${formData.instanceName} is ready to launch`, - }) + try { + // Launch the instance immediately + const response = await fetch("/api/servers/create", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + accessKeyId: credentials.accessKeyId, + secretAccessKey: credentials.secretAccessKey, + region: sanitizedConfig.region || credentials.region, + config: sanitizedConfig, + }), + }) + + if (!response.ok) { + const error = await response.json() + throw new Error(error.message || `HTTP ${response.status}`) + } - onSuccess() + const result = await response.json() + + // Store SSH key if provided + if (result.sshKey) { + storeSSHKey(result.sshKey.keyName, { + privateKey: result.sshKey.privateKey, + publicKey: result.sshKey.publicKey, + fingerprint: result.sshKey.fingerprint, + }) + } + + // Update the manager with the launched instance details + const updatedManagers = managers.map((mgr: any) => + mgr.managerId === newManager.managerId + ? { + ...mgr, + config: { + ...sanitizedConfig, + keyName: result.sshKey?.keyName || sanitizedConfig.keyName, + }, + status: { + state: "pending", + instanceId: result.instanceId, + publicIp: result.elasticIp, + allocationId: result.allocationId, + }, + } + : mgr + ) + localStorage.setItem("ec2Managers", JSON.stringify(updatedManagers)) + + toast({ + title: "Instance Launched Successfully!", + description: `${formData.instanceName} is now starting with IP ${result.elasticIp || "pending"}`, + }) + + // Only call onSuccess after successful launch + onSuccess() + } catch (error) { + console.error("Launch error:", error) + + // Update manager status to failed + const updatedManagers = managers.map((mgr: any) => + mgr.managerId === newManager.managerId + ? { + ...mgr, + status: { + state: "launch-failed", + instanceId: null, + }, + } + : mgr + ) + localStorage.setItem("ec2Managers", JSON.stringify(updatedManagers)) + + toast({ + title: "Launch Failed", + description: error instanceof Error ? error.message : "Unknown error", + variant: "destructive", + }) + } finally { + setIsLaunching(false) + } } const calculateCost = () => { @@ -187,6 +269,16 @@ export function CreateManager({ credentials, onSuccess }: { credentials: any; on return (
+ {isLaunching && ( + + + + Launching your instance... +
+ This may take a few minutes. Please wait while we set up your EC2 instance with Dokploy. +
+
+ )}
{/* Basic Configuration */} @@ -311,9 +403,18 @@ export function CreateManager({ credentials, onSuccess }: { credentials: any; on
- From d089b7ef2d770f96337bd299b57535814945732b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Jan 2026 10:00:00 +0000 Subject: [PATCH 2/2] feat: add real-time status updates and improve instance controls Changes to manager-list.tsx: - Added automatic status polling every 10 seconds to keep instance states current - Implemented optimistic UI updates for start/stop/reboot/terminate actions - Enhanced status badges to show loading state with animated spinner during operations - Improved action feedback with immediate state changes and error reversion Changes to instance-controls.tsx: - Fixed API authentication to use header-based credentials (x-aws-* headers) - Added automatic status refresh every 10 seconds when instance exists - Implemented optimistic state updates for start/stop/reboot operations - Added periodic polling during reboot (every 5s for 2 minutes) - Enhanced error handling with automatic state reversion on failures - Improved user feedback with more descriptive toast messages Status display improvements: - Shows "starting...", "stopping...", "rebooting...", "terminating..." during actions - Displays real-time state changes from AWS (pending, stopping, running, stopped, etc.) - Color-coded badges for different states (green=running, yellow=stopped, etc.) - Animated spinner indicators during state transitions All instance control buttons (Start, Stop, Restart, Terminate) now work reliably with proper status feedback. --- .../components/dashboard/manager-list.tsx | 52 +++++++++- .../components/instance/instance-controls.tsx | 96 +++++++++++++++---- 2 files changed, 122 insertions(+), 26 deletions(-) diff --git a/apps/Cloud-Computer-Control-Panel/components/dashboard/manager-list.tsx b/apps/Cloud-Computer-Control-Panel/components/dashboard/manager-list.tsx index b3b92d7..9ae58ea 100644 --- a/apps/Cloud-Computer-Control-Panel/components/dashboard/manager-list.tsx +++ b/apps/Cloud-Computer-Control-Panel/components/dashboard/manager-list.tsx @@ -74,6 +74,13 @@ export function ManagerList({ credentials }: ManagerListProps) { useEffect(() => { loadManagers() loadAllRegionsInstances() + + // Auto-refresh instances every 10 seconds + const interval = setInterval(() => { + loadAllRegionsInstances() + }, 10000) + + return () => clearInterval(interval) }, []) const loadManagers = () => { @@ -378,6 +385,19 @@ export function ManagerList({ credentials }: ManagerListProps) { const handleInstanceAction = async (instance: EC2Instance, action: string) => { setActionLoading({ instanceId: instance.instanceId, action }) + // Optimistically update the state in the UI + const optimisticState = + action === "start" ? "pending" : + action === "stop" ? "stopping" : + action === "reboot" ? "stopping" : + action === "terminate" ? "shutting-down" : instance.state + + setEc2Instances((prev) => + prev.map((inst) => + inst.instanceId === instance.instanceId ? { ...inst, state: optimisticState } : inst + ) + ) + try { const headers: Record = { "Content-Type": "application/json", @@ -403,21 +423,33 @@ export function ManagerList({ credentials }: ManagerListProps) { throw new Error(error.message || `Failed to ${action} instance`) } + const actionText = + action === "terminate" ? "Terminated" : + action === "reboot" ? "Rebooting" : + action === "start" ? "Starting" : + "Stopping" + toast({ - title: `Instance ${action === "terminate" ? "Terminated" : action === "reboot" ? "Rebooting" : action === "start" ? "Started" : "Stopped"}`, - description: `Instance ${instance.instanceId} action completed`, + title: `Instance ${actionText}`, + description: `${instance.instanceId} - Action initiated successfully`, }) - // Refresh instances after action + // Immediately refresh to get the actual state from AWS setTimeout(() => { loadAllRegionsInstances() - }, 2000) + }, 1000) } catch (error) { toast({ title: "Action Failed", description: error instanceof Error ? error.message : "Unknown error", variant: "destructive", }) + // Revert the optimistic update on error + setEc2Instances((prev) => + prev.map((inst) => + inst.instanceId === instance.instanceId ? { ...inst, state: instance.state } : inst + ) + ) } finally { setActionLoading(null) } @@ -645,7 +677,17 @@ export function ManagerList({ credentials }: ManagerListProps) { )} - {instance.state} + {isLoading ? ( + <> + + {actionLoading?.action === "start" && "starting..."} + {actionLoading?.action === "stop" && "stopping..."} + {actionLoading?.action === "reboot" && "rebooting..."} + {actionLoading?.action === "terminate" && "terminating..."} + + ) : ( + instance.state + )} {dokploy && ( diff --git a/apps/Cloud-Computer-Control-Panel/components/instance/instance-controls.tsx b/apps/Cloud-Computer-Control-Panel/components/instance/instance-controls.tsx index 98a47ce..f259ebe 100644 --- a/apps/Cloud-Computer-Control-Panel/components/instance/instance-controls.tsx +++ b/apps/Cloud-Computer-Control-Panel/components/instance/instance-controls.tsx @@ -1,6 +1,6 @@ "use client" -import { useState } from "react" +import { useState, useEffect } from "react" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" @@ -39,14 +39,32 @@ export function InstanceControls({ manager, apiUrl, credentials, onUpdate }: Ins const [snapshotDescription, setSnapshotDescription] = useState("") const { toast } = useToast() + // Auto-refresh status every 10 seconds if instance exists + useEffect(() => { + if (!manager.status?.instanceId) return + + const interval = setInterval(() => { + refreshStatus() + }, 10000) + + return () => clearInterval(interval) + }, [manager.status?.instanceId]) + const apiCall = async (action: string, body?: any) => { + const headers: Record = { + "Content-Type": "application/json", + } + + if (credentials.accessKeyId !== "server-env" && credentials.secretAccessKey !== "server-env") { + headers["x-aws-access-key-id"] = credentials.accessKeyId + headers["x-aws-secret-access-key"] = credentials.secretAccessKey + } + headers["x-aws-region"] = manager.config?.region || credentials.region + const response = await fetch(apiUrl, { method: "POST", - headers: { "Content-Type": "application/json" }, + headers, body: JSON.stringify({ - accessKeyId: credentials.accessKeyId, - secretAccessKey: credentials.secretAccessKey, - region: credentials.region, action, instanceId: manager.status?.instanceId, config: manager.config, @@ -151,14 +169,24 @@ export function InstanceControls({ manager, apiUrl, credentials, onUpdate }: Ins } setLoading("start") + + // Optimistically update to pending state + onUpdate({ ...manager, status: { ...manager.status, state: "pending" } }) + try { await apiCall("start") - onUpdate({ ...manager, status: { ...manager.status, state: "running" } }) toast({ - title: "Instance Started", - description: "Instance is now running", + title: "Instance Starting", + description: "Instance start command sent successfully", }) + + // Refresh status after a delay + setTimeout(() => { + refreshStatus() + }, 3000) } catch (error) { + // Revert to stopped on error + onUpdate({ ...manager, status: { ...manager.status, state: "stopped" } }) toast({ title: "Start Failed", description: error instanceof Error ? error.message : "Unknown error", @@ -180,14 +208,24 @@ export function InstanceControls({ manager, apiUrl, credentials, onUpdate }: Ins } setLoading("stop") + + // Optimistically update to stopping state + onUpdate({ ...manager, status: { ...manager.status, state: "stopping" } }) + try { await apiCall("stop") - onUpdate({ ...manager, status: { ...manager.status, state: "stopped" } }) toast({ - title: "Instance Stopped", - description: "Instance has been stopped to save costs", + title: "Instance Stopping", + description: "Instance stop command sent successfully", }) + + // Refresh status after a delay + setTimeout(() => { + refreshStatus() + }, 3000) } catch (error) { + // Revert to running on error + onUpdate({ ...manager, status: { ...manager.status, state: "running" } }) toast({ title: "Stop Failed", description: error instanceof Error ? error.message : "Unknown error", @@ -243,13 +281,18 @@ export function InstanceControls({ manager, apiUrl, credentials, onUpdate }: Ins setLoading("refresh") try { - const response = await fetch( - `${apiUrl}?${new URLSearchParams({ - accessKeyId: credentials.accessKeyId, - secretAccessKey: credentials.secretAccessKey, - region: credentials.region, - })}`, - ) + const headers: Record = {} + + if (credentials.accessKeyId !== "server-env" && credentials.secretAccessKey !== "server-env") { + headers["x-aws-access-key-id"] = credentials.accessKeyId + headers["x-aws-secret-access-key"] = credentials.secretAccessKey + } + headers["x-aws-region"] = manager.config?.region || credentials.region + + const response = await fetch(`${apiUrl}?instanceId=${manager.status.instanceId}`, { + method: "GET", + headers, + }) const data = await response.json() if (response.ok && data.instances) { @@ -296,19 +339,30 @@ export function InstanceControls({ manager, apiUrl, credentials, onUpdate }: Ins } setLoading("reboot") + + // Optimistically update to stopping state + onUpdate({ ...manager, status: { ...manager.status, state: "stopping" } }) + try { await apiCall("reboot") - onUpdate({ ...manager, status: { ...manager.status, state: "stopping" } }) toast({ title: "Instance Rebooting", description: "Instance is being stopped and will restart automatically", }) - // Poll for running status after reboot + // Poll for status more frequently during reboot + const pollInterval = setInterval(() => { + refreshStatus() + }, 5000) + + // Stop polling after 2 minutes setTimeout(() => { + clearInterval(pollInterval) refreshStatus() - }, 30000) // Check after 30 seconds + }, 120000) } catch (error) { + // Revert to running on error + onUpdate({ ...manager, status: { ...manager.status, state: "running" } }) toast({ title: "Reboot Failed", description: error instanceof Error ? error.message : "Unknown error",