Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down Expand Up @@ -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<string, string> = {
"Content-Type": "application/json",
Expand All @@ -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)
}
Expand Down Expand Up @@ -645,7 +677,17 @@ export function ManagerList({ credentials }: ManagerListProps) {
</Badge>
)}
<Badge variant="outline" className={getStateColor(instance.state)}>
{instance.state}
{isLoading ? (
<>
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
{actionLoading?.action === "start" && "starting..."}
{actionLoading?.action === "stop" && "stopping..."}
{actionLoading?.action === "reboot" && "rebooting..."}
{actionLoading?.action === "terminate" && "terminating..."}
</>
) : (
instance.state
)}
</Badge>
{dokploy && (
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
Expand Down
133 changes: 117 additions & 16 deletions apps/Cloud-Computer-Control-Panel/components/instance/create-manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down Expand Up @@ -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",
Expand All @@ -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 = () => {
Expand Down Expand Up @@ -187,6 +269,16 @@ export function CreateManager({ credentials, onSuccess }: { credentials: any; on

return (
<form onSubmit={handleSubmit}>
{isLaunching && (
<Alert className="mb-6 border-blue-500 bg-blue-50 dark:bg-blue-950/20">
<Loader2 className="h-4 w-4 animate-spin text-blue-600" />
<AlertDescription className="text-blue-900 dark:text-blue-100">
<strong>Launching your instance...</strong>
<br />
This may take a few minutes. Please wait while we set up your EC2 instance with Dokploy.
</AlertDescription>
</Alert>
)}
<div className="grid gap-6 md:grid-cols-2">
{/* Basic Configuration */}
<Card>
Expand Down Expand Up @@ -311,9 +403,18 @@ export function CreateManager({ credentials, onSuccess }: { credentials: any; on
</div>
</CardContent>
<CardFooter className="border-t pt-6">
<Button type="submit" size="lg" className="w-full">
<Rocket className="h-4 w-4 mr-2" />
Create Manager
<Button type="submit" size="lg" className="w-full" disabled={isLaunching}>
{isLaunching ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Launching Instance...
</>
) : (
<>
<Rocket className="h-4 w-4 mr-2" />
Launch Instance
</>
)}
</Button>
</CardFooter>
</Card>
Expand Down
Loading