From f1448d512cc0caf814ac88ce3cc36e807aa8f3a4 Mon Sep 17 00:00:00 2001 From: Warp Agent Date: Sun, 28 Dec 2025 15:07:15 +0000 Subject: [PATCH] fix some bugs --- frontend/src/components/Layout.tsx | 2 +- frontend/src/pages/Dashboard.tsx | 140 ++++++++++++++++++++--------- frontend/src/pages/IAM.tsx | 105 +++++++++++++++------- 3 files changed, 169 insertions(+), 78 deletions(-) diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 0f5710d..9f58aea 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -171,7 +171,7 @@ export default function Layout() { {/* Page content */} -
+
diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index 7c4b1fa..9db78a2 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -25,6 +25,34 @@ import { ResponsiveContainer, } from 'recharts' +// Mock data - moved outside component to prevent re-creation on every render +const MOCK_ACTIVE_JOBS = [ + { + id: '1', + name: 'Daily Backup: VM-Cluster-01', + type: 'Replication', + progress: 45, + speed: '145 MB/s', + status: 'running', + eta: '1h 12m', + }, + { + id: '2', + name: 'ZFS Scrub: Pool-01', + type: 'Maintenance', + progress: 78, + speed: '1.2 GB/s', + status: 'running', + }, +] + +const MOCK_SYSTEM_LOGS = [ + { time: '10:45:22', level: 'INFO', source: 'systemd', message: 'Started User Manager for UID 1000.' }, + { time: '10:45:15', level: 'WARN', source: 'smartd', message: 'Device: /dev/ada5, SMART Usage Attribute: 194 Temperature_Celsius changed from 38 to 41' }, + { time: '10:44:58', level: 'INFO', source: 'kernel', message: 'ix0: link state changed to UP' }, + { time: '10:42:10', level: 'INFO', source: 'zfs', message: 'zfs_arc_reclaim_thread: reclaiming 157286400 bytes ...' }, +] + export default function Dashboard() { const [activeTab, setActiveTab] = useState<'jobs' | 'logs' | 'alerts'>('jobs') const [networkDataPoints, setNetworkDataPoints] = useState>([]) @@ -37,63 +65,83 @@ export default function Dashboard() { return response.data }, refetchInterval: refreshInterval * 1000, + staleTime: refreshInterval * 1000 * 2, // Consider data fresh for 2x the interval + refetchOnWindowFocus: false, // Don't refetch on window focus + refetchOnMount: false, // Don't refetch on mount if data is fresh + notifyOnChangeProps: ['data', 'error'], + structuralSharing: (oldData, newData) => { + // Only update if data actually changed + if (JSON.stringify(oldData) === JSON.stringify(newData)) { + return oldData + } + return newData + }, }) const { data: metrics } = useQuery({ queryKey: ['metrics'], queryFn: monitoringApi.getMetrics, refetchInterval: refreshInterval * 1000, + staleTime: refreshInterval * 1000 * 2, + refetchOnWindowFocus: false, + refetchOnMount: false, + notifyOnChangeProps: ['data', 'error'], + structuralSharing: (oldData, newData) => { + if (JSON.stringify(oldData) === JSON.stringify(newData)) { + return oldData + } + return newData + }, }) const { data: alerts } = useQuery({ queryKey: ['alerts', 'dashboard'], queryFn: () => monitoringApi.listAlerts({ is_acknowledged: false, limit: 10 }), refetchInterval: refreshInterval * 1000, + staleTime: refreshInterval * 1000 * 2, + refetchOnWindowFocus: false, + refetchOnMount: false, + notifyOnChangeProps: ['data', 'error'], + structuralSharing: (oldData, newData) => { + if (JSON.stringify(oldData) === JSON.stringify(newData)) { + return oldData + } + return newData + }, }) const { data: repositories = [] } = useQuery({ queryKey: ['storage', 'repositories'], queryFn: storageApi.listRepositories, + staleTime: 60 * 1000, // Consider repositories fresh for 60 seconds + refetchOnWindowFocus: false, + refetchOnMount: false, + notifyOnChangeProps: ['data', 'error'], + structuralSharing: (oldData, newData) => { + if (JSON.stringify(oldData) === JSON.stringify(newData)) { + return oldData + } + return newData + }, }) - // Calculate uptime (mock for now, would come from metrics) - const uptime = metrics?.system?.uptime_seconds || 0 - const days = Math.floor(uptime / 86400) - const hours = Math.floor((uptime % 86400) / 3600) - const minutes = Math.floor((uptime % 3600) / 60) + // Memoize uptime calculations to prevent recalculation on every render + const { days, hours, minutes } = useMemo(() => { + const uptimeValue = metrics?.system?.uptime_seconds || 0 + return { + days: Math.floor(uptimeValue / 86400), + hours: Math.floor((uptimeValue % 86400) / 3600), + minutes: Math.floor((uptimeValue % 3600) / 60), + } + }, [metrics?.system?.uptime_seconds]) - // Mock active jobs (would come from tasks API) - const activeJobs = [ - { - id: '1', - name: 'Daily Backup: VM-Cluster-01', - type: 'Replication', - progress: 45, - speed: '145 MB/s', - status: 'running', - eta: '1h 12m', - }, - { - id: '2', - name: 'ZFS Scrub: Pool-01', - type: 'Maintenance', - progress: 78, - speed: '1.2 GB/s', - status: 'running', - }, - ] - - // Mock system logs - const systemLogs = [ - { time: '10:45:22', level: 'INFO', source: 'systemd', message: 'Started User Manager for UID 1000.' }, - { time: '10:45:15', level: 'WARN', source: 'smartd', message: 'Device: /dev/ada5, SMART Usage Attribute: 194 Temperature_Celsius changed from 38 to 41' }, - { time: '10:44:58', level: 'INFO', source: 'kernel', message: 'ix0: link state changed to UP' }, - { time: '10:42:10', level: 'INFO', source: 'zfs', message: 'zfs_arc_reclaim_thread: reclaiming 157286400 bytes ...' }, - ] - - const totalStorage = Array.isArray(repositories) ? repositories.reduce((sum, repo) => sum + (repo?.size_bytes || 0), 0) : 0 - const usedStorage = Array.isArray(repositories) ? repositories.reduce((sum, repo) => sum + (repo?.used_bytes || 0), 0) : 0 - const storagePercent = totalStorage > 0 ? (usedStorage / totalStorage) * 100 : 0 + // Use memoized storage calculations to prevent unnecessary recalculations + const { totalStorage, usedStorage, storagePercent } = useMemo(() => { + const total = Array.isArray(repositories) ? repositories.reduce((sum, repo) => sum + (repo?.size_bytes || 0), 0) : 0 + const used = Array.isArray(repositories) ? repositories.reduce((sum, repo) => sum + (repo?.used_bytes || 0), 0) : 0 + const percent = total > 0 ? (used / total) * 100 : 0 + return { totalStorage: total, usedStorage: used, storagePercent: percent } + }, [repositories]) // Initialize network data useEffect(() => { @@ -157,11 +205,15 @@ export default function Dashboard() { return Math.max(...networkDataPoints.map((d) => d.inbound + d.outbound)) }, [networkDataPoints]) - const systemStatus = health?.status === 'healthy' ? 'System Healthy' : 'System Degraded' - const isHealthy = health?.status === 'healthy' + // Memoize system status to prevent recalculation + const { systemStatus, isHealthy } = useMemo(() => { + const status = health?.status === 'healthy' ? 'System Healthy' : 'System Degraded' + const healthy = health?.status === 'healthy' + return { systemStatus: status, isHealthy: healthy } + }, [health?.status]) return ( -
+
{/* Header */}
@@ -420,9 +472,9 @@ export default function Dashboard() { }`} > Active Jobs{' '} - {activeJobs.length > 0 && ( + {MOCK_ACTIVE_JOBS.length > 0 && ( - {activeJobs.length} + {MOCK_ACTIVE_JOBS.length} )} @@ -473,7 +525,7 @@ export default function Dashboard() { - {activeJobs.map((job) => ( + {MOCK_ACTIVE_JOBS.map((job) => ( {job.name} {job.type} @@ -519,7 +571,7 @@ export default function Dashboard() {
- {systemLogs.map((log, idx) => ( + {MOCK_SYSTEM_LOGS.map((log, idx) => (
{log.time} diff --git a/frontend/src/pages/IAM.tsx b/frontend/src/pages/IAM.tsx index 4797c96..c287ba5 100644 --- a/frontend/src/pages/IAM.tsx +++ b/frontend/src/pages/IAM.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useRef } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { ChevronRight, Search, Filter, UserPlus, History, ChevronLeft, MoreVertical, Lock, Verified, Wrench, Eye, HardDrive, Shield, ArrowRight, Network, ChevronRight as ChevronRightIcon, X, Edit, Trash2, User as UserIcon, Plus } from 'lucide-react' import { Button } from '@/components/ui/button' @@ -651,6 +651,30 @@ function EditUserForm({ user, onClose, onSuccess }: EditUserFormProps) { const [selectedRole, setSelectedRole] = useState('') const [selectedGroup, setSelectedGroup] = useState('') const queryClient = useQueryClient() + + // Use refs to always get the latest state values + const userRolesRef = useRef(userRoles) + const userGroupsRef = useRef(userGroups) + + // Update refs whenever state changes + useEffect(() => { + userRolesRef.current = userRoles + console.log('useEffect userRoles - state updated:', userRoles) + }, [userRoles]) + + useEffect(() => { + userGroupsRef.current = userGroups + console.log('useEffect userGroups - state updated:', userGroups) + }, [userGroups]) + + // Debug: log state changes + useEffect(() => { + console.log('EditUserForm - userRoles state changed:', userRoles) + }, [userRoles]) + + useEffect(() => { + console.log('EditUserForm - userGroups state changed:', userGroups) + }, [userGroups]) // Fetch available roles and groups const { data: availableRoles = [] } = useQuery({ @@ -688,20 +712,27 @@ function EditUserForm({ user, onClose, onSuccess }: EditUserFormProps) { mutationFn: (roleName: string) => iamApi.assignRoleToUser(user.id, roleName), onMutate: async (roleName: string) => { // Optimistic update: add role to state immediately + console.log('assignRoleMutation onMutate - BEFORE update, current userRoles:', userRolesRef.current) setUserRoles(prev => { - if (prev.includes(roleName)) return prev - return [...prev, roleName] + const newRoles = prev.includes(roleName) ? prev : [...prev, roleName] + console.log('assignRoleMutation onMutate - prev:', prev, 'roleName:', roleName, 'newRoles:', newRoles) + // Also update ref immediately + userRolesRef.current = newRoles + return newRoles }) setSelectedRole('') + console.log('assignRoleMutation onMutate - AFTER update, ref should be:', userRolesRef.current) }, - onSuccess: async () => { - // Verify with server data - const updatedUser = await iamApi.getUser(user.id) - setUserRoles(updatedUser.roles || []) + onSuccess: async (_, roleName: string) => { + // Don't overwrite state with server data - keep optimistic update + // Just invalidate queries for other components queryClient.invalidateQueries({ queryKey: ['iam-users'] }) - await queryClient.refetchQueries({ queryKey: ['iam-users'] }) queryClient.invalidateQueries({ queryKey: ['iam-user', user.id] }) - await queryClient.refetchQueries({ queryKey: ['iam-user', user.id] }) + // Use functional update to get current state + setUserRoles(current => { + console.log('assignRoleMutation onSuccess - roleName:', roleName, 'current userRoles:', current) + return current + }) }, onError: (error: any, roleName: string) => { console.error('Failed to assign role:', error, roleName) @@ -720,14 +751,12 @@ function EditUserForm({ user, onClose, onSuccess }: EditUserFormProps) { setUserRoles(prev => prev.filter(r => r !== roleName)) return { previousRoles } }, - onSuccess: async () => { - // Verify with server data - const updatedUser = await iamApi.getUser(user.id) - setUserRoles(updatedUser.roles || []) + onSuccess: async (_, roleName: string) => { + // Don't overwrite state with server data - keep optimistic update + // Just invalidate queries for other components queryClient.invalidateQueries({ queryKey: ['iam-users'] }) - await queryClient.refetchQueries({ queryKey: ['iam-users'] }) queryClient.invalidateQueries({ queryKey: ['iam-user', user.id] }) - await queryClient.refetchQueries({ queryKey: ['iam-user', user.id] }) + console.log('Role removed successfully:', roleName, 'Current userRoles:', userRoles) }, onError: (error: any, _roleName: string, context: any) => { console.error('Failed to remove role:', error) @@ -743,20 +772,27 @@ function EditUserForm({ user, onClose, onSuccess }: EditUserFormProps) { mutationFn: (groupName: string) => iamApi.assignGroupToUser(user.id, groupName), onMutate: async (groupName: string) => { // Optimistic update: add group to state immediately + console.log('assignGroupMutation onMutate - BEFORE update, current userGroups:', userGroupsRef.current) setUserGroups(prev => { - if (prev.includes(groupName)) return prev - return [...prev, groupName] + const newGroups = prev.includes(groupName) ? prev : [...prev, groupName] + console.log('assignGroupMutation onMutate - prev:', prev, 'groupName:', groupName, 'newGroups:', newGroups) + // Also update ref immediately + userGroupsRef.current = newGroups + return newGroups }) setSelectedGroup('') + console.log('assignGroupMutation onMutate - AFTER update, ref should be:', userGroupsRef.current) }, - onSuccess: async () => { - // Verify with server data - const updatedUser = await iamApi.getUser(user.id) - setUserGroups(updatedUser.groups || []) + onSuccess: async (_, groupName: string) => { + // Don't overwrite state with server data - keep optimistic update + // Just invalidate queries for other components queryClient.invalidateQueries({ queryKey: ['iam-users'] }) - await queryClient.refetchQueries({ queryKey: ['iam-users'] }) queryClient.invalidateQueries({ queryKey: ['iam-user', user.id] }) - await queryClient.refetchQueries({ queryKey: ['iam-user', user.id] }) + // Use functional update to get current state + setUserGroups(current => { + console.log('assignGroupMutation onSuccess - groupName:', groupName, 'current userGroups:', current) + return current + }) }, onError: (error: any, groupName: string) => { console.error('Failed to assign group:', error, groupName) @@ -775,14 +811,12 @@ function EditUserForm({ user, onClose, onSuccess }: EditUserFormProps) { setUserGroups(prev => prev.filter(g => g !== groupName)) return { previousGroups } }, - onSuccess: async () => { - // Verify with server data - const updatedUser = await iamApi.getUser(user.id) - setUserGroups(updatedUser.groups || []) + onSuccess: async (_, groupName: string) => { + // Don't overwrite state with server data - keep optimistic update + // Just invalidate queries for other components queryClient.invalidateQueries({ queryKey: ['iam-users'] }) - await queryClient.refetchQueries({ queryKey: ['iam-users'] }) queryClient.invalidateQueries({ queryKey: ['iam-user', user.id] }) - await queryClient.refetchQueries({ queryKey: ['iam-user', user.id] }) + console.log('Group removed successfully:', groupName, 'Current userGroups:', userGroups) }, onError: (error: any, _groupName: string, context: any) => { console.error('Failed to remove group:', error) @@ -796,16 +830,21 @@ function EditUserForm({ user, onClose, onSuccess }: EditUserFormProps) { const handleSubmit = (e: React.FormEvent) => { e.preventDefault() + // Use refs to get the latest state values (avoid closure issues) + const currentRoles = userRolesRef.current + const currentGroups = userGroupsRef.current const payload = { email: email.trim(), full_name: fullName.trim() || undefined, is_active: isActive, - roles: userRoles, - groups: userGroups, + roles: currentRoles, + groups: currentGroups, } console.log('EditUserForm - Submitting payload:', payload) - console.log('EditUserForm - userRoles:', userRoles) - console.log('EditUserForm - userGroups:', userGroups) + console.log('EditUserForm - currentRoles from ref:', currentRoles) + console.log('EditUserForm - currentGroups from ref:', currentGroups) + console.log('EditUserForm - userRoles from state:', userRoles) + console.log('EditUserForm - userGroups from state:', userGroups) updateMutation.mutate(payload) }