First swing.

This commit is contained in:
2025-10-12 17:44:54 +00:00
parent 33454bc4e1
commit e5bd3c36f5
106 changed files with 7592 additions and 1270 deletions

View File

@@ -1,39 +1,171 @@
import { useState } from "react";
import { useState, useEffect } from "react";
import { Card } from "./ui/card";
import { Button } from "./ui/button";
import { Cloud, HelpCircle, Edit2, Check, X } from "lucide-react";
import { Cloud, HelpCircle, Edit2, Check, X, Loader2, AlertCircle } from "lucide-react";
import { Input, Label } from "./ui";
import { useInstanceConfig, useInstanceContext } from "../hooks";
interface CloudConfig {
domain: string;
internalDomain: string;
dhcpRange: string;
dns: {
ip: string;
};
router: {
ip: string;
};
dnsmasq: {
interface: string;
};
}
export function CloudComponent() {
const [domainValue, setDomainValue] = useState("cloud.payne.io");
const [internalDomainValue, setInternalDomainValue] = useState(
"internal.cloud.payne.io"
);
const { currentInstance } = useInstanceContext();
const { config: fullConfig, isLoading, error, updateConfig, isUpdating } = useInstanceConfig(currentInstance);
// Extract cloud config from full config
const config = fullConfig?.cloud as CloudConfig | undefined;
const [editingDomains, setEditingDomains] = useState(false);
const [editingNetwork, setEditingNetwork] = useState(false);
const [formValues, setFormValues] = useState<CloudConfig | null>(null);
const [tempDomain, setTempDomain] = useState(domainValue);
const [tempInternalDomain, setTempInternalDomain] =
useState(internalDomainValue);
// Sync form values when config loads
useEffect(() => {
if (config && !formValues) {
setFormValues(config as CloudConfig);
}
}, [config, formValues]);
const handleDomainsEdit = () => {
setTempDomain(domainValue);
setTempInternalDomain(internalDomainValue);
setEditingDomains(true);
if (config) {
setFormValues(config as CloudConfig);
setEditingDomains(true);
}
};
const handleDomainsSave = () => {
setDomainValue(tempDomain);
setInternalDomainValue(tempInternalDomain);
setEditingDomains(false);
const handleNetworkEdit = () => {
if (config) {
setFormValues(config as CloudConfig);
setEditingNetwork(true);
}
};
const handleDomainsSave = async () => {
if (!formValues || !fullConfig) return;
try {
// Update only the cloud section, preserving other config sections
await updateConfig({
...fullConfig,
cloud: {
domain: formValues.domain,
internalDomain: formValues.internalDomain,
dhcpRange: formValues.dhcpRange,
dns: formValues.dns,
router: formValues.router,
dnsmasq: formValues.dnsmasq,
},
});
setEditingDomains(false);
} catch (err) {
console.error('Failed to save domains:', err);
}
};
const handleNetworkSave = async () => {
if (!formValues || !fullConfig) return;
try {
// Update only the cloud section, preserving other config sections
await updateConfig({
...fullConfig,
cloud: {
domain: formValues.domain,
internalDomain: formValues.internalDomain,
dhcpRange: formValues.dhcpRange,
dns: formValues.dns,
router: formValues.router,
dnsmasq: formValues.dnsmasq,
},
});
setEditingNetwork(false);
} catch (err) {
console.error('Failed to save network settings:', err);
}
};
const handleDomainsCancel = () => {
setTempDomain(domainValue);
setTempInternalDomain(internalDomainValue);
setFormValues(config as CloudConfig);
setEditingDomains(false);
};
const handleNetworkCancel = () => {
setFormValues(config as CloudConfig);
setEditingNetwork(false);
};
const updateFormValue = (path: string, value: string) => {
if (!formValues) return;
setFormValues(prev => {
if (!prev) return prev;
// Handle nested paths like "dns.ip"
const keys = path.split('.');
if (keys.length === 1) {
return { ...prev, [keys[0]]: value };
}
// Handle nested object updates
const [parentKey, childKey] = keys;
return {
...prev,
[parentKey]: {
...(prev[parentKey as keyof CloudConfig] as Record<string, unknown>),
[childKey]: value,
},
};
});
};
// Show message if no instance is selected
if (!currentInstance) {
return (
<Card className="p-8 text-center">
<Cloud className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<h3 className="text-lg font-medium mb-2">No Instance Selected</h3>
<p className="text-muted-foreground mb-4">
Please select or create an instance to manage cloud configuration.
</p>
</Card>
);
}
// Show loading state
if (isLoading || !formValues) {
return (
<Card className="p-8 text-center">
<Loader2 className="h-12 w-12 text-primary mx-auto mb-4 animate-spin" />
<p className="text-muted-foreground">Loading cloud configuration...</p>
</Card>
);
}
// Show error state
if (error) {
return (
<Card className="p-8 text-center">
<AlertCircle className="h-12 w-12 text-red-500 mx-auto mb-4" />
<h3 className="text-lg font-medium mb-2">Error Loading Configuration</h3>
<p className="text-muted-foreground mb-4">
{(error as Error)?.message || 'An error occurred'}
</p>
</Card>
);
}
return (
<div className="space-y-6">
<Card className="p-6">
@@ -51,7 +183,7 @@ export function CloudComponent() {
<div className="space-y-6">
{/* Domains Section */}
<Card className="p-4 border-l-4 border-l-green-500">
<Card className="p-4 border-l-4 border-l-blue-500">
<div className="flex items-center justify-between mb-3">
<div>
<h3 className="font-medium">Domain Configuration</h3>
@@ -68,6 +200,7 @@ export function CloudComponent() {
variant="outline"
size="sm"
onClick={handleDomainsEdit}
disabled={isUpdating}
>
<Edit2 className="h-4 w-4 mr-1" />
Edit
@@ -82,8 +215,8 @@ export function CloudComponent() {
<Label htmlFor="domain-edit">Public Domain</Label>
<Input
id="domain-edit"
value={tempDomain}
onChange={(e) => setTempDomain(e.target.value)}
value={formValues.domain}
onChange={(e) => updateFormValue('domain', e.target.value)}
placeholder="example.com"
className="mt-1"
/>
@@ -92,21 +225,26 @@ export function CloudComponent() {
<Label htmlFor="internal-domain-edit">Internal Domain</Label>
<Input
id="internal-domain-edit"
value={tempInternalDomain}
onChange={(e) => setTempInternalDomain(e.target.value)}
value={formValues.internalDomain}
onChange={(e) => updateFormValue('internalDomain', e.target.value)}
placeholder="internal.example.com"
className="mt-1"
/>
</div>
<div className="flex gap-2">
<Button size="sm" onClick={handleDomainsSave}>
<Check className="h-4 w-4 mr-1" />
<Button size="sm" onClick={handleDomainsSave} disabled={isUpdating}>
{isUpdating ? (
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
) : (
<Check className="h-4 w-4 mr-1" />
)}
Save
</Button>
<Button
variant="outline"
size="sm"
onClick={handleDomainsCancel}
disabled={isUpdating}
>
<X className="h-4 w-4 mr-1" />
Cancel
@@ -118,13 +256,135 @@ export function CloudComponent() {
<div>
<Label>Public Domain</Label>
<div className="mt-1 p-2 bg-muted rounded-md font-mono text-sm">
{domainValue}
{formValues.domain}
</div>
</div>
<div>
<Label>Internal Domain</Label>
<div className="mt-1 p-2 bg-muted rounded-md font-mono text-sm">
{internalDomainValue}
{formValues.internalDomain}
</div>
</div>
</div>
)}
</Card>
{/* Network Configuration Section */}
<Card className="p-4 border-l-4 border-l-green-500">
<div className="flex items-center justify-between mb-3">
<div>
<h3 className="font-medium">Network Configuration</h3>
<p className="text-sm text-muted-foreground">
Network settings and DHCP configuration
</p>
</div>
<div className="flex items-center gap-2">
<Button variant="ghost" size="sm">
<HelpCircle className="h-4 w-4" />
</Button>
{!editingNetwork && (
<Button
variant="outline"
size="sm"
onClick={handleNetworkEdit}
disabled={isUpdating}
>
<Edit2 className="h-4 w-4 mr-1" />
Edit
</Button>
)}
</div>
</div>
{editingNetwork ? (
<div className="space-y-3">
<div>
<Label htmlFor="dhcp-range-edit">DHCP Range</Label>
<Input
id="dhcp-range-edit"
value={formValues.dhcpRange}
onChange={(e) => updateFormValue('dhcpRange', e.target.value)}
placeholder="192.168.1.100,192.168.1.200"
className="mt-1"
/>
<p className="text-xs text-muted-foreground mt-1">
Format: start_ip,end_ip
</p>
</div>
<div>
<Label htmlFor="dns-ip-edit">DNS Server IP</Label>
<Input
id="dns-ip-edit"
value={formValues.dns.ip}
onChange={(e) => updateFormValue('dns.ip', e.target.value)}
placeholder="192.168.1.1"
className="mt-1"
/>
</div>
<div>
<Label htmlFor="router-ip-edit">Router IP</Label>
<Input
id="router-ip-edit"
value={formValues.router.ip}
onChange={(e) => updateFormValue('router.ip', e.target.value)}
placeholder="192.168.1.1"
className="mt-1"
/>
</div>
<div>
<Label htmlFor="dnsmasq-interface-edit">Dnsmasq Interface</Label>
<Input
id="dnsmasq-interface-edit"
value={formValues.dnsmasq.interface}
onChange={(e) => updateFormValue('dnsmasq.interface', e.target.value)}
placeholder="eth0"
className="mt-1"
/>
</div>
<div className="flex gap-2">
<Button size="sm" onClick={handleNetworkSave} disabled={isUpdating}>
{isUpdating ? (
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
) : (
<Check className="h-4 w-4 mr-1" />
)}
Save
</Button>
<Button
variant="outline"
size="sm"
onClick={handleNetworkCancel}
disabled={isUpdating}
>
<X className="h-4 w-4 mr-1" />
Cancel
</Button>
</div>
</div>
) : (
<div className="space-y-3">
<div>
<Label>DHCP Range</Label>
<div className="mt-1 p-2 bg-muted rounded-md font-mono text-sm">
{formValues.dhcpRange}
</div>
</div>
<div>
<Label>DNS Server IP</Label>
<div className="mt-1 p-2 bg-muted rounded-md font-mono text-sm">
{formValues.dns.ip}
</div>
</div>
<div>
<Label>Router IP</Label>
<div className="mt-1 p-2 bg-muted rounded-md font-mono text-sm">
{formValues.router.ip}
</div>
</div>
<div>
<Label>Dnsmasq Interface</Label>
<div className="mt-1 p-2 bg-muted rounded-md font-mono text-sm">
{formValues.dnsmasq.interface}
</div>
</div>
</div>