fix some bugs
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user