fix: scale replicas in response, K8s metrics client, quota precheck, auth tests
- Add GetMetrics method to MetricsClient interface and implement cluster metrics API - Add QuotaPrecheck service for validating resource quotas before deployment - Add auth DTO with role/permission models and auth handler tests - Add instance diagnostics: mounted NFS volumes, labels, annotations in pod diagnostics - Update workspace handler with GetWorkspace endpoint and shared-user list - Fix monitoring handler to use correct service method name - Add tail_lines fallback in instance handler for snake_case query params - Update nginx config for SSE log streaming support (no buffering) - Add comprehensive test coverage: auth_service_test, auth_handler_test, auth_dto_test, metrics_client_test, quota_precheck_test - Update error messages for quota validation and instance operations - ModifyModal: fix YAML lineWidth:0, modified keys summary, delta-only submit - InstanceCard: correctly disable scale-minus when replicas <= 0 - SidebarLayout: add hover transition for sidebar items - Update todo.md and lessons.md with latest fixes
This commit is contained in:
@ -5,7 +5,7 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Box, Settings, StopCircle, CheckCircle, XCircle, Clock,
|
||||
Network, Activity, GitBranch, Layers,
|
||||
Network, Activity, GitBranch, Layers, User,
|
||||
AlertTriangle, HelpCircle, Minus, Plus, Loader2,
|
||||
} from "lucide-react";
|
||||
import type { InstanceResponse } from "@/api";
|
||||
@ -116,6 +116,7 @@ export const InstanceCard: React.FC<InstanceCardProps> = ({
|
||||
const version = instance.version || "—";
|
||||
const namespace = instance.namespace || "default";
|
||||
const revision = instance.revision ?? "—";
|
||||
const ownerLabel = ownerDisplayName(instance.ownerUsername, instance.ownerId);
|
||||
|
||||
const currentReplicas: number = instance.replicas ?? 0;
|
||||
|
||||
@ -150,7 +151,7 @@ export const InstanceCard: React.FC<InstanceCardProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="hover-lift group relative bg-white border border-slate-200 rounded-lg flex items-center gap-3 px-4 py-3 transition-all">
|
||||
<div className="hover-lift group relative bg-white border border-slate-200 rounded-lg flex flex-col gap-3 px-4 py-3 transition-all lg:flex-row lg:items-center">
|
||||
{/* Left color bar (status) */}
|
||||
<div className={`self-stretch w-1 rounded-full flex-shrink-0 ${statusInfo.bg} border ${statusInfo.border}`} />
|
||||
|
||||
@ -161,10 +162,10 @@ export const InstanceCard: React.FC<InstanceCardProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Name + Chart info */}
|
||||
<div className="flex-1 min-w-0 flex items-center gap-4">
|
||||
<div className="w-full min-w-0 flex-1 flex items-center gap-4 lg:w-auto">
|
||||
<div className="min-w-0">
|
||||
<h4 className="text-sm font-semibold text-slate-900 truncate">{instanceName}</h4>
|
||||
<div className="flex items-center gap-3 text-xs text-slate-500 mt-0.5">
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-slate-500 mt-0.5">
|
||||
<span className="flex items-center gap-1">
|
||||
<Box className="w-3 h-3" />
|
||||
<span className="truncate max-w-[200px]">{chart}:{version}</span>
|
||||
@ -177,6 +178,12 @@ export const InstanceCard: React.FC<InstanceCardProps> = ({
|
||||
<GitBranch className="w-3 h-3" />
|
||||
rev{revision}
|
||||
</span>
|
||||
{ownerLabel && (
|
||||
<span className="flex min-w-0 items-center gap-1">
|
||||
<User className="w-3 h-3" />
|
||||
<span className="truncate max-w-[120px]">{ownerLabel}</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -196,7 +203,7 @@ export const InstanceCard: React.FC<InstanceCardProps> = ({
|
||||
)}
|
||||
|
||||
{/* Scale controls */}
|
||||
<div className="flex items-center gap-0.5 flex-shrink-0">
|
||||
<div className="flex w-full items-center justify-end gap-0.5 flex-shrink-0 lg:w-auto">
|
||||
{canScale ? (
|
||||
<>
|
||||
<button
|
||||
@ -229,7 +236,7 @@ export const InstanceCard: React.FC<InstanceCardProps> = ({
|
||||
</div>
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex items-center gap-1.5 flex-shrink-0">
|
||||
<div className="flex w-full flex-wrap items-center justify-end gap-1.5 flex-shrink-0 lg:w-auto">
|
||||
<button
|
||||
onClick={() => onViewEntries(instance)}
|
||||
className="flex items-center gap-1 px-2 py-1.5 rounded-md text-slate-500 hover:text-blue-600 hover:bg-blue-50 transition-colors text-xs font-medium"
|
||||
@ -266,3 +273,12 @@ export const InstanceCard: React.FC<InstanceCardProps> = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const ownerDisplayName = (ownerUsername?: string, ownerId?: string): string => {
|
||||
const username = ownerUsername?.trim();
|
||||
if (username) return username;
|
||||
const id = ownerId?.trim();
|
||||
if (!id) return "";
|
||||
if (id.length <= 12) return id;
|
||||
return `${id.slice(0, 8)}...${id.slice(-4)}`;
|
||||
};
|
||||
|
||||
@ -35,7 +35,6 @@ export const ModifyModal: React.FC<ModifyModalProps> = ({
|
||||
const [valuesYaml, setValuesYaml] = useState("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [originalValuesYaml, setOriginalValuesYaml] = useState("");
|
||||
const [modifiedKeys, setModifiedKeys] = useState<string[]>([]);
|
||||
|
||||
// Values Diff support
|
||||
@ -60,7 +59,6 @@ export const ModifyModal: React.FC<ModifyModalProps> = ({
|
||||
if (data?.current && Object.keys(data.current).length > 0) {
|
||||
const currentYaml = stringifyYaml(data.current, { lineWidth: 0 });
|
||||
setValuesYaml(currentYaml);
|
||||
setOriginalValuesYaml(currentYaml);
|
||||
setDiffData({ current: data.current, defaults: data.defaults ?? {} });
|
||||
}
|
||||
} catch (err) {
|
||||
@ -71,7 +69,6 @@ export const ModifyModal: React.FC<ModifyModalProps> = ({
|
||||
if (detail.values && Object.keys(detail.values).length > 0) {
|
||||
const y = stringifyYaml(detail.values, { lineWidth: 0 });
|
||||
setValuesYaml(y);
|
||||
setOriginalValuesYaml(y);
|
||||
}
|
||||
} catch (err2) {
|
||||
console.error('[ModifyModal] Failed to load instance detail:', err2);
|
||||
@ -143,29 +140,18 @@ export const ModifyModal: React.FC<ModifyModalProps> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const computeDeltaValues = (): Record<string, any> | undefined => {
|
||||
if (!valuesYaml.trim()) return undefined;
|
||||
try {
|
||||
const current = parseYaml(valuesYaml);
|
||||
if (!originalValuesYaml) return current; // no original to compare against
|
||||
const original = parseYaml(originalValuesYaml);
|
||||
return diffObjects(current, original);
|
||||
} catch {
|
||||
return parseValuesYaml(valuesYaml);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const deltaValues = computeDeltaValues();
|
||||
if (valuesYaml.trim()) {
|
||||
parseValuesYaml(valuesYaml);
|
||||
}
|
||||
const payload: UpdateInstanceRequest = {
|
||||
version: tag && tag !== instance.version ? tag : undefined,
|
||||
description: description.trim() || undefined,
|
||||
values: deltaValues,
|
||||
valuesYaml: valuesYaml.trim() || undefined,
|
||||
};
|
||||
|
||||
@ -268,7 +254,7 @@ export const ModifyModal: React.FC<ModifyModalProps> = ({
|
||||
Configuration Values
|
||||
</label>
|
||||
<p className="text-xs text-slate-500">
|
||||
Editing current deployed values. Only modified keys are sent to the server.
|
||||
Editing current deployed values. The full YAML is submitted so nested chart values stay intact.
|
||||
</p>
|
||||
{modifiedKeys.length > 0 && (
|
||||
<div className="flex flex-wrap items-center gap-1.5 text-xs">
|
||||
@ -368,27 +354,6 @@ export const ModifyModal: React.FC<ModifyModalProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
/** Deep diff: returns only keys in `current` whose values differ from `original` */
|
||||
const diffObjects = (current: Record<string, any>, original: Record<string, any>): Record<string, any> => {
|
||||
const delta: Record<string, any> = {};
|
||||
for (const key of Object.keys(current)) {
|
||||
const cv = current[key];
|
||||
const ov = original[key];
|
||||
if (JSON.stringify(cv) !== JSON.stringify(ov)) {
|
||||
if (typeof cv === 'object' && cv !== null && !Array.isArray(cv) &&
|
||||
typeof ov === 'object' && ov !== null && !Array.isArray(ov)) {
|
||||
const nested = diffObjects(cv, ov);
|
||||
if (Object.keys(nested).length > 0) {
|
||||
delta[key] = nested;
|
||||
}
|
||||
} else {
|
||||
delta[key] = cv;
|
||||
}
|
||||
}
|
||||
}
|
||||
return delta;
|
||||
};
|
||||
|
||||
const parseValuesYaml = (source: string): Record<string, any> => {
|
||||
const parsed = parseYaml(source);
|
||||
if (parsed == null) {
|
||||
|
||||
Reference in New Issue
Block a user