refactor: full-stack restructure with multi-tenancy, workspace management, and K8s diagnostics
- Add Workspace domain (entity, repository, service, handler, DTO) - Add multi-tenant K8s client with tenant binding and quota management - Add K8s diagnostics client (instance diagnostics) - Add authorization middleware (authz package) - Restructure frontend to feature-based architecture (features/) - Add User Management page in configuration - Add AccessDenied page and route guards - Refactor shared components (form inputs, layout, UI) - Update Tailwind config for new design system - Add comprehensive documentation (docs/, tasks/, plans) - Improve cluster service with better kubeconfig handling - Add tests for crypto, config, helm client, tenant binding
This commit is contained in:
@ -0,0 +1,244 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { Activity, AlertTriangle, Box, Copy, FileText, RotateCw, Server, Terminal, X } from "lucide-react";
|
||||
import { getInstanceDiagnostics, type InstanceDiagnosticsResponse, type InstanceResponse } from "@/api";
|
||||
import { Button, Badge, LoadingState } from "@/shared/components";
|
||||
import { formatApiError } from "@/shared/utils";
|
||||
import { useToast } from "@/shared";
|
||||
|
||||
type TabKey = "summary" | "events" | "logs";
|
||||
|
||||
interface DiagnosticsModalProps {
|
||||
instance: InstanceResponse;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const DiagnosticsModal: React.FC<DiagnosticsModalProps> = ({ instance, onClose }) => {
|
||||
const { success, error: toastError } = useToast();
|
||||
const [data, setData] = useState<InstanceDiagnosticsResponse | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState<TabKey>("summary");
|
||||
|
||||
const loadDiagnostics = async () => {
|
||||
if (!instance.clusterId || !instance.id) return;
|
||||
setLoading(true);
|
||||
try {
|
||||
setData(await getInstanceDiagnostics({ clusterId: instance.clusterId, instanceId: instance.id }, { tailLines: 300 }));
|
||||
} catch (err) {
|
||||
toastError(formatApiError(err) || "Failed to load diagnostics");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void loadDiagnostics();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [instance.clusterId, instance.id]);
|
||||
|
||||
const combinedLogs = useMemo(
|
||||
() =>
|
||||
(data?.logs ?? [])
|
||||
.map((entry) => `# ${entry.pod || "pod"} / ${entry.container || "container"}\n${entry.error || entry.log || ""}`)
|
||||
.join("\n\n"),
|
||||
[data?.logs]
|
||||
);
|
||||
|
||||
const copyLogs = async () => {
|
||||
await navigator.clipboard.writeText(combinedLogs);
|
||||
success("Logs copied");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/45 p-4 backdrop-blur-sm">
|
||||
<div className="flex max-h-[90vh] w-full max-w-6xl flex-col overflow-hidden rounded-xl border border-slate-200 bg-white shadow-2xl">
|
||||
<div className="flex items-start justify-between border-b border-slate-200 px-6 py-5">
|
||||
<div>
|
||||
<div className="flex items-center gap-2 text-sm font-semibold text-blue-700">
|
||||
<Activity className="h-4 w-4" />
|
||||
Runtime diagnostics
|
||||
</div>
|
||||
<h2 className="mt-1 text-xl font-semibold text-slate-950">{instance.name}</h2>
|
||||
<p className="mt-1 text-sm text-slate-500">
|
||||
{instance.namespace} · {data?.collectedAt ? new Date(data.collectedAt).toLocaleString() : "live Kubernetes data"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type="button" variant="secondary" size="sm" icon={RotateCw} onClick={loadDiagnostics} loading={loading}>
|
||||
Refresh
|
||||
</Button>
|
||||
<button onClick={onClose} className="rounded-lg p-2 text-slate-500 hover:bg-slate-100 hover:text-slate-900">
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-b border-slate-200 px-6 py-3">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<TabButton active={activeTab === "summary"} onClick={() => setActiveTab("summary")} icon={Box} label="Describe" />
|
||||
<TabButton active={activeTab === "events"} onClick={() => setActiveTab("events")} icon={AlertTriangle} label="Events" />
|
||||
<TabButton active={activeTab === "logs"} onClick={() => setActiveTab("logs")} icon={Terminal} label="Pod Logs" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-6">
|
||||
{loading ? (
|
||||
<LoadingState message="Loading Kubernetes diagnostics..." />
|
||||
) : !data ? (
|
||||
<div className="rounded-lg border border-amber-200 bg-amber-50 p-4 text-sm text-amber-800">
|
||||
Diagnostics data is not available.
|
||||
</div>
|
||||
) : activeTab === "summary" ? (
|
||||
<SummaryTab data={data} />
|
||||
) : activeTab === "events" ? (
|
||||
<EventsTab data={data} />
|
||||
) : (
|
||||
<LogsTab data={data} combinedLogs={combinedLogs} onCopy={copyLogs} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TabButton: React.FC<{
|
||||
active: boolean;
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
}> = ({ active, icon: Icon, label, onClick }) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={`inline-flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-semibold transition ${
|
||||
active ? "bg-blue-600 text-white shadow-sm" : "bg-slate-100 text-slate-600 hover:bg-slate-200 hover:text-slate-950"
|
||||
}`}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
|
||||
const SummaryTab = ({ data }: { data: InstanceDiagnosticsResponse }) => (
|
||||
<div className="space-y-5">
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<MetricCard icon={Box} label="Pods" value={data.pods?.length ?? 0} />
|
||||
<MetricCard icon={Server} label="Services" value={data.services?.length ?? 0} />
|
||||
<MetricCard icon={AlertTriangle} label="Events" value={data.events?.length ?? 0} />
|
||||
</div>
|
||||
<section className="space-y-3">
|
||||
<h3 className="text-sm font-semibold uppercase tracking-wide text-slate-500">Pods</h3>
|
||||
{(data.pods ?? []).length === 0 ? (
|
||||
<EmptyLine text="No pods matched this Helm release." />
|
||||
) : (
|
||||
(data.pods ?? []).map((pod) => (
|
||||
<div key={pod.name} className="rounded-lg border border-slate-200 bg-slate-50 p-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h4 className="font-mono text-sm font-semibold text-slate-950">{pod.name}</h4>
|
||||
<p className="mt-1 text-xs text-slate-500">
|
||||
{pod.nodeName || "unscheduled"} · podIP {pod.podIp || "-"} · restarts {pod.restartCount ?? 0}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant={pod.phase === "Running" ? "success" : "warning"} size="sm">
|
||||
{pod.phase || "Unknown"}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="mt-3 grid gap-2 md:grid-cols-2">
|
||||
{(pod.containers ?? []).map((container) => (
|
||||
<div key={container.name} className="rounded-md border border-slate-200 bg-white p-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-medium text-slate-900">{container.name}</span>
|
||||
<Badge variant={container.ready ? "success" : "warning"} size="sm">
|
||||
{container.state || "unknown"}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="mt-1 truncate font-mono text-xs text-slate-500" title={container.image}>
|
||||
{container.image}
|
||||
</p>
|
||||
{(container.reason || container.message) && (
|
||||
<p className="mt-2 text-xs text-amber-700">{container.reason || container.message}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</section>
|
||||
<section className="space-y-3">
|
||||
<h3 className="text-sm font-semibold uppercase tracking-wide text-slate-500">Services</h3>
|
||||
{(data.services ?? []).length === 0 ? <EmptyLine text="No services matched this Helm release." /> : null}
|
||||
{(data.services ?? []).map((svc) => (
|
||||
<div key={svc.name} className="rounded-lg border border-slate-200 bg-white p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="font-mono text-sm font-semibold text-slate-950">{svc.name}</span>
|
||||
<Badge variant="secondary" size="sm">{svc.type}</Badge>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-slate-500">ClusterIP {svc.clusterIP || "-"}</p>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{(svc.ports ?? []).map((port) => (
|
||||
<span key={`${port.name}-${port.port}`} className="rounded bg-slate-100 px-2 py-1 font-mono text-xs text-slate-700">
|
||||
{port.name || "port"} {port.port}:{port.targetPort}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
const EventsTab = ({ data }: { data: InstanceDiagnosticsResponse }) => (
|
||||
<div className="space-y-3">
|
||||
{(data.events ?? []).length === 0 ? <EmptyLine text="No Kubernetes events matched this release." /> : null}
|
||||
{(data.events ?? []).map((event, index) => (
|
||||
<div key={`${event.involvedName}-${event.reason}-${index}`} className="rounded-lg border border-slate-200 bg-white p-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={event.type === "Warning" ? "warning" : "secondary"} size="sm">{event.type || "Normal"}</Badge>
|
||||
<span className="font-semibold text-slate-950">{event.reason}</span>
|
||||
</div>
|
||||
<span className="text-xs text-slate-500">{event.lastTimestamp ? new Date(event.lastTimestamp).toLocaleString() : ""}</span>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-slate-700">{event.message}</p>
|
||||
<p className="mt-2 text-xs text-slate-500">
|
||||
{event.involvedKind}/{event.involvedName} · count {event.count ?? 1}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const LogsTab = ({ data, combinedLogs, onCopy }: { data: InstanceDiagnosticsResponse; combinedLogs: string; onCopy: () => void }) => (
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-end">
|
||||
<Button type="button" variant="secondary" size="sm" icon={Copy} onClick={onCopy} disabled={!combinedLogs}>
|
||||
Copy Logs
|
||||
</Button>
|
||||
</div>
|
||||
{(data.logs ?? []).length === 0 ? <EmptyLine text="No pod logs were returned." /> : null}
|
||||
{(data.logs ?? []).map((entry) => (
|
||||
<div key={`${entry.pod}-${entry.container}`} className="overflow-hidden rounded-lg border border-slate-800 bg-slate-950">
|
||||
<div className="flex items-center gap-2 border-b border-slate-800 px-4 py-2 text-xs text-slate-300">
|
||||
<FileText className="h-3.5 w-3.5" />
|
||||
<span className="font-mono">{entry.pod}/{entry.container}</span>
|
||||
</div>
|
||||
<pre className="max-h-96 overflow-auto p-4 text-xs leading-5 text-slate-100">{entry.error || entry.log || ""}</pre>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
const MetricCard = ({ icon: Icon, label, value }: { icon: React.ComponentType<{ className?: string }>; label: string; value: number }) => (
|
||||
<div className="rounded-lg border border-slate-200 bg-white p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm font-medium text-slate-500">{label}</span>
|
||||
<Icon className="h-5 w-5 text-blue-600" />
|
||||
</div>
|
||||
<p className="mt-2 text-3xl font-semibold text-slate-950">{value}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const EmptyLine = ({ text }: { text: string }) => (
|
||||
<div className="rounded-lg border border-slate-200 bg-slate-50 p-4 text-sm text-slate-500">{text}</div>
|
||||
);
|
||||
@ -321,7 +321,7 @@ export const EntriesModal: React.FC<EntriesModalProps> = ({ instance, onClose })
|
||||
kubernetes: { color: "bg-green-600/20 text-green-400 border-green-500/30", label: "Live from Kubernetes" },
|
||||
manifest: { color: "bg-blue-600/20 text-blue-400 border-blue-500/30", label: "From Helm Manifest" },
|
||||
notes: { color: "bg-yellow-600/20 text-yellow-400 border-yellow-500/30", label: "From Helm Notes" },
|
||||
none: { color: "bg-gray-600/20 text-gray-400 border-gray-500/30", label: "No Data Available" },
|
||||
none: { color: "bg-slate-200/20 text-slate-500 border-gray-500/30", label: "No Data Available" },
|
||||
};
|
||||
|
||||
const badge = badges[source as keyof typeof badges] || badges.none;
|
||||
@ -335,11 +335,11 @@ export const EntriesModal: React.FC<EntriesModalProps> = ({ instance, onClose })
|
||||
};
|
||||
|
||||
const renderService = (service: ServiceEntry, index: number) => (
|
||||
<div key={service.name || `service-${index}`} className="bg-gray-800/50 border border-gray-700 rounded-lg p-4 space-y-3">
|
||||
<div key={service.name || `service-${index}`} className="bg-slate-50 border border-slate-200 rounded-lg p-4 space-y-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-white">{service.name || `Service ${index + 1}`}</h4>
|
||||
<p className="text-xs text-gray-400 mt-1">Type: {service.type || 'Unknown'}</p>
|
||||
<h4 className="text-sm font-semibold text-slate-900">{service.name || `Service ${index + 1}`}</h4>
|
||||
<p className="text-xs text-slate-500 mt-1">Type: {service.type || 'Unknown'}</p>
|
||||
</div>
|
||||
<span className="px-2 py-1 text-xs font-medium bg-blue-600/20 text-blue-400 border border-blue-500/30 rounded">
|
||||
{service.type || 'Unknown'}
|
||||
@ -349,18 +349,18 @@ export const EntriesModal: React.FC<EntriesModalProps> = ({ instance, onClose })
|
||||
<div className="space-y-2">
|
||||
{/* Cluster IP */}
|
||||
{service.cluster_ip && (
|
||||
<div className="flex items-center justify-between bg-gray-900/50 rounded p-2">
|
||||
<span className="text-xs text-gray-400">Cluster IP:</span>
|
||||
<div className="flex items-center justify-between bg-slate-50 rounded p-2">
|
||||
<span className="text-xs text-slate-500">Cluster IP:</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-mono text-white">{service.cluster_ip}</span>
|
||||
<span className="text-sm font-mono text-slate-900">{service.cluster_ip}</span>
|
||||
<button
|
||||
onClick={() => copyToClipboard(service.cluster_ip!, "Cluster IP")}
|
||||
className="p-1 hover:bg-gray-700 rounded transition"
|
||||
className="p-1 hover:bg-slate-100 rounded transition"
|
||||
>
|
||||
{copiedText === service.cluster_ip ? (
|
||||
<CheckCircle className="w-3 h-3 text-green-400" />
|
||||
) : (
|
||||
<Copy className="w-3 h-3 text-gray-400" />
|
||||
<Copy className="w-3 h-3 text-slate-500" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
@ -369,10 +369,10 @@ export const EntriesModal: React.FC<EntriesModalProps> = ({ instance, onClose })
|
||||
|
||||
{/* Ports */}
|
||||
{service.ports && service.ports.length > 0 && service.ports.map((port, idx) => (
|
||||
<div key={idx} className="flex items-center justify-between bg-gray-900/50 rounded p-2">
|
||||
<span className="text-xs text-gray-400">{port.name || `Port ${idx + 1}`}:</span>
|
||||
<div key={idx} className="flex items-center justify-between bg-slate-50 rounded p-2">
|
||||
<span className="text-xs text-slate-500">{port.name || `Port ${idx + 1}`}:</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-mono text-white">
|
||||
<span className="text-sm font-mono text-slate-900">
|
||||
{port.port} → {port.target_port} {port.protocol || 'TCP'}
|
||||
{port.node_port && ` (NodePort: ${port.node_port})`}
|
||||
</span>
|
||||
@ -386,7 +386,7 @@ export const EntriesModal: React.FC<EntriesModalProps> = ({ instance, onClose })
|
||||
<p className="text-xs text-green-400 mb-2 font-medium">LoadBalancer Entries:</p>
|
||||
{service.loadBalancer.ingress.map((ing, idx) => (
|
||||
<div key={idx} className="flex items-center justify-between mb-1">
|
||||
<span className="text-sm font-mono text-white">
|
||||
<span className="text-sm font-mono text-slate-900">
|
||||
{ing.ip || ing.hostname}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
@ -396,19 +396,19 @@ export const EntriesModal: React.FC<EntriesModalProps> = ({ instance, onClose })
|
||||
href={`http://${ing.ip}:${service.ports?.[0]?.port || 80}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-1 hover:bg-gray-700 rounded transition"
|
||||
className="p-1 hover:bg-slate-100 rounded transition"
|
||||
title="Open in browser"
|
||||
>
|
||||
<ExternalLink className="w-3 h-3 text-blue-400" />
|
||||
</a>
|
||||
<button
|
||||
onClick={() => copyToClipboard(ing.ip!, "IP")}
|
||||
className="p-1 hover:bg-gray-700 rounded transition"
|
||||
className="p-1 hover:bg-slate-100 rounded transition"
|
||||
>
|
||||
{copiedText === ing.ip ? (
|
||||
<CheckCircle className="w-3 h-3 text-green-400" />
|
||||
) : (
|
||||
<Copy className="w-3 h-3 text-gray-400" />
|
||||
<Copy className="w-3 h-3 text-slate-500" />
|
||||
)}
|
||||
</button>
|
||||
</>
|
||||
@ -423,12 +423,12 @@ export const EntriesModal: React.FC<EntriesModalProps> = ({ instance, onClose })
|
||||
);
|
||||
|
||||
const renderIngress = (ingress: IngressEntry, index: number) => (
|
||||
<div key={ingress.name || `ingress-${index}`} className="bg-gray-800/50 border border-gray-700 rounded-lg p-4 space-y-3">
|
||||
<div key={ingress.name || `ingress-${index}`} className="bg-slate-50 border border-slate-200 rounded-lg p-4 space-y-3">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h4 className="text-sm font-semibold text-white">{ingress.name || `Ingress ${index + 1}`}</h4>
|
||||
<h4 className="text-sm font-semibold text-slate-900">{ingress.name || `Ingress ${index + 1}`}</h4>
|
||||
{ingress.class_name && (
|
||||
<p className="text-xs text-gray-400 mt-1">Class: {ingress.class_name}</p>
|
||||
<p className="text-xs text-slate-500 mt-1">Class: {ingress.class_name}</p>
|
||||
)}
|
||||
</div>
|
||||
<Globe className="w-5 h-5 text-purple-400" />
|
||||
@ -436,30 +436,30 @@ export const EntriesModal: React.FC<EntriesModalProps> = ({ instance, onClose })
|
||||
|
||||
<div className="space-y-2">
|
||||
{ingress.rules?.map((rule, ruleIdx) => (
|
||||
<div key={ruleIdx} className="bg-gray-900/50 rounded p-3 space-y-2">
|
||||
<div key={ruleIdx} className="bg-slate-50 rounded p-3 space-y-2">
|
||||
{(() => {
|
||||
const host = rule.host;
|
||||
if (!host) return null;
|
||||
return (
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-sm font-medium text-white">{host}</span>
|
||||
<span className="text-sm font-medium text-slate-900">{host}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<a
|
||||
href={`https://${host}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="p-1 hover:bg-gray-700 rounded transition"
|
||||
className="p-1 hover:bg-slate-100 rounded transition"
|
||||
>
|
||||
<ExternalLink className="w-3 h-3 text-blue-400" />
|
||||
</a>
|
||||
<button
|
||||
onClick={() => copyToClipboard(host, "Host")}
|
||||
className="p-1 hover:bg-gray-700 rounded transition"
|
||||
className="p-1 hover:bg-slate-100 rounded transition"
|
||||
>
|
||||
{copiedText === host ? (
|
||||
<CheckCircle className="w-3 h-3 text-green-400" />
|
||||
) : (
|
||||
<Copy className="w-3 h-3 text-gray-400" />
|
||||
<Copy className="w-3 h-3 text-slate-500" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
@ -470,7 +470,7 @@ export const EntriesModal: React.FC<EntriesModalProps> = ({ instance, onClose })
|
||||
const serviceName = path.backend?.service?.name || "service";
|
||||
const servicePort = path.backend?.service?.port ?? "-";
|
||||
return (
|
||||
<div key={pathIdx} className="text-xs text-gray-400 ml-4">
|
||||
<div key={pathIdx} className="text-xs text-slate-500 ml-4">
|
||||
• {path.path || '/'} → {serviceName}:{servicePort}
|
||||
</div>
|
||||
);
|
||||
@ -489,20 +489,20 @@ export const EntriesModal: React.FC<EntriesModalProps> = ({ instance, onClose })
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 backdrop-blur-sm flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-gray-900 border border-gray-700 rounded-lg max-w-4xl w-full max-h-[90vh] flex flex-col">
|
||||
<div className="bg-white border border-slate-200 rounded-lg max-w-4xl w-full max-h-[90vh] flex flex-col">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-700">
|
||||
<div className="flex items-center justify-between p-6 border-b border-slate-200">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-white">Instance Entries</h2>
|
||||
<p className="text-sm text-gray-400 mt-1">
|
||||
<h2 className="text-xl font-semibold text-slate-900">Instance Entries</h2>
|
||||
<p className="text-sm text-slate-500 mt-1">
|
||||
{instance.name} ({instance.namespace})
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 hover:bg-gray-800 rounded-lg transition"
|
||||
className="p-2 hover:bg-white rounded-lg transition"
|
||||
>
|
||||
<X className="w-5 h-5 text-gray-400" />
|
||||
<X className="w-5 h-5 text-slate-500" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -511,14 +511,14 @@ export const EntriesModal: React.FC<EntriesModalProps> = ({ instance, onClose })
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500"></div>
|
||||
<span className="ml-3 text-gray-400">Loading entries...</span>
|
||||
<span className="ml-3 text-slate-500">Loading entries...</span>
|
||||
</div>
|
||||
) : error ? (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-red-400">{error}</p>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="mt-4 px-4 py-2 bg-gray-800 text-white rounded-lg hover:bg-gray-700 transition"
|
||||
className="mt-4 px-4 py-2 bg-white text-slate-900 rounded-lg hover:bg-slate-100 transition"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
@ -527,7 +527,7 @@ export const EntriesModal: React.FC<EntriesModalProps> = ({ instance, onClose })
|
||||
<div className="space-y-6">
|
||||
{/* Source Badge */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-medium text-gray-400">Data Source:</h3>
|
||||
<h3 className="text-sm font-medium text-slate-500">Data Source:</h3>
|
||||
{getSourceBadge(entries.source)}
|
||||
</div>
|
||||
|
||||
@ -536,7 +536,7 @@ export const EntriesModal: React.FC<EntriesModalProps> = ({ instance, onClose })
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Network className="w-5 h-5 text-blue-400" />
|
||||
<h3 className="text-lg font-semibold text-white">
|
||||
<h3 className="text-lg font-semibold text-slate-900">
|
||||
Services ({entries.services.length})
|
||||
</h3>
|
||||
</div>
|
||||
@ -551,7 +551,7 @@ export const EntriesModal: React.FC<EntriesModalProps> = ({ instance, onClose })
|
||||
<div>
|
||||
<div className="flex items-center gap-2 mb-3">
|
||||
<Globe className="w-5 h-5 text-purple-400" />
|
||||
<h3 className="text-lg font-semibold text-white">
|
||||
<h3 className="text-lg font-semibold text-slate-900">
|
||||
Ingresses ({entries.ingresses.length})
|
||||
</h3>
|
||||
</div>
|
||||
@ -564,9 +564,9 @@ export const EntriesModal: React.FC<EntriesModalProps> = ({ instance, onClose })
|
||||
{/* Helm Notes (as fallback) */}
|
||||
{entries.notes && entries.source === "notes" && (
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white mb-3">Helm Notes</h3>
|
||||
<div className="bg-gray-800/50 border border-gray-700 rounded-lg p-4">
|
||||
<pre className="text-xs text-gray-300 whitespace-pre-wrap font-mono">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-3">Helm Notes</h3>
|
||||
<div className="bg-slate-50 border border-slate-200 rounded-lg p-4">
|
||||
<pre className="text-xs text-slate-700 whitespace-pre-wrap font-mono">
|
||||
{entries.notes}
|
||||
</pre>
|
||||
</div>
|
||||
@ -579,8 +579,8 @@ export const EntriesModal: React.FC<EntriesModalProps> = ({ instance, onClose })
|
||||
!entries.notes && (
|
||||
<div className="text-center py-12">
|
||||
<Network className="w-12 h-12 text-gray-600 mx-auto mb-4" />
|
||||
<p className="text-gray-400">No entries found for this instance</p>
|
||||
<p className="text-xs text-gray-500 mt-2">Data source: {entries.source || 'unknown'}</p>
|
||||
<p className="text-slate-500">No entries found for this instance</p>
|
||||
<p className="text-xs text-slate-500 mt-2">Data source: {entries.source || 'unknown'}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -588,10 +588,10 @@ export const EntriesModal: React.FC<EntriesModalProps> = ({ instance, onClose })
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="p-6 border-t border-gray-700 flex justify-end">
|
||||
<div className="p-6 border-t border-slate-200 flex justify-end">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 bg-gray-800 text-white rounded-lg hover:bg-gray-700 transition"
|
||||
className="px-4 py-2 bg-white text-slate-900 rounded-lg hover:bg-slate-100 transition"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
|
||||
@ -12,6 +12,7 @@ import {
|
||||
XCircle,
|
||||
Clock,
|
||||
Network,
|
||||
Activity,
|
||||
Box,
|
||||
Calendar,
|
||||
GitBranch,
|
||||
@ -29,6 +30,7 @@ interface InstanceCardProps {
|
||||
onTerminate: (instance: InstanceResponse) => void;
|
||||
onRefresh: (instance: InstanceResponse) => void;
|
||||
onViewEntries: (instance: InstanceResponse) => void;
|
||||
onViewDiagnostics: (instance: InstanceResponse) => void;
|
||||
}
|
||||
|
||||
type StatusVisual = {
|
||||
@ -99,16 +101,16 @@ const STATUS_INFO_MAP: Record<InstanceStatus, StatusVisual> = {
|
||||
},
|
||||
[INSTANCE_STATUS.uninstalled]: {
|
||||
icon: StopCircle,
|
||||
color: "text-slate-300",
|
||||
bg: "bg-gradient-to-r from-slate-500/20 to-gray-500/20 border-slate-500/40",
|
||||
color: "text-slate-700",
|
||||
bg: "bg-gradient-to-r from-slate-500/20 to-gray-500/20 border-slate-300/40",
|
||||
glow: "shadow-slate-500/20",
|
||||
label: "Uninstalled",
|
||||
defaultReason: "Instance has been removed from the cluster.",
|
||||
},
|
||||
[INSTANCE_STATUS.unknown]: {
|
||||
icon: HelpCircle,
|
||||
color: "text-slate-300",
|
||||
bg: "bg-gradient-to-r from-slate-500/20 to-gray-500/20 border-slate-500/40",
|
||||
color: "text-slate-700",
|
||||
bg: "bg-gradient-to-r from-slate-500/20 to-gray-500/20 border-slate-300/40",
|
||||
glow: "shadow-slate-500/20",
|
||||
label: "Unknown",
|
||||
defaultReason: "Awaiting next state update.",
|
||||
@ -136,6 +138,7 @@ export const InstanceCard: React.FC<InstanceCardProps> = ({
|
||||
onTerminate,
|
||||
onRefresh,
|
||||
onViewEntries,
|
||||
onViewDiagnostics,
|
||||
}) => {
|
||||
const normalizedStatus = (instance.status ?? INSTANCE_STATUS.unknown) as InstanceStatus;
|
||||
const statusInfo =
|
||||
@ -164,12 +167,12 @@ export const InstanceCard: React.FC<InstanceCardProps> = ({
|
||||
typeof instance.lastError === "string" ? instance.lastError.trim() : "";
|
||||
|
||||
return (
|
||||
<div className="group relative bg-gradient-to-br from-slate-800/80 via-slate-800/50 to-slate-900/80 border border-slate-700/50 rounded-xl hover:border-blue-500/50 hover:shadow-xl hover:shadow-blue-500/10 transition-all duration-300 overflow-hidden">
|
||||
<div className="group relative bg-gradient-to-br from-white via-white to-slate-50 border border-slate-200 rounded-xl hover:border-blue-500/50 hover:shadow-xl hover:shadow-blue-500/10 transition-all duration-300 overflow-hidden">
|
||||
{/* Decorative gradient overlay */}
|
||||
<div className="absolute top-0 right-0 w-64 h-64 bg-gradient-to-br from-blue-500/5 to-purple-500/5 rounded-full blur-3xl -z-0 opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||
|
||||
{/* Header with enhanced design */}
|
||||
<div className="relative px-6 py-5 border-b border-slate-700/50 bg-gradient-to-r from-slate-800/30 to-transparent">
|
||||
<div className="relative px-6 py-5 border-b border-slate-200 bg-gradient-to-r from-slate-50 to-white">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-4 flex-1">
|
||||
{/* Enhanced icon with glow effect */}
|
||||
@ -179,12 +182,12 @@ export const InstanceCard: React.FC<InstanceCardProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-white to-slate-300 truncate">
|
||||
<h3 className="text-xl font-bold text-slate-950 truncate">
|
||||
{instanceName}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<Package className="w-4 h-4 text-slate-400" />
|
||||
<p className="text-sm text-slate-400 font-mono">
|
||||
<Package className="w-4 h-4 text-slate-500" />
|
||||
<p className="text-sm text-slate-500 font-mono">
|
||||
{repository}
|
||||
</p>
|
||||
<span className="text-slate-600">•</span>
|
||||
@ -206,10 +209,10 @@ export const InstanceCard: React.FC<InstanceCardProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-col gap-1 text-sm text-slate-300">
|
||||
<span className="font-medium text-slate-200">{statusReason}</span>
|
||||
<div className="mt-4 flex flex-col gap-1 text-sm text-slate-700">
|
||||
<span className="font-medium text-slate-700">{statusReason}</span>
|
||||
{lastOperationLabel && (
|
||||
<span className="text-xs uppercase tracking-wide text-slate-400">
|
||||
<span className="text-xs uppercase tracking-wide text-slate-500">
|
||||
Operation: {lastOperationLabel}
|
||||
</span>
|
||||
)}
|
||||
@ -217,48 +220,48 @@ export const InstanceCard: React.FC<InstanceCardProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Enhanced Content Grid */}
|
||||
<div className="relative px-6 py-5 space-y-4 bg-gradient-to-b from-transparent to-slate-900/30">
|
||||
<div className="relative px-6 py-5 space-y-4 bg-gradient-to-b from-white to-slate-50">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{/* Namespace */}
|
||||
<div className="p-3 bg-slate-800/50 border border-slate-700/50 rounded-lg hover:border-purple-500/30 transition-colors">
|
||||
<div className="p-3 bg-white border border-slate-200 rounded-lg hover:border-purple-500/30 transition-colors">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Layers className="w-4 h-4 text-purple-400" />
|
||||
<p className="text-xs text-slate-400 uppercase font-semibold tracking-wider">Namespace</p>
|
||||
<p className="text-xs text-slate-500 uppercase font-semibold tracking-wider">Namespace</p>
|
||||
</div>
|
||||
<p className="text-sm font-bold text-white">
|
||||
<p className="text-sm font-bold text-slate-900">
|
||||
{namespace}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Revision */}
|
||||
<div className="p-3 bg-slate-800/50 border border-slate-700/50 rounded-lg hover:border-green-500/30 transition-colors">
|
||||
<div className="p-3 bg-white border border-slate-200 rounded-lg hover:border-green-500/30 transition-colors">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<GitBranch className="w-4 h-4 text-green-400" />
|
||||
<p className="text-xs text-slate-400 uppercase font-semibold tracking-wider">Revision</p>
|
||||
<p className="text-xs text-slate-500 uppercase font-semibold tracking-wider">Revision</p>
|
||||
</div>
|
||||
<p className="text-sm font-bold text-white">
|
||||
<p className="text-sm font-bold text-slate-900">
|
||||
{revision}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Repository - Full Width */}
|
||||
<div className="col-span-2 p-3 bg-slate-800/50 border border-slate-700/50 rounded-lg hover:border-blue-500/30 transition-colors">
|
||||
<div className="col-span-2 p-3 bg-white border border-slate-200 rounded-lg hover:border-blue-500/30 transition-colors">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Package className="w-4 h-4 text-blue-400" />
|
||||
<p className="text-xs text-slate-400 uppercase font-semibold tracking-wider">Repository</p>
|
||||
<p className="text-xs text-slate-500 uppercase font-semibold tracking-wider">Repository</p>
|
||||
</div>
|
||||
<p className="text-sm font-mono text-white truncate" title={repository}>
|
||||
<p className="text-sm font-mono text-slate-900 truncate" title={repository}>
|
||||
{repository}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Launched Date - Full Width */}
|
||||
<div className="col-span-2 p-3 bg-slate-800/50 border border-slate-700/50 rounded-lg hover:border-amber-500/30 transition-colors">
|
||||
<div className="col-span-2 p-3 bg-white border border-slate-200 rounded-lg hover:border-amber-500/30 transition-colors">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Calendar className="w-4 h-4 text-amber-400" />
|
||||
<p className="text-xs text-slate-400 uppercase font-semibold tracking-wider">Launched</p>
|
||||
<p className="text-xs text-slate-500 uppercase font-semibold tracking-wider">Launched</p>
|
||||
</div>
|
||||
<p className="text-sm font-bold text-white">
|
||||
<p className="text-sm font-bold text-slate-900">
|
||||
{createdAtText}
|
||||
</p>
|
||||
</div>
|
||||
@ -267,7 +270,7 @@ export const InstanceCard: React.FC<InstanceCardProps> = ({
|
||||
{lastError && (
|
||||
<div className="flex items-start gap-3 p-4 border border-rose-500/30 bg-rose-500/10 rounded-lg">
|
||||
<div className="p-2 bg-rose-500/20 rounded-lg border border-rose-500/40">
|
||||
<AlertTriangle className="w-5 h-5 text-rose-300" />
|
||||
<AlertTriangle className="w-5 h-5 text-rose-700" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-rose-200">Last error</p>
|
||||
@ -278,47 +281,51 @@ export const InstanceCard: React.FC<InstanceCardProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Enhanced Actions Bar */}
|
||||
<div className="relative px-6 py-4 bg-gradient-to-r from-slate-900/80 via-slate-900/50 to-slate-900/80 border-t border-slate-700/50 backdrop-blur-sm">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="relative px-6 py-4 bg-gradient-to-r from-slate-50 via-slate-50 to-white border-t border-slate-200 backdrop-blur-sm">
|
||||
<div className="grid grid-cols-2 gap-2 md:grid-cols-3 xl:grid-cols-5">
|
||||
<button
|
||||
onClick={() => onRefresh(instance)}
|
||||
className="group/btn inline-flex items-center gap-2 px-4 py-2.5 text-sm font-semibold text-slate-300 bg-slate-700/50 hover:bg-slate-600/50 rounded-lg transition-all duration-200 hover:scale-105 hover:shadow-lg border border-slate-600/50 hover:border-slate-500"
|
||||
className="group/btn inline-flex min-w-0 items-center justify-center gap-2 rounded-lg border border-slate-300 bg-white px-3 py-2.5 text-sm font-semibold text-slate-700 transition-all duration-200 hover:border-slate-300 hover:bg-slate-100 hover:shadow-lg"
|
||||
title="Refresh status"
|
||||
>
|
||||
<RefreshCw className="w-4 h-4 group-hover/btn:rotate-180 transition-transform duration-500" />
|
||||
Refresh
|
||||
<span className="truncate">Refresh</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => onViewEntries(instance)}
|
||||
className="group/btn inline-flex items-center gap-2 px-4 py-2.5 text-sm font-semibold text-emerald-300 bg-gradient-to-r from-emerald-600/20 to-green-600/20 border border-emerald-500/40 rounded-lg hover:from-emerald-600/30 hover:to-green-600/30 hover:border-emerald-500/60 transition-all duration-200 hover:scale-105 hover:shadow-lg hover:shadow-emerald-500/20"
|
||||
className="group/btn inline-flex min-w-0 items-center justify-center gap-2 rounded-lg border border-emerald-500/40 bg-emerald-50 px-3 py-2.5 text-sm font-semibold text-emerald-700 transition-all duration-200 hover:border-emerald-500/60 hover:bg-emerald-100 hover:shadow-lg"
|
||||
title="View service entries"
|
||||
>
|
||||
<Network className="w-4 h-4 group-hover/btn:scale-110 transition-transform" />
|
||||
Entries
|
||||
<span className="truncate">Entries</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onViewDiagnostics(instance)}
|
||||
className="group/btn inline-flex min-w-0 items-center justify-center gap-2 rounded-lg border border-indigo-200 bg-indigo-50 px-3 py-2.5 text-sm font-semibold text-indigo-700 transition-all duration-200 hover:border-indigo-300 hover:bg-indigo-100 hover:shadow-lg"
|
||||
title="View describe, events, and pod logs"
|
||||
>
|
||||
<Activity className="w-4 h-4 group-hover/btn:scale-110 transition-transform" />
|
||||
<span className="truncate">Diagnostics</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => onModify(instance)}
|
||||
className="group/btn inline-flex items-center gap-2 px-4 py-2.5 text-sm font-semibold text-blue-300 bg-gradient-to-r from-blue-600/20 to-cyan-600/20 border border-blue-500/40 rounded-lg hover:from-blue-600/30 hover:to-cyan-600/30 hover:border-blue-500/60 transition-all duration-200 hover:scale-105 hover:shadow-lg hover:shadow-blue-500/20"
|
||||
className="group/btn inline-flex min-w-0 items-center justify-center gap-2 rounded-lg border border-blue-500/40 bg-blue-50 px-3 py-2.5 text-sm font-semibold text-blue-700 transition-all duration-200 hover:border-blue-500/60 hover:bg-blue-100 hover:shadow-lg"
|
||||
title="Modify instance configuration"
|
||||
>
|
||||
<Settings className="w-4 h-4 group-hover/btn:rotate-90 transition-transform duration-300" />
|
||||
Modify
|
||||
<span className="truncate">Modify</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => onTerminate(instance)}
|
||||
className="group/btn inline-flex items-center gap-2 px-4 py-2.5 text-sm font-semibold text-rose-300 bg-gradient-to-r from-rose-600/20 to-red-600/20 border border-rose-500/40 rounded-lg hover:from-rose-600/30 hover:to-red-600/30 hover:border-rose-500/60 transition-all duration-200 hover:scale-105 hover:shadow-lg hover:shadow-rose-500/20"
|
||||
className="group/btn inline-flex min-w-0 items-center justify-center gap-2 rounded-lg border border-rose-500/40 bg-red-50 px-3 py-2.5 text-sm font-semibold text-rose-700 transition-all duration-200 hover:border-rose-500/60 hover:bg-rose-100 hover:shadow-lg"
|
||||
title="Terminate instance"
|
||||
>
|
||||
<StopCircle className="w-4 h-4 group-hover/btn:scale-110 transition-transform" />
|
||||
Terminate
|
||||
<span className="truncate">Delete</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
*/
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Settings } from "lucide-react";
|
||||
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
||||
import type { InstanceResponse, UpdateInstanceRequest } from "@/api";
|
||||
import { getValuesSchema } from "@/api";
|
||||
import {
|
||||
@ -13,7 +14,6 @@ import {
|
||||
FormField,
|
||||
Input,
|
||||
Textarea,
|
||||
Checkbox,
|
||||
ErrorState,
|
||||
LoadingState,
|
||||
Badge,
|
||||
@ -35,8 +35,6 @@ export const ModifyModal: React.FC<ModifyModalProps> = ({
|
||||
const [tag, setTag] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [valuesYaml, setValuesYaml] = useState("");
|
||||
const [wait, setWait] = useState(true);
|
||||
const [timeout, setTimeout_] = useState(300);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
@ -58,7 +56,7 @@ export const ModifyModal: React.FC<ModifyModalProps> = ({
|
||||
? JSON.parse(instance.values)
|
||||
: instance.values;
|
||||
setFormValues(parsedValues);
|
||||
setValuesYaml(typeof parsedValues === 'object' ? JSON.stringify(parsedValues, null, 2) : String(parsedValues));
|
||||
setValuesYaml(typeof parsedValues === 'object' ? stringifyYaml(parsedValues) : String(parsedValues));
|
||||
} catch (err) {
|
||||
console.error('[ModifyModal] Failed to parse existing values:', err);
|
||||
setValuesYaml(String(instance.values) || "");
|
||||
@ -104,8 +102,7 @@ export const ModifyModal: React.FC<ModifyModalProps> = ({
|
||||
|
||||
const handleFormValuesChange = (values: Record<string, any>) => {
|
||||
setFormValues(values);
|
||||
// Also update YAML representation
|
||||
setValuesYaml(JSON.stringify(values, null, 2));
|
||||
setValuesYaml(stringifyYaml(values));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
@ -116,7 +113,9 @@ export const ModifyModal: React.FC<ModifyModalProps> = ({
|
||||
try {
|
||||
const payload: UpdateInstanceRequest = {
|
||||
version: tag && tag !== instance.version ? tag : undefined,
|
||||
values: valuesYaml.trim() ? JSON.parse(valuesYaml) : undefined,
|
||||
description: description.trim() || undefined,
|
||||
values: valuesYaml.trim() ? parseValuesYaml(valuesYaml) : undefined,
|
||||
valuesYaml: valuesYaml.trim() || undefined,
|
||||
};
|
||||
|
||||
if (!instance.clusterId || !instance.id) {
|
||||
@ -128,8 +127,8 @@ export const ModifyModal: React.FC<ModifyModalProps> = ({
|
||||
await onConfirm(instance.clusterId, instance.id, payload);
|
||||
onClose();
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof SyntaxError) {
|
||||
setError("Invalid JSON/YAML values. Please fix the configuration.");
|
||||
if (err instanceof Error && err.message.includes("YAML")) {
|
||||
setError(err.message);
|
||||
} else {
|
||||
setError((err as Error).message || "Failed to modify instance");
|
||||
}
|
||||
@ -144,7 +143,7 @@ export const ModifyModal: React.FC<ModifyModalProps> = ({
|
||||
onClose={onClose}
|
||||
title={`Modify Instance - ${instance.name || "Unnamed"}`}
|
||||
icon={Settings}
|
||||
iconColor="text-blue-400"
|
||||
iconColor="text-blue-600"
|
||||
size="lg"
|
||||
footer={
|
||||
<>
|
||||
@ -175,15 +174,15 @@ export const ModifyModal: React.FC<ModifyModalProps> = ({
|
||||
)}
|
||||
|
||||
{/* Current Info */}
|
||||
<div className="bg-gray-800/50 border border-gray-700 rounded-lg p-4 space-y-2">
|
||||
<p className="text-sm text-gray-300">
|
||||
<span className="font-medium text-white">Current Version:</span> {instance.version || "N/A"}
|
||||
<div className="bg-slate-50 border border-slate-200 rounded-lg p-4 space-y-2">
|
||||
<p className="text-sm text-slate-700">
|
||||
<span className="font-medium text-slate-900">Current Version:</span> {instance.version || "N/A"}
|
||||
</p>
|
||||
<p className="text-sm text-gray-300">
|
||||
<span className="font-medium text-white">Cluster:</span> {instance.clusterId || "N/A"}
|
||||
<p className="text-sm text-slate-700">
|
||||
<span className="font-medium text-slate-900">Cluster:</span> {instance.clusterId || "N/A"}
|
||||
</p>
|
||||
<p className="text-sm text-gray-300">
|
||||
<span className="font-medium text-white">Repository:</span> {instance.repository || "N/A"}
|
||||
<p className="text-sm text-slate-700">
|
||||
<span className="font-medium text-slate-900">Repository:</span> {instance.repository || "N/A"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -215,12 +214,13 @@ export const ModifyModal: React.FC<ModifyModalProps> = ({
|
||||
{/* Values Configuration */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<label className="block text-sm font-medium text-gray-200">
|
||||
<label className="block text-sm font-medium text-slate-700">
|
||||
Configuration Values
|
||||
</label>
|
||||
{valuesSchema?.properties && (
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setInputMethod('form')}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
@ -232,6 +232,7 @@ export const ModifyModal: React.FC<ModifyModalProps> = ({
|
||||
</Badge>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setInputMethod('yaml')}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
@ -265,25 +266,9 @@ export const ModifyModal: React.FC<ModifyModalProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Options */}
|
||||
<div className="space-y-3">
|
||||
<Checkbox
|
||||
id="wait"
|
||||
checked={wait}
|
||||
onChange={(e) => setWait(e.target.checked)}
|
||||
label="Wait for all resources to be ready"
|
||||
/>
|
||||
|
||||
<FormField label="Timeout (seconds)">
|
||||
<Input
|
||||
type="number"
|
||||
value={timeout}
|
||||
onChange={(e) => setTimeout_(parseInt(e.target.value) || 300)}
|
||||
min={60}
|
||||
max={3600}
|
||||
/>
|
||||
</FormField>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500">
|
||||
Update applies the selected chart version and values override. Resource readiness is tracked from the instance list after submit.
|
||||
</p>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
@ -324,3 +309,14 @@ const extractJsonSchema = (schemaResponse: unknown): JsonSchema | null => {
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const parseValuesYaml = (source: string): Record<string, any> => {
|
||||
const parsed = parseYaml(source);
|
||||
if (parsed == null) {
|
||||
return {};
|
||||
}
|
||||
if (typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
throw new Error("Values YAML must be an object");
|
||||
}
|
||||
return parsed as Record<string, any>;
|
||||
};
|
||||
|
||||
@ -20,6 +20,7 @@ import { InstanceErrors, SuccessMessages, formatApiError } from "@/shared/utils"
|
||||
import { InstanceCard } from "../components/InstanceCard";
|
||||
import { ModifyModal } from "../components/ModifyModal";
|
||||
import { EntriesModal } from "../components/EntriesModal";
|
||||
import { DiagnosticsModal } from "../components/DiagnosticsModal";
|
||||
import { globalCache } from "@/shared/services/artifact-cache";
|
||||
|
||||
const AUTO_REFRESH_INTERVAL_MS = 30000;
|
||||
@ -47,6 +48,7 @@ const InstancesManagementPage: React.FC = () => {
|
||||
// Modals
|
||||
const [modifyInstance, setModifyInstance] = useState<Instance | null>(null);
|
||||
const [entriesInstance, setEntriesInstance] = useState<Instance | null>(null);
|
||||
const [diagnosticsInstance, setDiagnosticsInstance] = useState<Instance | null>(null);
|
||||
|
||||
// 核心数据加载函数 - 使用全局缓存
|
||||
const loadDataCore = useCallback(async (options: LoadDataOptions = {}) => {
|
||||
@ -225,6 +227,10 @@ const InstancesManagementPage: React.FC = () => {
|
||||
setEntriesInstance(instance);
|
||||
}, []);
|
||||
|
||||
const handleViewDiagnostics = useCallback((instance: Instance) => {
|
||||
setDiagnosticsInstance(instance);
|
||||
}, []);
|
||||
|
||||
const handleModifyConfirm = useCallback(async (
|
||||
clusterId: string,
|
||||
instanceId: string,
|
||||
@ -333,43 +339,40 @@ const InstancesManagementPage: React.FC = () => {
|
||||
<div className={`grid grid-cols-1 gap-5 mb-8 ${
|
||||
clusters.length > 1 ? 'md:grid-cols-3' : 'md:grid-cols-2'
|
||||
}`}>
|
||||
<div className="relative group overflow-hidden bg-gradient-to-br from-blue-900/40 via-blue-800/30 to-blue-900/40 border border-blue-500/30 rounded-xl p-6 hover:border-blue-400/50 hover:shadow-xl hover:shadow-blue-500/20 transition-all duration-300">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-blue-500/10 rounded-full blur-3xl group-hover:bg-blue-500/20 transition-all"></div>
|
||||
<div className="relative group overflow-hidden bg-white border border-blue-100 rounded-lg p-6 hover:border-blue-200 hover:shadow-md transition-all duration-300">
|
||||
<div className="relative flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-blue-300 uppercase tracking-wider mb-2">Total Instances</p>
|
||||
<p className="text-4xl font-bold text-white">{totalInstances}</p>
|
||||
<p className="text-sm font-semibold text-blue-700 uppercase tracking-wider mb-2">Total Instances</p>
|
||||
<p className="text-4xl font-bold text-slate-900">{totalInstances}</p>
|
||||
</div>
|
||||
<div className="p-4 bg-blue-500/20 rounded-xl border border-blue-400/30 shadow-lg shadow-blue-500/30">
|
||||
<Package className="w-8 h-8 text-blue-400" />
|
||||
<div className="p-4 bg-blue-50 rounded-lg border border-blue-100">
|
||||
<Package className="w-8 h-8 text-blue-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="relative group overflow-hidden bg-gradient-to-br from-emerald-900/40 via-emerald-800/30 to-green-900/40 border border-emerald-500/30 rounded-xl p-6 hover:border-emerald-400/50 hover:shadow-xl hover:shadow-emerald-500/20 transition-all duration-300">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-emerald-500/10 rounded-full blur-3xl group-hover:bg-emerald-500/20 transition-all"></div>
|
||||
<div className="relative group overflow-hidden bg-white border border-emerald-100 rounded-lg p-6 hover:border-emerald-200 hover:shadow-md transition-all duration-300">
|
||||
<div className="relative flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-emerald-300 uppercase tracking-wider mb-2">Clusters</p>
|
||||
<p className="text-4xl font-bold text-white">{clusters.length}</p>
|
||||
<p className="text-sm font-semibold text-emerald-700 uppercase tracking-wider mb-2">Clusters</p>
|
||||
<p className="text-4xl font-bold text-slate-900">{clusters.length}</p>
|
||||
</div>
|
||||
<div className="p-4 bg-emerald-500/20 rounded-xl border border-emerald-400/30 shadow-lg shadow-emerald-500/30">
|
||||
<Server className="w-8 h-8 text-emerald-400" />
|
||||
<div className="p-4 bg-emerald-50 rounded-lg border border-emerald-100">
|
||||
<Server className="w-8 h-8 text-emerald-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Only show Filtered when there are multiple clusters */}
|
||||
{clusters.length > 1 && (
|
||||
<div className="relative group overflow-hidden bg-gradient-to-br from-purple-900/40 via-purple-800/30 to-purple-900/40 border border-purple-500/30 rounded-xl p-6 hover:border-purple-400/50 hover:shadow-xl hover:shadow-purple-500/20 transition-all duration-300">
|
||||
<div className="absolute top-0 right-0 w-32 h-32 bg-purple-500/10 rounded-full blur-3xl group-hover:bg-purple-500/20 transition-all"></div>
|
||||
<div className="relative group overflow-hidden bg-white border border-violet-100 rounded-lg p-6 hover:border-violet-200 hover:shadow-md transition-all duration-300">
|
||||
<div className="relative flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-semibold text-purple-300 uppercase tracking-wider mb-2">Showing</p>
|
||||
<p className="text-4xl font-bold text-white">{filteredInstances.length}</p>
|
||||
<p className="text-sm font-semibold text-violet-700 uppercase tracking-wider mb-2">Showing</p>
|
||||
<p className="text-4xl font-bold text-slate-900">{filteredInstances.length}</p>
|
||||
</div>
|
||||
<div className="p-4 bg-purple-500/20 rounded-xl border border-purple-400/30 shadow-lg shadow-purple-500/30">
|
||||
<Boxes className="w-8 h-8 text-purple-400" />
|
||||
<div className="p-4 bg-violet-50 rounded-lg border border-violet-100">
|
||||
<Boxes className="w-8 h-8 text-violet-600" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -379,13 +382,13 @@ const InstancesManagementPage: React.FC = () => {
|
||||
|
||||
{/* Enhanced Filters */}
|
||||
{clusters.length > 1 && (
|
||||
<div className="mb-6 p-5 bg-gradient-to-r from-slate-800/50 via-slate-800/30 to-slate-800/50 border border-slate-700/50 rounded-xl">
|
||||
<div className="mb-6 p-5 bg-white border border-slate-200 rounded-lg shadow-sm">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 bg-gradient-to-br from-cyan-500/20 to-blue-500/20 rounded-lg border border-cyan-500/30">
|
||||
<Server className="w-5 h-5 text-cyan-400" />
|
||||
</div>
|
||||
<label className="text-sm font-semibold text-slate-300">
|
||||
<label className="text-sm font-semibold text-slate-700">
|
||||
Filter by Cluster:
|
||||
</label>
|
||||
</div>
|
||||
@ -441,10 +444,10 @@ const InstancesManagementPage: React.FC = () => {
|
||||
<Server className="w-5 h-5 text-emerald-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-white">
|
||||
<h2 className="text-xl font-bold text-slate-900">
|
||||
{cluster.name || "Unnamed Cluster"}
|
||||
</h2>
|
||||
<p className="text-sm text-slate-400 mt-0.5">
|
||||
<p className="text-sm text-slate-500 mt-0.5">
|
||||
{instances.length} {instances.length === 1 ? 'instance' : 'instances'} running
|
||||
</p>
|
||||
</div>
|
||||
@ -458,6 +461,7 @@ const InstancesManagementPage: React.FC = () => {
|
||||
onTerminate={handleTerminate}
|
||||
onRefresh={handleRefresh}
|
||||
onViewEntries={handleViewEntries}
|
||||
onViewDiagnostics={handleViewDiagnostics}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@ -474,6 +478,7 @@ const InstancesManagementPage: React.FC = () => {
|
||||
onTerminate={handleTerminate}
|
||||
onRefresh={handleRefresh}
|
||||
onViewEntries={handleViewEntries}
|
||||
onViewDiagnostics={handleViewDiagnostics}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@ -497,6 +502,13 @@ const InstancesManagementPage: React.FC = () => {
|
||||
onClose={() => setEntriesInstance(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{diagnosticsInstance && (
|
||||
<DiagnosticsModal
|
||||
instance={diagnosticsInstance}
|
||||
onClose={() => setDiagnosticsInstance(null)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user