feat: fix YAML field conversion, admin namespace, streaming logs, and vllm-serve deploy
- Fix Axios keysToSnake converting user values map keys (gpuMem -> gpu_mem) - Add skipRecurseKeys to keysToSnake for values/valuesYaml fields - Add values_yaml alt json tag and Normalize() in DTOs - Check both camelCase/snake_case in enforceNamespaceValues - Read both tailLines/tail_lines query param for diagnostics - Admin users can freely choose namespace in LaunchModal (free-text input) - Block only kube-system/kube-public/kube-node-lease for admin - Regular users keep existing namespace restrictions - Add SSE streaming pod logs endpoint (backend + frontend) - New PodLogStreamer interface and K8s Follow:true implementation - SSE handler with text/event-stream output - Frontend DiagnosticsModal: Stream button, auto-scroll, live indicator - Remove per-card Refresh button from InstanceCard (redundant with page refresh) - Deploy bge-m3 on vllm-serve 0.6.0 (gpuMem=10000, status=deployed)
This commit is contained in:
@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import React, { useEffect, useMemo, useRef, 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 { getInstanceDiagnostics, streamInstanceLogs, type InstanceDiagnosticsResponse, type InstanceResponse } from "@/api";
|
||||
import { Button, Badge, LoadingState } from "@/shared/components";
|
||||
import { formatApiError } from "@/shared/utils";
|
||||
import { useToast } from "@/shared";
|
||||
@ -17,6 +17,9 @@ export const DiagnosticsModal: React.FC<DiagnosticsModalProps> = ({ instance, on
|
||||
const [data, setData] = useState<InstanceDiagnosticsResponse | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState<TabKey>("summary");
|
||||
const [streamingKey, setStreamingKey] = useState<string | null>(null);
|
||||
const [streamingLines, setStreamingLines] = useState<string[]>([]);
|
||||
const streamCtrlRef = useRef<AbortController | null>(null);
|
||||
|
||||
const loadDiagnostics = async () => {
|
||||
if (!instance.clusterId || !instance.id) return;
|
||||
@ -30,11 +33,48 @@ export const DiagnosticsModal: React.FC<DiagnosticsModalProps> = ({ instance, on
|
||||
}
|
||||
};
|
||||
|
||||
const startStream = (pod: string, container: string) => {
|
||||
// Stop any existing stream first
|
||||
stopStream();
|
||||
const key = `${pod}/${container}`;
|
||||
setStreamingKey(key);
|
||||
setStreamingLines([]);
|
||||
const ctrl = streamInstanceLogs(
|
||||
instance.clusterId!,
|
||||
instance.id!,
|
||||
pod,
|
||||
container,
|
||||
200,
|
||||
(line) => setStreamingLines((prev) => [...prev, line]),
|
||||
() => { setStreamingKey(null); },
|
||||
(err) => { toastError(formatApiError(err) || "Stream error"); setStreamingKey(null); },
|
||||
);
|
||||
streamCtrlRef.current = ctrl;
|
||||
};
|
||||
|
||||
const stopStream = () => {
|
||||
if (streamCtrlRef.current) {
|
||||
streamCtrlRef.current.abort();
|
||||
streamCtrlRef.current = null;
|
||||
}
|
||||
setStreamingKey(null);
|
||||
setStreamingLines([]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
void loadDiagnostics();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [instance.clusterId, instance.id]);
|
||||
|
||||
// Cleanup stream on unmount
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (streamCtrlRef.current) {
|
||||
streamCtrlRef.current.abort();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
const combinedLogs = useMemo(
|
||||
() =>
|
||||
(data?.logs ?? [])
|
||||
@ -63,9 +103,15 @@ export const DiagnosticsModal: React.FC<DiagnosticsModalProps> = ({ instance, on
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button type="button" variant="secondary" size="sm" icon={RotateCw} onClick={loadDiagnostics} loading={loading}>
|
||||
Refresh
|
||||
</Button>
|
||||
{streamingKey ? (
|
||||
<Button type="button" variant="danger" size="sm" onClick={stopStream}>
|
||||
Stop
|
||||
</Button>
|
||||
) : (
|
||||
<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>
|
||||
@ -92,7 +138,15 @@ export const DiagnosticsModal: React.FC<DiagnosticsModalProps> = ({ instance, on
|
||||
) : activeTab === "events" ? (
|
||||
<EventsTab data={data} />
|
||||
) : (
|
||||
<LogsTab data={data} combinedLogs={combinedLogs} onCopy={copyLogs} />
|
||||
<LogsTab
|
||||
data={data}
|
||||
combinedLogs={combinedLogs}
|
||||
onCopy={copyLogs}
|
||||
streamingKey={streamingKey}
|
||||
streamingLines={streamingLines}
|
||||
onStartStream={startStream}
|
||||
onStopStream={stopStream}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@ -209,25 +263,85 @@ const EventsTab = ({ data }: { data: InstanceDiagnosticsResponse }) => (
|
||||
</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>
|
||||
const LogsTab = ({
|
||||
data,
|
||||
combinedLogs,
|
||||
onCopy,
|
||||
streamingKey,
|
||||
streamingLines,
|
||||
onStartStream,
|
||||
onStopStream,
|
||||
}: {
|
||||
data: InstanceDiagnosticsResponse;
|
||||
combinedLogs: string;
|
||||
onCopy: () => void;
|
||||
streamingKey: string | null;
|
||||
streamingLines: string[];
|
||||
onStartStream: (pod: string, container: string) => void;
|
||||
onStopStream: () => void;
|
||||
}) => {
|
||||
const preRef = useRef<HTMLPreElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (streamingKey && preRef.current) {
|
||||
preRef.current.scrollTop = preRef.current.scrollHeight;
|
||||
}
|
||||
}, [streamingLines, streamingKey]);
|
||||
|
||||
return (
|
||||
<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>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
{(data.logs ?? []).length === 0 ? <EmptyLine text="No pod logs were returned." /> : null}
|
||||
{(data.logs ?? []).map((entry) => {
|
||||
const entryKey = `${entry.pod}/${entry.container}`;
|
||||
const isStreaming = streamingKey === entryKey;
|
||||
return (
|
||||
<div key={entryKey} 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">{entryKey}</span>
|
||||
{isStreaming && (
|
||||
<span className="ml-auto inline-flex items-center gap-1.5">
|
||||
<span className="h-2 w-2 rounded-full bg-emerald-400 animate-pulse" />
|
||||
<span className="text-emerald-400">Live</span>
|
||||
</span>
|
||||
)}
|
||||
{isStreaming ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onStopStream}
|
||||
className="ml-2 rounded px-2 py-0.5 text-xs font-medium text-rose-400 hover:bg-rose-500/20 transition"
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onStartStream(entry.pod!, entry.container!)}
|
||||
className="ml-auto rounded px-2 py-0.5 text-xs font-medium text-emerald-400 hover:bg-emerald-500/20 transition"
|
||||
>
|
||||
Stream
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<pre
|
||||
ref={isStreaming ? preRef : undefined}
|
||||
className="max-h-96 overflow-auto p-4 text-xs leading-5 text-slate-100"
|
||||
>
|
||||
{isStreaming
|
||||
? streamingLines.join("\n") || "Waiting for log data..."
|
||||
: 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">
|
||||
|
||||
@ -7,7 +7,6 @@ import {
|
||||
Package,
|
||||
Settings,
|
||||
StopCircle,
|
||||
RefreshCw,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
Clock,
|
||||
@ -282,16 +281,7 @@ export const InstanceCard: React.FC<InstanceCardProps> = ({
|
||||
|
||||
{/* Enhanced Actions Bar */}
|
||||
<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 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" />
|
||||
<span className="truncate">Refresh</span>
|
||||
</button>
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 md:grid-cols-2 xl:grid-cols-4">
|
||||
<button
|
||||
onClick={() => onViewEntries(instance)}
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user