From c7981074d4fdb688b2574f63685e8b39f2fca05b Mon Sep 17 00:00:00 2001 From: keyhan Date: Fri, 15 May 2026 01:11:10 +0330 Subject: [PATCH] fix(ui): keep resource scaling form values while user is editing Skip syncing form state from the 5s resources poll when the form is dirty, and reset dirty only after apply or workload tab change. Co-authored-by: Cursor --- frontend/src/app/dashboard/apps/[id]/page.tsx | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/frontend/src/app/dashboard/apps/[id]/page.tsx b/frontend/src/app/dashboard/apps/[id]/page.tsx index 271acdd..8b1b975 100644 --- a/frontend/src/app/dashboard/apps/[id]/page.tsx +++ b/frontend/src/app/dashboard/apps/[id]/page.tsx @@ -70,6 +70,7 @@ export default function AppDetailPage() { replicas: 1, }); const [scaleWorkload, setScaleWorkload] = useState<'app' | 'database' | 'redis' | 'rabbitmq'>('app'); + const [resourceFormDirty, setResourceFormDirty] = useState(false); const [showDbDiskExpand, setShowDbDiskExpand] = useState(false); const [dbStorageSize, setDbStorageSize] = useState('1'); const [dbStorageLoading, setDbStorageLoading] = useState(false); @@ -467,8 +468,19 @@ export default function AppDetailPage() { return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; }; - // Sync form when resource data loads (selected workload) + const patchResourceForm = (patch: Partial) => { + setResourceFormDirty(true); + setResourceForm((f) => ({ ...f, ...patch })); + }; + + // Sync form when workload changes or when live metrics refresh — but not while user is editing. useEffect(() => { + setResourceFormDirty(false); + }, [scaleWorkload]); + + useEffect(() => { + if (resourceFormDirty) return; + const workloads = resourceUsage?.workloads; const w = workloads?.find((x) => x.key === scaleWorkload) || @@ -487,7 +499,7 @@ export default function AppDetailPage() { replicas: w.configured.replicas ?? 1, }); } - }, [resourceUsage, scaleWorkload]); + }, [resourceUsage, scaleWorkload, resourceFormDirty]); // Auto-scroll logs to bottom useEffect(() => { @@ -560,6 +572,7 @@ export default function AppDetailPage() { mutationFn: (data: { cpuRequest?: string; cpuLimit?: string; memoryRequest?: string; memoryLimit?: string; replicas?: number }) => api.post(`/billing/applications/${appId}/upgrade`, data), onSuccess: (res) => { + setResourceFormDirty(false); invalidateAll(); queryClient.invalidateQueries({ queryKey: ['resources', appId] }); queryClient.invalidateQueries({ queryKey: ['wallet'] }); @@ -585,6 +598,7 @@ export default function AppDetailPage() { memoryLimit?: string; }) => api.patch(`/applications/${appId}/resources`, data), onSuccess: () => { + setResourceFormDirty(false); invalidateAll(); queryClient.invalidateQueries({ queryKey: ['resources', appId] }); toast.success('Resources updated'); @@ -2107,7 +2121,7 @@ export default function AppDetailPage() { setResourceForm((f) => ({ ...f, cpuRequest: e.target.value }))} + onChange={(e) => patchResourceForm({ cpuRequest: e.target.value })} className="input-field text-sm" placeholder="100m" /> @@ -2117,7 +2131,7 @@ export default function AppDetailPage() { setResourceForm((f) => ({ ...f, cpuLimit: e.target.value }))} + onChange={(e) => patchResourceForm({ cpuLimit: e.target.value })} className="input-field text-sm" placeholder="500m" /> @@ -2127,7 +2141,7 @@ export default function AppDetailPage() { setResourceForm((f) => ({ ...f, memoryRequest: e.target.value }))} + onChange={(e) => patchResourceForm({ memoryRequest: e.target.value })} className="input-field text-sm" placeholder="128Mi" /> @@ -2137,7 +2151,7 @@ export default function AppDetailPage() { setResourceForm((f) => ({ ...f, memoryLimit: e.target.value }))} + onChange={(e) => patchResourceForm({ memoryLimit: e.target.value })} className="input-field text-sm" placeholder="512Mi" /> @@ -2148,7 +2162,7 @@ export default function AppDetailPage() {