fix some bugs

This commit is contained in:
Warp Agent
2025-12-28 15:07:15 +00:00
parent 5021d46ba0
commit f1448d512c
3 changed files with 169 additions and 78 deletions

View File

@@ -171,7 +171,7 @@ export default function Layout() {
</div>
{/* Page content */}
<main className="flex-1 overflow-hidden">
<main className="flex-1 overflow-y-auto">
<Outlet />
</main>
</div>

View File

@@ -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<Array<{ time: string; inbound: number; outbound: number }>>([])
@@ -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 (
<div className="min-h-screen bg-background-dark text-white overflow-hidden">
<div className="h-full bg-background-dark text-white">
{/* Header */}
<header className="flex-none px-6 py-5 border-b border-border-dark bg-background-dark/95 backdrop-blur z-10">
<div className="flex flex-wrap justify-between items-end gap-3 max-w-[1600px] mx-auto">
@@ -420,9 +472,9 @@ export default function Dashboard() {
}`}
>
Active Jobs{' '}
{activeJobs.length > 0 && (
{MOCK_ACTIVE_JOBS.length > 0 && (
<span className="ml-2 bg-primary/20 text-primary px-1.5 py-0.5 rounded text-xs">
{activeJobs.length}
{MOCK_ACTIVE_JOBS.length}
</span>
)}
</button>
@@ -473,7 +525,7 @@ export default function Dashboard() {
</tr>
</thead>
<tbody className="text-sm divide-y divide-border-dark">
{activeJobs.map((job) => (
{MOCK_ACTIVE_JOBS.map((job) => (
<tr key={job.id} className="group hover:bg-[#233648] transition-colors">
<td className="px-6 py-4 font-medium text-white">{job.name}</td>
<td className="px-6 py-4 text-text-secondary">{job.type}</td>
@@ -519,7 +571,7 @@ export default function Dashboard() {
<div className="flex-1 overflow-y-auto custom-scrollbar bg-[#111a22]">
<table className="w-full text-left border-collapse">
<tbody className="text-sm font-mono divide-y divide-border-dark/50">
{systemLogs.map((log, idx) => (
{MOCK_SYSTEM_LOGS.map((log, idx) => (
<tr key={idx} className="group hover:bg-[#233648] transition-colors">
<td className="px-6 py-2 text-text-secondary w-32 whitespace-nowrap">
{log.time}

View File

@@ -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)
}