mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-11-11 01:16:12 +00:00
Updated frontend to snake_case and fixed bugs with some pages that were not showing. Fixed authentication side.
This commit is contained in:
@@ -27,70 +27,70 @@ function App() {
|
||||
<Routes>
|
||||
<Route path="/login" element={<Login />} />
|
||||
<Route path="/" element={
|
||||
<ProtectedRoute requirePermission="canViewDashboard">
|
||||
<ProtectedRoute requirePermission="can_view_dashboard">
|
||||
<Layout>
|
||||
<Dashboard />
|
||||
</Layout>
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/hosts" element={
|
||||
<ProtectedRoute requirePermission="canViewHosts">
|
||||
<ProtectedRoute requirePermission="can_view_hosts">
|
||||
<Layout>
|
||||
<Hosts />
|
||||
</Layout>
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/hosts/:hostId" element={
|
||||
<ProtectedRoute requirePermission="canViewHosts">
|
||||
<ProtectedRoute requirePermission="can_view_hosts">
|
||||
<Layout>
|
||||
<HostDetail />
|
||||
</Layout>
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/packages" element={
|
||||
<ProtectedRoute requirePermission="canViewPackages">
|
||||
<ProtectedRoute requirePermission="can_view_packages">
|
||||
<Layout>
|
||||
<Packages />
|
||||
</Layout>
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/repositories" element={
|
||||
<ProtectedRoute requirePermission="canViewHosts">
|
||||
<ProtectedRoute requirePermission="can_view_hosts">
|
||||
<Layout>
|
||||
<Repositories />
|
||||
</Layout>
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/repositories/:repositoryId" element={
|
||||
<ProtectedRoute requirePermission="canViewHosts">
|
||||
<ProtectedRoute requirePermission="can_view_hosts">
|
||||
<Layout>
|
||||
<RepositoryDetail />
|
||||
</Layout>
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/users" element={
|
||||
<ProtectedRoute requirePermission="canViewUsers">
|
||||
<ProtectedRoute requirePermission="can_view_users">
|
||||
<Layout>
|
||||
<Users />
|
||||
</Layout>
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/permissions" element={
|
||||
<ProtectedRoute requirePermission="canManageSettings">
|
||||
<ProtectedRoute requirePermission="can_manage_settings">
|
||||
<Layout>
|
||||
<Permissions />
|
||||
</Layout>
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/settings" element={
|
||||
<ProtectedRoute requirePermission="canManageSettings">
|
||||
<ProtectedRoute requirePermission="can_manage_settings">
|
||||
<Layout>
|
||||
<Settings />
|
||||
</Layout>
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/options" element={
|
||||
<ProtectedRoute requirePermission="canManageHosts">
|
||||
<ProtectedRoute requirePermission="can_manage_hosts">
|
||||
<Layout>
|
||||
<Options />
|
||||
</Layout>
|
||||
@@ -104,7 +104,7 @@ function App() {
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
<Route path="/packages/:packageId" element={
|
||||
<ProtectedRoute requirePermission="canViewPackages">
|
||||
<ProtectedRoute requirePermission="can_view_packages">
|
||||
<Layout>
|
||||
<PackageDetail />
|
||||
</Layout>
|
||||
|
||||
@@ -46,11 +46,11 @@ const Layout = ({ children }) => {
|
||||
const userMenuRef = useRef(null)
|
||||
|
||||
// Fetch dashboard stats for the "Last updated" info
|
||||
const { data: stats, refetch } = useQuery({
|
||||
const { data: stats, refetch, isFetching } = useQuery({
|
||||
queryKey: ['dashboardStats'],
|
||||
queryFn: () => dashboardAPI.getStats().then(res => res.data),
|
||||
refetchInterval: 60000, // Refresh every minute
|
||||
staleTime: 30000, // Consider data stale after 30 seconds
|
||||
staleTime: 5 * 60 * 1000, // Data stays fresh for 5 minutes
|
||||
refetchOnWindowFocus: false, // Don't refetch when window regains focus
|
||||
})
|
||||
|
||||
// Fetch version info
|
||||
@@ -477,10 +477,11 @@ const Layout = ({ children }) => {
|
||||
<span className="truncate">Updated: {formatRelativeTimeShort(stats.lastUpdated)}</span>
|
||||
<button
|
||||
onClick={() => refetch()}
|
||||
className="p-1 hover:bg-secondary-100 dark:hover:bg-secondary-700 rounded flex-shrink-0"
|
||||
disabled={isFetching}
|
||||
className="p-1 hover:bg-secondary-100 dark:hover:bg-secondary-700 rounded flex-shrink-0 disabled:opacity-50"
|
||||
title="Refresh data"
|
||||
>
|
||||
<RefreshCw className="h-3 w-3" />
|
||||
<RefreshCw className={`h-3 w-3 ${isFetching ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
{versionInfo && (
|
||||
<span className="text-xs text-secondary-400 dark:text-secondary-500 flex-shrink-0">
|
||||
@@ -516,10 +517,11 @@ const Layout = ({ children }) => {
|
||||
<div className="flex flex-col items-center py-1 border-t border-secondary-200 dark:border-secondary-700">
|
||||
<button
|
||||
onClick={() => refetch()}
|
||||
className="p-1 hover:bg-secondary-100 dark:hover:bg-secondary-700 rounded"
|
||||
disabled={isFetching}
|
||||
className="p-1 hover:bg-secondary-100 dark:hover:bg-secondary-700 rounded disabled:opacity-50"
|
||||
title={`Refresh data - Updated: ${formatRelativeTimeShort(stats.lastUpdated)}`}
|
||||
>
|
||||
<RefreshCw className="h-3 w-3" />
|
||||
<RefreshCw className={`h-3 w-3 ${isFetching ? 'animate-spin' : ''}`} />
|
||||
</button>
|
||||
{versionInfo && (
|
||||
<span className="text-xs text-secondary-400 dark:text-secondary-500 mt-1">
|
||||
|
||||
@@ -15,6 +15,7 @@ export const AuthProvider = ({ children }) => {
|
||||
const [token, setToken] = useState(null)
|
||||
const [permissions, setPermissions] = useState(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [permissionsLoading, setPermissionsLoading] = useState(false)
|
||||
|
||||
// Initialize auth state from localStorage
|
||||
useEffect(() => {
|
||||
@@ -42,20 +43,17 @@ export const AuthProvider = ({ children }) => {
|
||||
setIsLoading(false)
|
||||
}, [])
|
||||
|
||||
// Periodically refresh permissions when user is logged in
|
||||
// Refresh permissions when user logs in (no automatic refresh)
|
||||
useEffect(() => {
|
||||
if (token && user) {
|
||||
// Refresh permissions every 30 seconds
|
||||
const interval = setInterval(() => {
|
||||
refreshPermissions()
|
||||
}, 30000)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
// Only refresh permissions once when user logs in
|
||||
refreshPermissions()
|
||||
}
|
||||
}, [token, user])
|
||||
|
||||
const fetchPermissions = async (authToken) => {
|
||||
try {
|
||||
setPermissionsLoading(true)
|
||||
const response = await fetch('/api/v1/permissions/user-permissions', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authToken}`,
|
||||
@@ -74,6 +72,8 @@ export const AuthProvider = ({ children }) => {
|
||||
} catch (error) {
|
||||
console.error('Error fetching permissions:', error)
|
||||
return null
|
||||
} finally {
|
||||
setPermissionsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,25 +199,29 @@ export const AuthProvider = ({ children }) => {
|
||||
|
||||
// Permission checking functions
|
||||
const hasPermission = (permission) => {
|
||||
// If permissions are still loading, return false to show loading state
|
||||
if (permissionsLoading) {
|
||||
return false
|
||||
}
|
||||
return permissions?.[permission] === true
|
||||
}
|
||||
|
||||
const canViewDashboard = () => hasPermission('canViewDashboard')
|
||||
const canViewHosts = () => hasPermission('canViewHosts')
|
||||
const canManageHosts = () => hasPermission('canManageHosts')
|
||||
const canViewPackages = () => hasPermission('canViewPackages')
|
||||
const canManagePackages = () => hasPermission('canManagePackages')
|
||||
const canViewUsers = () => hasPermission('canViewUsers')
|
||||
const canManageUsers = () => hasPermission('canManageUsers')
|
||||
const canViewReports = () => hasPermission('canViewReports')
|
||||
const canExportData = () => hasPermission('canExportData')
|
||||
const canManageSettings = () => hasPermission('canManageSettings')
|
||||
const canViewDashboard = () => hasPermission('can_view_dashboard')
|
||||
const canViewHosts = () => hasPermission('can_view_hosts')
|
||||
const canManageHosts = () => hasPermission('can_manage_hosts')
|
||||
const canViewPackages = () => hasPermission('can_view_packages')
|
||||
const canManagePackages = () => hasPermission('can_manage_packages')
|
||||
const canViewUsers = () => hasPermission('can_view_users')
|
||||
const canManageUsers = () => hasPermission('can_manage_users')
|
||||
const canViewReports = () => hasPermission('can_view_reports')
|
||||
const canExportData = () => hasPermission('can_export_data')
|
||||
const canManageSettings = () => hasPermission('can_manage_settings')
|
||||
|
||||
const value = {
|
||||
user,
|
||||
token,
|
||||
permissions,
|
||||
isLoading,
|
||||
isLoading: isLoading || permissionsLoading,
|
||||
login,
|
||||
logout,
|
||||
updateProfile,
|
||||
|
||||
@@ -19,7 +19,8 @@ export const UpdateNotificationProvider = ({ children }) => {
|
||||
const { data: updateData, isLoading, error } = useQuery({
|
||||
queryKey: ['updateCheck'],
|
||||
queryFn: () => versionAPI.checkUpdates().then(res => res.data),
|
||||
refetchInterval: 5 * 60 * 1000, // Refetch every 5 minutes
|
||||
staleTime: 10 * 60 * 1000, // Data stays fresh for 10 minutes
|
||||
refetchOnWindowFocus: false, // Don't refetch when window regains focus
|
||||
retry: 1
|
||||
})
|
||||
|
||||
|
||||
@@ -89,11 +89,11 @@ const Dashboard = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const { data: stats, isLoading, error, refetch } = useQuery({
|
||||
const { data: stats, isLoading, error, refetch, isFetching } = useQuery({
|
||||
queryKey: ['dashboardStats'],
|
||||
queryFn: () => dashboardAPI.getStats().then(res => res.data),
|
||||
refetchInterval: 300000, // Refresh every 5 minutes instead of 1 minute
|
||||
staleTime: 120000, // Consider data stale after 2 minutes
|
||||
staleTime: 5 * 60 * 1000, // Data stays fresh for 5 minutes
|
||||
refetchOnWindowFocus: false, // Don't refetch when window regains focus
|
||||
})
|
||||
|
||||
// Fetch settings to get the agent update interval
|
||||
@@ -579,6 +579,26 @@ const Dashboard = () => {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Page Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-secondary-900 dark:text-white">Dashboard</h1>
|
||||
<p className="text-sm text-secondary-600 dark:text-secondary-400 mt-1">
|
||||
Overview of your PatchMon infrastructure
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => refetch()}
|
||||
disabled={isFetching}
|
||||
className="btn-outline flex items-center gap-2"
|
||||
title="Refresh dashboard data"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${isFetching ? 'animate-spin' : ''}`} />
|
||||
{isFetching ? 'Refreshing...' : 'Refresh'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Dynamically Rendered Cards - Unified Order */}
|
||||
{(() => {
|
||||
|
||||
@@ -46,15 +46,25 @@ const HostDetail = () => {
|
||||
const [isEditingFriendlyName, setIsEditingFriendlyName] = useState(false)
|
||||
const [editedFriendlyName, setEditedFriendlyName] = useState('')
|
||||
const [showAllUpdates, setShowAllUpdates] = useState(false)
|
||||
const [activeTab, setActiveTab] = useState('host')
|
||||
const [activeTab, setActiveTab] = useState(() => {
|
||||
// Restore tab state from localStorage
|
||||
const savedTab = localStorage.getItem(`host-detail-tab-${hostId}`)
|
||||
return savedTab || 'host'
|
||||
})
|
||||
|
||||
const { data: host, isLoading, error, refetch } = useQuery({
|
||||
const { data: host, isLoading, error, refetch, isFetching } = useQuery({
|
||||
queryKey: ['host', hostId],
|
||||
queryFn: () => dashboardAPI.getHostDetail(hostId).then(res => res.data),
|
||||
refetchInterval: 60000,
|
||||
staleTime: 30000,
|
||||
staleTime: 5 * 60 * 1000, // 5 minutes - data stays fresh longer
|
||||
refetchOnWindowFocus: false, // Don't refetch when window regains focus
|
||||
})
|
||||
|
||||
// Save tab state to localStorage when it changes
|
||||
const handleTabChange = (tabName) => {
|
||||
setActiveTab(tabName)
|
||||
localStorage.setItem(`host-detail-tab-${hostId}`, tabName)
|
||||
}
|
||||
|
||||
// Auto-show credentials modal for new/pending hosts
|
||||
React.useEffect(() => {
|
||||
if (host && host.status === 'pending') {
|
||||
@@ -72,7 +82,7 @@ const HostDetail = () => {
|
||||
|
||||
// Toggle auto-update mutation
|
||||
const toggleAutoUpdateMutation = useMutation({
|
||||
mutationFn: (autoUpdate) => adminHostsAPI.toggleAutoUpdate(hostId, autoUpdate).then(res => res.data),
|
||||
mutationFn: (auto_update) => adminHostsAPI.toggleAutoUpdate(hostId, auto_update).then(res => res.data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['host', hostId])
|
||||
queryClient.invalidateQueries(['hosts'])
|
||||
@@ -88,7 +98,7 @@ const HostDetail = () => {
|
||||
})
|
||||
|
||||
const handleDeleteHost = async () => {
|
||||
if (window.confirm(`Are you sure you want to delete host "${host.friendlyName}"? This action cannot be undone.`)) {
|
||||
if (window.confirm(`Are you sure you want to delete host "${host.friendly_name}"? This action cannot be undone.`)) {
|
||||
try {
|
||||
await deleteHostMutation.mutateAsync(hostId)
|
||||
} catch (error) {
|
||||
@@ -178,7 +188,7 @@ const HostDetail = () => {
|
||||
return 'Up to Date'
|
||||
}
|
||||
|
||||
const isStale = new Date() - new Date(host.lastUpdate) > 24 * 60 * 60 * 1000
|
||||
const isStale = new Date() - new Date(host.last_update) > 24 * 60 * 60 * 1000
|
||||
|
||||
return (
|
||||
<div className="h-screen flex flex-col">
|
||||
@@ -188,18 +198,18 @@ const HostDetail = () => {
|
||||
<Link to="/hosts" className="text-secondary-500 hover:text-secondary-700 dark:text-secondary-400 dark:hover:text-secondary-200">
|
||||
<ArrowLeft className="h-5 w-5" />
|
||||
</Link>
|
||||
<h1 className="text-xl font-semibold text-secondary-900 dark:text-white">{host.friendlyName}</h1>
|
||||
{host.systemUptime && (
|
||||
<h1 className="text-xl font-semibold text-secondary-900 dark:text-white">{host.friendly_name}</h1>
|
||||
{host.system_uptime && (
|
||||
<div className="flex items-center gap-1 text-sm text-secondary-600 dark:text-secondary-400">
|
||||
<Clock className="h-4 w-4" />
|
||||
<span className="text-xs font-medium">Uptime:</span>
|
||||
<span>{host.systemUptime}</span>
|
||||
<span>{host.system_uptime}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center gap-1 text-sm text-secondary-600 dark:text-secondary-400">
|
||||
<Clock className="h-4 w-4" />
|
||||
<span className="text-xs font-medium">Last updated:</span>
|
||||
<span>{formatRelativeTime(host.lastUpdate)}</span>
|
||||
<span>{formatRelativeTime(host.last_update)}</span>
|
||||
</div>
|
||||
<div className={`flex items-center gap-2 px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(isStale, host.stats.outdatedPackages > 0)}`}>
|
||||
{getStatusIcon(isStale, host.stats.outdatedPackages > 0)}
|
||||
@@ -207,6 +217,15 @@ const HostDetail = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => refetch()}
|
||||
disabled={isFetching}
|
||||
className="btn-outline flex items-center gap-2 text-sm"
|
||||
title="Refresh host data"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${isFetching ? 'animate-spin' : ''}`} />
|
||||
{isFetching ? 'Refreshing...' : 'Refresh'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowCredentialsModal(true)}
|
||||
className="btn-outline flex items-center gap-2 text-sm"
|
||||
@@ -232,7 +251,7 @@ const HostDetail = () => {
|
||||
<div className="card">
|
||||
<div className="flex border-b border-secondary-200 dark:border-secondary-600">
|
||||
<button
|
||||
onClick={() => setActiveTab('host')}
|
||||
onClick={() => handleTabChange('host')}
|
||||
className={`px-4 py-2 text-sm font-medium ${
|
||||
activeTab === 'host'
|
||||
? 'text-primary-600 dark:text-primary-400 border-b-2 border-primary-500'
|
||||
@@ -242,17 +261,7 @@ const HostDetail = () => {
|
||||
Host Info
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('hardware')}
|
||||
className={`px-4 py-2 text-sm font-medium ${
|
||||
activeTab === 'hardware'
|
||||
? 'text-primary-600 dark:text-primary-400 border-b-2 border-primary-500'
|
||||
: 'text-secondary-500 dark:text-secondary-400 hover:text-secondary-700 dark:hover:text-secondary-300'
|
||||
}`}
|
||||
>
|
||||
Hardware
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('network')}
|
||||
onClick={() => handleTabChange('network')}
|
||||
className={`px-4 py-2 text-sm font-medium ${
|
||||
activeTab === 'network'
|
||||
? 'text-primary-600 dark:text-primary-400 border-b-2 border-primary-500'
|
||||
@@ -262,7 +271,7 @@ const HostDetail = () => {
|
||||
Network
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('system')}
|
||||
onClick={() => handleTabChange('system')}
|
||||
className={`px-4 py-2 text-sm font-medium ${
|
||||
activeTab === 'system'
|
||||
? 'text-primary-600 dark:text-primary-400 border-b-2 border-primary-500'
|
||||
@@ -272,17 +281,17 @@ const HostDetail = () => {
|
||||
System
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('monitoring')}
|
||||
onClick={() => handleTabChange('monitoring')}
|
||||
className={`px-4 py-2 text-sm font-medium ${
|
||||
activeTab === 'monitoring'
|
||||
? 'text-primary-600 dark:text-primary-400 border-b-2 border-primary-500'
|
||||
: 'text-secondary-500 dark:text-secondary-400 hover:text-secondary-700 dark:hover:text-secondary-300'
|
||||
}`}
|
||||
>
|
||||
Resource Monitor
|
||||
Resource
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('history')}
|
||||
onClick={() => handleTabChange('history')}
|
||||
className={`px-4 py-2 text-sm font-medium ${
|
||||
activeTab === 'history'
|
||||
? 'text-primary-600 dark:text-primary-400 border-b-2 border-primary-500'
|
||||
@@ -301,7 +310,7 @@ const HostDetail = () => {
|
||||
<div>
|
||||
<p className="text-xs text-secondary-500 dark:text-secondary-300 mb-1">Friendly Name</p>
|
||||
<InlineEdit
|
||||
value={host.friendlyName}
|
||||
value={host.friendly_name}
|
||||
onSave={(newName) => updateFriendlyNameMutation.mutate(newName)}
|
||||
placeholder="Enter friendly name..."
|
||||
maxLength={100}
|
||||
@@ -341,8 +350,8 @@ const HostDetail = () => {
|
||||
<div>
|
||||
<p className="text-xs text-secondary-500 dark:text-secondary-300">Operating System</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<OSIcon osType={host.osType} className="h-4 w-4" />
|
||||
<p className="font-medium text-secondary-900 dark:text-white text-sm">{host.osType} {host.osVersion}</p>
|
||||
<OSIcon osType={host.os_type} className="h-4 w-4" />
|
||||
<p className="font-medium text-secondary-900 dark:text-white text-sm">{host.os_type} {host.os_version}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -356,29 +365,29 @@ const HostDetail = () => {
|
||||
|
||||
<div>
|
||||
<p className="text-xs text-secondary-500 dark:text-secondary-300">Last Update</p>
|
||||
<p className="font-medium text-secondary-900 dark:text-white text-sm">{formatRelativeTime(host.lastUpdate)}</p>
|
||||
<p className="font-medium text-secondary-900 dark:text-white text-sm">{formatRelativeTime(host.last_update)}</p>
|
||||
</div>
|
||||
|
||||
{host.agentVersion && (
|
||||
{host.agent_version && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-xs text-secondary-500 dark:text-secondary-300">Agent Version</p>
|
||||
<p className="font-medium text-secondary-900 dark:text-white text-sm">{host.agentVersion}</p>
|
||||
<p className="font-medium text-secondary-900 dark:text-white text-sm">{host.agent_version}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-secondary-500 dark:text-secondary-300">Auto-update</span>
|
||||
<button
|
||||
onClick={() => toggleAutoUpdateMutation.mutate(!host.autoUpdate)}
|
||||
onClick={() => toggleAutoUpdateMutation.mutate(!host.auto_update)}
|
||||
disabled={toggleAutoUpdateMutation.isPending}
|
||||
className={`relative inline-flex h-5 w-9 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 ${
|
||||
host.autoUpdate
|
||||
host.auto_update
|
||||
? 'bg-primary-600 dark:bg-primary-500'
|
||||
: 'bg-secondary-200 dark:bg-secondary-600'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-3 w-3 transform rounded-full bg-white transition-transform ${
|
||||
host.autoUpdate ? 'translate-x-5' : 'translate-x-1'
|
||||
host.auto_update ? 'translate-x-5' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
@@ -389,88 +398,34 @@ const HostDetail = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Hardware Information */}
|
||||
{activeTab === 'hardware' && (host.cpuModel || host.ramInstalled || host.diskDetails) && (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{host.cpuModel && (
|
||||
<div>
|
||||
<p className="text-xs text-secondary-500 dark:text-secondary-300">CPU Model</p>
|
||||
<p className="font-medium text-secondary-900 dark:text-white text-sm">{host.cpuModel}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{host.cpuCores && (
|
||||
<div>
|
||||
<p className="text-xs text-secondary-500 dark:text-secondary-300">CPU Cores</p>
|
||||
<p className="font-medium text-secondary-900 dark:text-white text-sm">{host.cpuCores}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{host.ramInstalled && (
|
||||
<div>
|
||||
<p className="text-xs text-secondary-500 dark:text-secondary-300">RAM Installed</p>
|
||||
<p className="font-medium text-secondary-900 dark:text-white text-sm">{host.ramInstalled} GB</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{host.swapSize !== undefined && (
|
||||
<div>
|
||||
<p className="text-xs text-secondary-500 dark:text-secondary-300">Swap Size</p>
|
||||
<p className="font-medium text-secondary-900 dark:text-white text-sm">{host.swapSize} GB</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{host.diskDetails && Array.isArray(host.diskDetails) && host.diskDetails.length > 0 && (
|
||||
<div className="pt-4 border-t border-secondary-200 dark:border-secondary-600">
|
||||
<h4 className="text-sm font-medium text-secondary-900 dark:text-white mb-3">Disk Details</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{host.diskDetails.map((disk, index) => (
|
||||
<div key={index} className="bg-secondary-50 dark:bg-secondary-700 p-3 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<HardDrive className="h-4 w-4 text-secondary-500" />
|
||||
<span className="font-medium text-secondary-900 dark:text-white text-sm">{disk.name}</span>
|
||||
</div>
|
||||
<p className="text-xs text-secondary-600 dark:text-secondary-300">Size: {disk.size}</p>
|
||||
{disk.mountpoint && (
|
||||
<p className="text-xs text-secondary-600 dark:text-secondary-300">Mount: {disk.mountpoint}</p>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Network Information */}
|
||||
{activeTab === 'network' && (host.gatewayIp || host.dnsServers || host.networkInterfaces) && (
|
||||
{activeTab === 'network' && (host.gateway_ip || host.dns_servers || host.network_interfaces) && (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{host.gatewayIp && (
|
||||
{host.gateway_ip && (
|
||||
<div>
|
||||
<p className="text-xs text-secondary-500 dark:text-secondary-300">Gateway IP</p>
|
||||
<p className="font-medium text-secondary-900 dark:text-white font-mono text-sm">{host.gatewayIp}</p>
|
||||
<p className="font-medium text-secondary-900 dark:text-white font-mono text-sm">{host.gateway_ip}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{host.dnsServers && Array.isArray(host.dnsServers) && host.dnsServers.length > 0 && (
|
||||
{host.dns_servers && Array.isArray(host.dns_servers) && host.dns_servers.length > 0 && (
|
||||
<div>
|
||||
<p className="text-xs text-secondary-500 dark:text-secondary-300">DNS Servers</p>
|
||||
<div className="space-y-1">
|
||||
{host.dnsServers.map((dns, index) => (
|
||||
{host.dns_servers.map((dns, index) => (
|
||||
<p key={index} className="font-medium text-secondary-900 dark:text-white font-mono text-sm">{dns}</p>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{host.networkInterfaces && Array.isArray(host.networkInterfaces) && host.networkInterfaces.length > 0 && (
|
||||
{host.network_interfaces && Array.isArray(host.network_interfaces) && host.network_interfaces.length > 0 && (
|
||||
<div>
|
||||
<p className="text-xs text-secondary-500 dark:text-secondary-300">Network Interfaces</p>
|
||||
<div className="space-y-1">
|
||||
{host.networkInterfaces.map((iface, index) => (
|
||||
{host.network_interfaces.map((iface, index) => (
|
||||
<p key={index} className="font-medium text-secondary-900 dark:text-white text-sm">{iface.name}</p>
|
||||
))}
|
||||
</div>
|
||||
@@ -481,7 +436,7 @@ const HostDetail = () => {
|
||||
)}
|
||||
|
||||
{/* System Information */}
|
||||
{activeTab === 'system' && (host.kernelVersion || host.selinuxStatus || host.architecture) && (
|
||||
{activeTab === 'system' && (host.kernel_version || host.selinux_status || host.architecture) && (
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{host.architecture && (
|
||||
@@ -491,24 +446,24 @@ const HostDetail = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{host.kernelVersion && (
|
||||
{host.kernel_version && (
|
||||
<div>
|
||||
<p className="text-xs text-secondary-500 dark:text-secondary-300">Kernel Version</p>
|
||||
<p className="font-medium text-secondary-900 dark:text-white font-mono text-sm">{host.kernelVersion}</p>
|
||||
<p className="font-medium text-secondary-900 dark:text-white font-mono text-sm">{host.kernel_version}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{host.selinuxStatus && (
|
||||
{host.selinux_status && (
|
||||
<div>
|
||||
<p className="text-xs text-secondary-500 dark:text-secondary-300">SELinux Status</p>
|
||||
<span className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
|
||||
host.selinuxStatus === 'enabled'
|
||||
host.selinux_status === 'enabled'
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200'
|
||||
: host.selinuxStatus === 'permissive'
|
||||
: host.selinux_status === 'permissive'
|
||||
? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'
|
||||
: 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200'
|
||||
}`}>
|
||||
{host.selinuxStatus}
|
||||
{host.selinux_status}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -518,22 +473,15 @@ const HostDetail = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Empty state for tabs with no data */}
|
||||
{activeTab === 'hardware' && !(host.cpuModel || host.ramInstalled || host.diskDetails) && (
|
||||
<div className="text-center py-8">
|
||||
<Cpu className="h-8 w-8 text-secondary-400 mx-auto mb-2" />
|
||||
<p className="text-sm text-secondary-500 dark:text-secondary-300">No hardware information available</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'network' && !(host.gatewayIp || host.dnsServers || host.networkInterfaces) && (
|
||||
{activeTab === 'network' && !(host.gateway_ip || host.dns_servers || host.network_interfaces) && (
|
||||
<div className="text-center py-8">
|
||||
<Wifi className="h-8 w-8 text-secondary-400 mx-auto mb-2" />
|
||||
<p className="text-sm text-secondary-500 dark:text-secondary-300">No network information available</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'system' && !(host.kernelVersion || host.selinuxStatus || host.architecture) && (
|
||||
{activeTab === 'system' && !(host.kernel_version || host.selinux_status || host.architecture) && (
|
||||
<div className="text-center py-8">
|
||||
<Terminal className="h-8 w-8 text-secondary-400 mx-auto mb-2" />
|
||||
<p className="text-sm text-secondary-500 dark:text-secondary-300">No system information available</p>
|
||||
@@ -541,35 +489,143 @@ const HostDetail = () => {
|
||||
)}
|
||||
|
||||
{/* System Monitoring */}
|
||||
{activeTab === 'monitoring' && host.loadAverage && Array.isArray(host.loadAverage) && host.loadAverage.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
{activeTab === 'monitoring' && (
|
||||
<div className="space-y-6">
|
||||
{/* System Overview */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<div>
|
||||
<p className="text-xs text-secondary-500 dark:text-secondary-300">Load Average</p>
|
||||
<p className="font-medium text-secondary-900 dark:text-white text-sm">
|
||||
{host.loadAverage.map((load, index) => (
|
||||
<span key={index}>
|
||||
{load.toFixed(2)}
|
||||
{index < host.loadAverage.length - 1 && ', '}
|
||||
</span>
|
||||
{/* System Uptime */}
|
||||
{host.system_uptime && (
|
||||
<div className="bg-secondary-50 dark:bg-secondary-700 p-4 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Clock className="h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||||
<p className="text-xs text-secondary-500 dark:text-secondary-300">System Uptime</p>
|
||||
</div>
|
||||
<p className="font-medium text-secondary-900 dark:text-white text-sm">{host.system_uptime}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CPU Model */}
|
||||
{host.cpu_model && (
|
||||
<div className="bg-secondary-50 dark:bg-secondary-700 p-4 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Cpu className="h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||||
<p className="text-xs text-secondary-500 dark:text-secondary-300">CPU Model</p>
|
||||
</div>
|
||||
<p className="font-medium text-secondary-900 dark:text-white text-sm">{host.cpu_model}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CPU Cores */}
|
||||
{host.cpu_cores && (
|
||||
<div className="bg-secondary-50 dark:bg-secondary-700 p-4 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Cpu className="h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||||
<p className="text-xs text-secondary-500 dark:text-secondary-300">CPU Cores</p>
|
||||
</div>
|
||||
<p className="font-medium text-secondary-900 dark:text-white text-sm">{host.cpu_cores}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* RAM Installed */}
|
||||
{host.ram_installed && (
|
||||
<div className="bg-secondary-50 dark:bg-secondary-700 p-4 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<MemoryStick className="h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||||
<p className="text-xs text-secondary-500 dark:text-secondary-300">RAM Installed</p>
|
||||
</div>
|
||||
<p className="font-medium text-secondary-900 dark:text-white text-sm">{host.ram_installed} GB</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Swap Size */}
|
||||
{host.swap_size !== undefined && host.swap_size !== null && (
|
||||
<div className="bg-secondary-50 dark:bg-secondary-700 p-4 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<MemoryStick className="h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||||
<p className="text-xs text-secondary-500 dark:text-secondary-300">Swap Size</p>
|
||||
</div>
|
||||
<p className="font-medium text-secondary-900 dark:text-white text-sm">{host.swap_size} GB</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Load Average */}
|
||||
{host.load_average && Array.isArray(host.load_average) && host.load_average.length > 0 && host.load_average.some(load => load != null) && (
|
||||
<div className="bg-secondary-50 dark:bg-secondary-700 p-4 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Activity className="h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||||
<p className="text-xs text-secondary-500 dark:text-secondary-300">Load Average</p>
|
||||
</div>
|
||||
<p className="font-medium text-secondary-900 dark:text-white text-sm">
|
||||
{host.load_average.filter(load => load != null).map((load, index) => (
|
||||
<span key={index}>
|
||||
{typeof load === 'number' ? load.toFixed(2) : String(load)}
|
||||
{index < host.load_average.filter(load => load != null).length - 1 && ', '}
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Disk Information */}
|
||||
{host.disk_details && Array.isArray(host.disk_details) && host.disk_details.length > 0 && (
|
||||
<div className="pt-4 border-t border-secondary-200 dark:border-secondary-600">
|
||||
<h4 className="text-sm font-medium text-secondary-900 dark:text-white mb-3 flex items-center gap-2">
|
||||
<HardDrive className="h-4 w-4 text-primary-600 dark:text-primary-400" />
|
||||
Disk Usage
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{host.disk_details.map((disk, index) => (
|
||||
<div key={index} className="bg-secondary-50 dark:bg-secondary-700 p-3 rounded-lg">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<HardDrive className="h-4 w-4 text-secondary-500" />
|
||||
<span className="font-medium text-secondary-900 dark:text-white text-sm">{disk.name || `Disk ${index + 1}`}</span>
|
||||
</div>
|
||||
{disk.size && (
|
||||
<p className="text-xs text-secondary-600 dark:text-secondary-300 mb-1">Size: {disk.size}</p>
|
||||
)}
|
||||
{disk.mountpoint && (
|
||||
<p className="text-xs text-secondary-600 dark:text-secondary-300 mb-1">Mount: {disk.mountpoint}</p>
|
||||
)}
|
||||
{disk.usage && typeof disk.usage === 'number' && (
|
||||
<div className="mt-2">
|
||||
<div className="flex justify-between text-xs text-secondary-600 dark:text-secondary-300 mb-1">
|
||||
<span>Usage</span>
|
||||
<span>{disk.usage}%</span>
|
||||
</div>
|
||||
<div className="w-full bg-secondary-200 dark:bg-secondary-600 rounded-full h-2">
|
||||
<div
|
||||
className="bg-primary-600 dark:bg-primary-400 h-2 rounded-full transition-all duration-300"
|
||||
style={{ width: `${Math.min(Math.max(disk.usage, 0), 100)}%` }}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* No Data State */}
|
||||
{!host.system_uptime && !host.cpu_model && !host.cpu_cores && !host.ram_installed && host.swap_size === undefined &&
|
||||
(!host.load_average || !Array.isArray(host.load_average) || host.load_average.length === 0 || !host.load_average.some(load => load != null)) &&
|
||||
(!host.disk_details || !Array.isArray(host.disk_details) || host.disk_details.length === 0) && (
|
||||
<div className="text-center py-8">
|
||||
<Monitor className="h-8 w-8 text-secondary-400 mx-auto mb-2" />
|
||||
<p className="text-sm text-secondary-500 dark:text-secondary-300">No monitoring data available</p>
|
||||
<p className="text-xs text-secondary-400 dark:text-secondary-400 mt-1">
|
||||
Monitoring data will appear once the agent collects system information
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'monitoring' && (!host.loadAverage || !Array.isArray(host.loadAverage) || host.loadAverage.length === 0) && (
|
||||
<div className="text-center py-8">
|
||||
<Monitor className="h-8 w-8 text-secondary-400 mx-auto mb-2" />
|
||||
<p className="text-sm text-secondary-500 dark:text-secondary-300">No monitoring data available</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Update History */}
|
||||
{activeTab === 'history' && (
|
||||
<div className="overflow-x-auto">
|
||||
{host.updateHistory?.length > 0 ? (
|
||||
{host.update_history?.length > 0 ? (
|
||||
<>
|
||||
<table className="min-w-full divide-y divide-secondary-200 dark:divide-secondary-600">
|
||||
<thead className="bg-secondary-50 dark:bg-secondary-700">
|
||||
@@ -589,7 +645,7 @@ const HostDetail = () => {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-secondary-800 divide-y divide-secondary-200 dark:divide-secondary-600">
|
||||
{(showAllUpdates ? host.updateHistory : host.updateHistory.slice(0, 5)).map((update, index) => (
|
||||
{(showAllUpdates ? host.update_history : host.update_history.slice(0, 5)).map((update, index) => (
|
||||
<tr key={update.id} className="hover:bg-secondary-50 dark:hover:bg-secondary-700">
|
||||
<td className="px-4 py-2 whitespace-nowrap">
|
||||
<div className="flex items-center gap-1.5">
|
||||
@@ -607,14 +663,14 @@ const HostDetail = () => {
|
||||
{formatDate(update.timestamp)}
|
||||
</td>
|
||||
<td className="px-4 py-2 whitespace-nowrap text-xs text-secondary-900 dark:text-white">
|
||||
{update.packagesCount}
|
||||
{update.packages_count}
|
||||
</td>
|
||||
<td className="px-4 py-2 whitespace-nowrap">
|
||||
{update.securityCount > 0 ? (
|
||||
{update.security_count > 0 ? (
|
||||
<div className="flex items-center gap-1">
|
||||
<Shield className="h-3 w-3 text-danger-600" />
|
||||
<span className="text-xs text-danger-600 font-medium">
|
||||
{update.securityCount}
|
||||
{update.security_count}
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
@@ -626,7 +682,7 @@ const HostDetail = () => {
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{host.updateHistory.length > 5 && (
|
||||
{host.update_history.length > 5 && (
|
||||
<div className="px-4 py-2 border-t border-secondary-200 dark:border-secondary-600 bg-secondary-50 dark:bg-secondary-700">
|
||||
<button
|
||||
onClick={() => setShowAllUpdates(!showAllUpdates)}
|
||||
@@ -640,7 +696,7 @@ const HostDetail = () => {
|
||||
) : (
|
||||
<>
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
Show All ({host.updateHistory.length} total)
|
||||
Show All ({host.update_history.length} total)
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
@@ -746,7 +802,7 @@ const CredentialsModal = ({ host, isOpen, onClose }) => {
|
||||
}
|
||||
|
||||
const getSetupCommands = () => {
|
||||
return `# Run this on the target host: ${host?.friendlyName}
|
||||
return `# Run this on the target host: ${host?.friendly_name}
|
||||
|
||||
echo "🔄 Setting up PatchMon agent..."
|
||||
|
||||
@@ -788,7 +844,7 @@ echo " - View logs: tail -f /var/log/patchmon-agent.log"`
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white dark:bg-secondary-800 rounded-lg p-6 w-full max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-medium text-secondary-900 dark:text-white">Host Setup - {host.friendlyName}</h3>
|
||||
<h3 className="text-lg font-medium text-secondary-900 dark:text-white">Host Setup - {host.friendly_name}</h3>
|
||||
<button onClick={onClose} className="text-secondary-400 hover:text-secondary-600 dark:text-secondary-500 dark:hover:text-secondary-300">
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
@@ -1069,7 +1125,7 @@ const DeleteConfirmationModal = ({ host, isOpen, onClose, onConfirm, isLoading }
|
||||
<div className="mb-6">
|
||||
<p className="text-secondary-700 dark:text-secondary-300">
|
||||
Are you sure you want to delete the host{' '}
|
||||
<span className="font-semibold">"{host.friendlyName}"</span>?
|
||||
<span className="font-semibold">"{host.friendly_name}"</span>?
|
||||
</p>
|
||||
<div className="mt-3 p-3 bg-danger-50 dark:bg-danger-900 border border-danger-200 dark:border-danger-700 rounded-md">
|
||||
<p className="text-sm text-danger-800 dark:text-danger-200">
|
||||
|
||||
@@ -38,7 +38,7 @@ import InlineGroupEdit from '../components/InlineGroupEdit'
|
||||
// Add Host Modal Component
|
||||
const AddHostModal = ({ isOpen, onClose, onSuccess }) => {
|
||||
const [formData, setFormData] = useState({
|
||||
friendlyName: '',
|
||||
friendly_name: '',
|
||||
hostGroupId: ''
|
||||
})
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
@@ -62,7 +62,7 @@ const AddHostModal = ({ isOpen, onClose, onSuccess }) => {
|
||||
const response = await adminHostsAPI.create(formData)
|
||||
console.log('Host created successfully:', response.data)
|
||||
onSuccess(response.data)
|
||||
setFormData({ friendlyName: '', hostGroupId: '' })
|
||||
setFormData({ friendly_name: '', hostGroupId: '' })
|
||||
onClose()
|
||||
} catch (err) {
|
||||
console.error('Full error object:', err)
|
||||
@@ -105,8 +105,8 @@ const AddHostModal = ({ isOpen, onClose, onSuccess }) => {
|
||||
<input
|
||||
type="text"
|
||||
required
|
||||
value={formData.friendlyName}
|
||||
onChange={(e) => setFormData({ ...formData, friendlyName: e.target.value })}
|
||||
value={formData.friendly_name}
|
||||
onChange={(e) => setFormData({ ...formData, friendly_name: e.target.value })}
|
||||
className="block w-full px-3 py-2.5 text-base border-2 border-secondary-300 dark:border-secondary-600 rounded-lg shadow-sm focus:ring-2 focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white transition-all duration-200"
|
||||
placeholder="server.example.com"
|
||||
/>
|
||||
@@ -252,7 +252,7 @@ echo "0 * * * * /usr/local/bin/patchmon-agent.sh update >/dev/null 2>&1" | sudo
|
||||
|
||||
fullSetup: `#!/bin/bash
|
||||
# Complete PatchMon Agent Setup Script
|
||||
# Run this on the target host: ${host?.friendlyName}
|
||||
# Run this on the target host: ${host?.friendly_name}
|
||||
|
||||
echo "🔄 Setting up PatchMon agent..."
|
||||
|
||||
@@ -295,7 +295,7 @@ echo " - View logs: tail -f /var/log/patchmon-agent.log"`
|
||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-lg p-6 w-full max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-medium text-secondary-900">Host Setup - {host.friendlyName}</h3>
|
||||
<h3 className="text-lg font-medium text-secondary-900">Host Setup - {host.friendly_name}</h3>
|
||||
<button onClick={onClose} className="text-secondary-400 hover:text-secondary-600">
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
@@ -351,7 +351,7 @@ echo " - View logs: tail -f /var/log/patchmon-agent.log"`
|
||||
<div className="bg-green-50 border border-green-200 rounded-md p-4">
|
||||
<h4 className="text-sm font-medium text-green-800 mb-2">🚀 One-Line Installation</h4>
|
||||
<p className="text-sm text-green-700">
|
||||
Copy and paste this single command on <strong>{host.friendlyName}</strong> to install and configure the PatchMon agent automatically.
|
||||
Copy and paste this single command on <strong>{host.friendly_name}</strong> to install and configure the PatchMon agent automatically.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -378,7 +378,7 @@ echo " - View logs: tail -f /var/log/patchmon-agent.log"`
|
||||
<ul className="text-sm text-blue-700 space-y-1">
|
||||
<li>• Downloads the PatchMon installation script</li>
|
||||
<li>• Installs the agent to <code>/usr/local/bin/patchmon-agent.sh</code></li>
|
||||
<li>• Configures API credentials for <strong>{host.friendlyName}</strong></li>
|
||||
<li>• Configures API credentials for <strong>{host.friendly_name}</strong></li>
|
||||
<li>• Tests the connection to PatchMon server</li>
|
||||
<li>• Sends initial package data</li>
|
||||
<li>• Sets up hourly automatic updates via crontab</li>
|
||||
@@ -444,7 +444,7 @@ echo " - View logs: tail -f /var/log/patchmon-agent.log"`
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-md p-4">
|
||||
<h4 className="text-sm font-medium text-amber-800 mb-2">⚠️ Security Note</h4>
|
||||
<p className="text-sm text-amber-700">
|
||||
Keep these credentials secure. They provide access to update package information for <strong>{host.friendlyName}</strong> only.
|
||||
Keep these credentials secure. They provide access to update package information for <strong>{host.friendly_name}</strong> only.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -455,7 +455,7 @@ echo " - View logs: tail -f /var/log/patchmon-agent.log"`
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-md p-4">
|
||||
<h4 className="text-sm font-medium text-blue-800 mb-2">📋 Step-by-Step Setup</h4>
|
||||
<p className="text-sm text-blue-700">
|
||||
Follow these commands on <strong>{host.friendlyName}</strong> to install and configure the PatchMon agent.
|
||||
Follow these commands on <strong>{host.friendly_name}</strong> to install and configure the PatchMon agent.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -552,7 +552,7 @@ echo " - View logs: tail -f /var/log/patchmon-agent.log"`
|
||||
<div className="bg-green-50 border border-green-200 rounded-md p-4">
|
||||
<h4 className="text-sm font-medium text-green-800 mb-2">🚀 Automated Setup</h4>
|
||||
<p className="text-sm text-green-700">
|
||||
Copy this complete setup script to <strong>{host.friendlyName}</strong> and run it to automatically install and configure everything.
|
||||
Copy this complete setup script to <strong>{host.friendly_name}</strong> and run it to automatically install and configure everything.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -573,7 +573,7 @@ echo " - View logs: tail -f /var/log/patchmon-agent.log"`
|
||||
<div className="mt-3 text-sm text-secondary-600">
|
||||
<p><strong>Usage:</strong></p>
|
||||
<p>1. Copy the script above</p>
|
||||
<p>2. Save it to a file on {host.friendlyName} (e.g., <code>setup-patchmon.sh</code>)</p>
|
||||
<p>2. Save it to a file on {host.friendly_name} (e.g., <code>setup-patchmon.sh</code>)</p>
|
||||
<p>3. Run: <code>chmod +x setup-patchmon.sh && sudo ./setup-patchmon.sh</code></p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -666,65 +666,38 @@ const Hosts = () => {
|
||||
{ id: 'ip', label: 'IP Address', visible: false, order: 2 },
|
||||
{ id: 'group', label: 'Group', visible: true, order: 3 },
|
||||
{ id: 'os', label: 'OS', visible: true, order: 4 },
|
||||
{ id: 'osVersion', label: 'OS Version', visible: false, order: 5 },
|
||||
{ id: 'agentVersion', label: 'Agent Version', visible: true, order: 6 },
|
||||
{ id: 'autoUpdate', label: 'Auto-update', visible: true, order: 7 },
|
||||
{ id: 'os_version', label: 'OS Version', visible: false, order: 5 },
|
||||
{ id: 'agent_version', label: 'Agent Version', visible: true, order: 6 },
|
||||
{ id: 'auto_update', label: 'Auto-update', visible: true, order: 7 },
|
||||
{ id: 'status', label: 'Status', visible: true, order: 8 },
|
||||
{ id: 'updates', label: 'Updates', visible: true, order: 9 },
|
||||
{ id: 'lastUpdate', label: 'Last Update', visible: true, order: 10 },
|
||||
{ id: 'last_update', label: 'Last Update', visible: true, order: 10 },
|
||||
{ id: 'actions', label: 'Actions', visible: true, order: 11 }
|
||||
]
|
||||
|
||||
const saved = localStorage.getItem('hosts-column-config')
|
||||
if (saved) {
|
||||
const savedConfig = JSON.parse(saved)
|
||||
|
||||
// Check if agentVersion column exists in saved config
|
||||
const hasAgentVersion = savedConfig.some(col => col.id === 'agentVersion')
|
||||
const hasAutoUpdate = savedConfig.some(col => col.id === 'autoUpdate')
|
||||
|
||||
let needsUpdate = false
|
||||
let updatedConfig = [...savedConfig]
|
||||
|
||||
if (!hasAgentVersion) {
|
||||
// Add agentVersion column to saved config
|
||||
const agentVersionColumn = { id: 'agentVersion', label: 'Agent Version', visible: true, order: 6 }
|
||||
try {
|
||||
const savedConfig = JSON.parse(saved)
|
||||
|
||||
// Insert agentVersion column at the correct position
|
||||
updatedConfig = updatedConfig.map(col => {
|
||||
if (col.order >= 6) {
|
||||
return { ...col, order: col.order + 1 }
|
||||
}
|
||||
return col
|
||||
})
|
||||
// Check if we have old camelCase column IDs that need to be migrated
|
||||
const hasOldColumns = savedConfig.some(col =>
|
||||
col.id === 'agentVersion' || col.id === 'autoUpdate' || col.id === 'osVersion' || col.id === 'lastUpdate'
|
||||
)
|
||||
|
||||
updatedConfig.push(agentVersionColumn)
|
||||
needsUpdate = true
|
||||
if (hasOldColumns) {
|
||||
// Clear the old configuration and use the default snake_case configuration
|
||||
localStorage.removeItem('hosts-column-config')
|
||||
return defaultConfig
|
||||
} else {
|
||||
// Use the existing configuration
|
||||
return savedConfig
|
||||
}
|
||||
} catch (error) {
|
||||
// If there's an error parsing the config, clear it and use default
|
||||
localStorage.removeItem('hosts-column-config')
|
||||
return defaultConfig
|
||||
}
|
||||
|
||||
if (!hasAutoUpdate) {
|
||||
// Add autoUpdate column to saved config
|
||||
const autoUpdateColumn = { id: 'autoUpdate', label: 'Auto-update', visible: true, order: 7 }
|
||||
|
||||
// Insert autoUpdate column at the correct position
|
||||
updatedConfig = updatedConfig.map(col => {
|
||||
if (col.order >= 7) {
|
||||
return { ...col, order: col.order + 1 }
|
||||
}
|
||||
return col
|
||||
})
|
||||
|
||||
updatedConfig.push(autoUpdateColumn)
|
||||
needsUpdate = true
|
||||
}
|
||||
|
||||
if (needsUpdate) {
|
||||
updatedConfig.sort((a, b) => a.order - b.order)
|
||||
localStorage.setItem('hosts-column-config', JSON.stringify(updatedConfig))
|
||||
return updatedConfig
|
||||
}
|
||||
|
||||
return savedConfig
|
||||
}
|
||||
|
||||
return defaultConfig
|
||||
@@ -732,11 +705,11 @@ const Hosts = () => {
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
const { data: hosts, isLoading, error, refetch } = useQuery({
|
||||
const { data: hosts, isLoading, error, refetch, isFetching } = useQuery({
|
||||
queryKey: ['hosts'],
|
||||
queryFn: () => dashboardAPI.getHosts().then(res => res.data),
|
||||
refetchInterval: 300000, // Refresh every 5 minutes instead of 1 minute
|
||||
staleTime: 120000, // Consider data stale after 2 minutes
|
||||
staleTime: 5 * 60 * 1000, // Data stays fresh for 5 minutes
|
||||
refetchOnWindowFocus: false, // Don't refetch when window regains focus
|
||||
})
|
||||
|
||||
const { data: hostGroups } = useQuery({
|
||||
@@ -776,7 +749,7 @@ const Hosts = () => {
|
||||
|
||||
// Toggle auto-update mutation
|
||||
const toggleAutoUpdateMutation = useMutation({
|
||||
mutationFn: ({ hostId, autoUpdate }) => adminHostsAPI.toggleAutoUpdate(hostId, autoUpdate).then(res => res.data),
|
||||
mutationFn: ({ hostId, auto_update }) => adminHostsAPI.toggleAutoUpdate(hostId, auto_update).then(res => res.data),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(['hosts'])
|
||||
}
|
||||
@@ -859,9 +832,9 @@ const Hosts = () => {
|
||||
let filtered = hosts.filter(host => {
|
||||
// Search filter
|
||||
const matchesSearch = searchTerm === '' ||
|
||||
host.friendlyName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
host.friendly_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
host.ip?.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
host.osType?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
host.os_type?.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
|
||||
// Group filter
|
||||
const matchesGroup = groupFilter === 'all' ||
|
||||
@@ -872,7 +845,7 @@ const Hosts = () => {
|
||||
const matchesStatus = statusFilter === 'all' || (host.effectiveStatus || host.status) === statusFilter
|
||||
|
||||
// OS filter
|
||||
const matchesOs = osFilter === 'all' || host.osType?.toLowerCase() === osFilter.toLowerCase()
|
||||
const matchesOs = osFilter === 'all' || host.os_type?.toLowerCase() === osFilter.toLowerCase()
|
||||
|
||||
// URL filter for hosts needing updates, inactive hosts, or up-to-date hosts
|
||||
const filter = searchParams.get('filter')
|
||||
@@ -893,8 +866,8 @@ const Hosts = () => {
|
||||
|
||||
switch (sortField) {
|
||||
case 'friendlyName':
|
||||
aValue = a.friendlyName.toLowerCase()
|
||||
bValue = b.friendlyName.toLowerCase()
|
||||
aValue = a.friendly_name.toLowerCase()
|
||||
bValue = b.friendly_name.toLowerCase()
|
||||
break
|
||||
case 'hostname':
|
||||
aValue = a.hostname?.toLowerCase() || 'zzz_no_hostname'
|
||||
@@ -909,16 +882,16 @@ const Hosts = () => {
|
||||
bValue = b.hostGroup?.name || 'zzz_ungrouped'
|
||||
break
|
||||
case 'os':
|
||||
aValue = a.osType?.toLowerCase() || 'zzz_unknown'
|
||||
bValue = b.osType?.toLowerCase() || 'zzz_unknown'
|
||||
aValue = a.os_type?.toLowerCase() || 'zzz_unknown'
|
||||
bValue = b.os_type?.toLowerCase() || 'zzz_unknown'
|
||||
break
|
||||
case 'osVersion':
|
||||
aValue = a.osVersion?.toLowerCase() || 'zzz_unknown'
|
||||
bValue = b.osVersion?.toLowerCase() || 'zzz_unknown'
|
||||
case 'os_version':
|
||||
aValue = a.os_version?.toLowerCase() || 'zzz_unknown'
|
||||
bValue = b.os_version?.toLowerCase() || 'zzz_unknown'
|
||||
break
|
||||
case 'agentVersion':
|
||||
aValue = a.agentVersion?.toLowerCase() || 'zzz_no_version'
|
||||
bValue = b.agentVersion?.toLowerCase() || 'zzz_no_version'
|
||||
case 'agent_version':
|
||||
aValue = a.agent_version?.toLowerCase() || 'zzz_no_version'
|
||||
bValue = b.agent_version?.toLowerCase() || 'zzz_no_version'
|
||||
break
|
||||
case 'status':
|
||||
aValue = a.effectiveStatus || a.status
|
||||
@@ -928,9 +901,9 @@ const Hosts = () => {
|
||||
aValue = a.updatesCount || 0
|
||||
bValue = b.updatesCount || 0
|
||||
break
|
||||
case 'lastUpdate':
|
||||
aValue = new Date(a.lastUpdate)
|
||||
bValue = new Date(b.lastUpdate)
|
||||
case 'last_update':
|
||||
aValue = new Date(a.last_update)
|
||||
bValue = new Date(b.last_update)
|
||||
break
|
||||
default:
|
||||
aValue = a[sortField]
|
||||
@@ -962,7 +935,7 @@ const Hosts = () => {
|
||||
groupKey = (host.effectiveStatus || host.status).charAt(0).toUpperCase() + (host.effectiveStatus || host.status).slice(1)
|
||||
break
|
||||
case 'os':
|
||||
groupKey = host.osType || 'Unknown'
|
||||
groupKey = host.os_type || 'Unknown'
|
||||
break
|
||||
default:
|
||||
groupKey = 'All Hosts'
|
||||
@@ -1022,10 +995,10 @@ const Hosts = () => {
|
||||
{ id: 'ip', label: 'IP Address', visible: false, order: 3 },
|
||||
{ id: 'group', label: 'Group', visible: true, order: 4 },
|
||||
{ id: 'os', label: 'OS', visible: true, order: 5 },
|
||||
{ id: 'osVersion', label: 'OS Version', visible: false, order: 6 },
|
||||
{ id: 'os_version', label: 'OS Version', visible: false, order: 6 },
|
||||
{ id: 'status', label: 'Status', visible: true, order: 7 },
|
||||
{ id: 'updates', label: 'Updates', visible: true, order: 8 },
|
||||
{ id: 'lastUpdate', label: 'Last Update', visible: true, order: 9 },
|
||||
{ id: 'last_update', label: 'Last Update', visible: true, order: 9 },
|
||||
{ id: 'actions', label: 'Actions', visible: true, order: 10 }
|
||||
]
|
||||
updateColumnConfig(defaultConfig)
|
||||
@@ -1055,7 +1028,7 @@ const Hosts = () => {
|
||||
case 'host':
|
||||
return (
|
||||
<InlineEdit
|
||||
value={host.friendlyName}
|
||||
value={host.friendly_name}
|
||||
onSave={(newName) => updateFriendlyNameMutation.mutate({ hostId: host.id, friendlyName: newName })}
|
||||
placeholder="Enter friendly name..."
|
||||
maxLength={100}
|
||||
@@ -1101,30 +1074,30 @@ const Hosts = () => {
|
||||
case 'os':
|
||||
return (
|
||||
<div className="flex items-center gap-2 text-sm text-secondary-900 dark:text-white">
|
||||
<OSIcon osType={host.osType} className="h-4 w-4" />
|
||||
<span>{host.osType}</span>
|
||||
<OSIcon osType={host.os_type} className="h-4 w-4" />
|
||||
<span>{host.os_type}</span>
|
||||
</div>
|
||||
)
|
||||
case 'osVersion':
|
||||
case 'os_version':
|
||||
return (
|
||||
<div className="text-sm text-secondary-900 dark:text-white">
|
||||
{host.osVersion || 'N/A'}
|
||||
{host.os_version || 'N/A'}
|
||||
</div>
|
||||
)
|
||||
case 'agentVersion':
|
||||
case 'agent_version':
|
||||
return (
|
||||
<div className="text-sm text-secondary-900 dark:text-white">
|
||||
{host.agentVersion || 'N/A'}
|
||||
{host.agent_version || 'N/A'}
|
||||
</div>
|
||||
)
|
||||
case 'autoUpdate':
|
||||
case 'auto_update':
|
||||
return (
|
||||
<span className={`text-sm font-medium ${
|
||||
host.autoUpdate
|
||||
host.auto_update
|
||||
? 'text-green-600 dark:text-green-400'
|
||||
: 'text-red-600 dark:text-red-400'
|
||||
}`}>
|
||||
{host.autoUpdate ? 'Yes' : 'No'}
|
||||
{host.auto_update ? 'Yes' : 'No'}
|
||||
</span>
|
||||
)
|
||||
case 'status':
|
||||
@@ -1139,10 +1112,10 @@ const Hosts = () => {
|
||||
{host.updatesCount || 0}
|
||||
</div>
|
||||
)
|
||||
case 'lastUpdate':
|
||||
case 'last_update':
|
||||
return (
|
||||
<div className="text-sm text-secondary-500 dark:text-secondary-300">
|
||||
{formatRelativeTime(host.lastUpdate)}
|
||||
{formatRelativeTime(host.last_update)}
|
||||
</div>
|
||||
)
|
||||
case 'actions':
|
||||
@@ -1260,6 +1233,33 @@ const Hosts = () => {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Page Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-secondary-900 dark:text-white">Hosts</h1>
|
||||
<p className="text-sm text-secondary-600 dark:text-secondary-400 mt-1">
|
||||
Manage and monitor your connected hosts
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => refetch()}
|
||||
disabled={isFetching}
|
||||
className="btn-outline flex items-center gap-2"
|
||||
title="Refresh hosts data"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${isFetching ? 'animate-spin' : ''}`} />
|
||||
{isFetching ? 'Refreshing...' : 'Refresh'}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowAddModal(true)}
|
||||
className="btn-primary flex items-center gap-2"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Add Host
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stats Summary */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-4 gap-4 mb-4">
|
||||
@@ -1550,23 +1550,23 @@ const Hosts = () => {
|
||||
{column.label}
|
||||
{getSortIcon('os')}
|
||||
</button>
|
||||
) : column.id === 'osVersion' ? (
|
||||
) : column.id === 'os_version' ? (
|
||||
<button
|
||||
onClick={() => handleSort('osVersion')}
|
||||
onClick={() => handleSort('os_version')}
|
||||
className="flex items-center gap-2 hover:text-secondary-700"
|
||||
>
|
||||
{column.label}
|
||||
{getSortIcon('osVersion')}
|
||||
{getSortIcon('os_version')}
|
||||
</button>
|
||||
) : column.id === 'agentVersion' ? (
|
||||
) : column.id === 'agent_version' ? (
|
||||
<button
|
||||
onClick={() => handleSort('agentVersion')}
|
||||
onClick={() => handleSort('agent_version')}
|
||||
className="flex items-center gap-2 hover:text-secondary-700"
|
||||
>
|
||||
{column.label}
|
||||
{getSortIcon('agentVersion')}
|
||||
{getSortIcon('agent_version')}
|
||||
</button>
|
||||
) : column.id === 'autoUpdate' ? (
|
||||
) : column.id === 'auto_update' ? (
|
||||
<div className="flex items-center gap-2 font-normal text-xs text-secondary-500 dark:text-secondary-300 normal-case tracking-wider">
|
||||
{column.label}
|
||||
</div>
|
||||
@@ -1586,13 +1586,13 @@ const Hosts = () => {
|
||||
{column.label}
|
||||
{getSortIcon('updates')}
|
||||
</button>
|
||||
) : column.id === 'lastUpdate' ? (
|
||||
) : column.id === 'last_update' ? (
|
||||
<button
|
||||
onClick={() => handleSort('lastUpdate')}
|
||||
onClick={() => handleSort('last_update')}
|
||||
className="flex items-center gap-2 hover:text-secondary-700"
|
||||
>
|
||||
{column.label}
|
||||
{getSortIcon('lastUpdate')}
|
||||
{getSortIcon('last_update')}
|
||||
</button>
|
||||
) : (
|
||||
column.label
|
||||
@@ -1679,7 +1679,7 @@ const BulkAssignModal = ({ selectedHosts, hosts, onClose, onAssign, isLoading })
|
||||
|
||||
const selectedHostNames = hosts
|
||||
.filter(host => selectedHosts.includes(host.id))
|
||||
.map(host => host.friendlyName)
|
||||
.map(host => host.friendly_name)
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
Server,
|
||||
Users,
|
||||
AlertTriangle,
|
||||
CheckCircle
|
||||
CheckCircle,
|
||||
Settings
|
||||
} from 'lucide-react'
|
||||
import { hostGroupsAPI } from '../utils/api'
|
||||
|
||||
@@ -214,7 +215,7 @@ const Options = () => {
|
||||
|
||||
const renderComingSoonTab = (tabName) => (
|
||||
<div className="text-center py-12">
|
||||
<SettingsIcon className="h-12 w-12 text-secondary-400 mx-auto mb-4" />
|
||||
<Settings className="h-12 w-12 text-secondary-400 mx-auto mb-4" />
|
||||
<h3 className="text-lg font-medium text-secondary-900 dark:text-white mb-2">
|
||||
{tabName} Coming Soon
|
||||
</h3>
|
||||
|
||||
@@ -98,19 +98,19 @@ const Packages = () => {
|
||||
}
|
||||
}, [searchParams])
|
||||
|
||||
const { data: packages, isLoading, error, refetch } = useQuery({
|
||||
const { data: packages, isLoading, error, refetch, isFetching } = useQuery({
|
||||
queryKey: ['packages'],
|
||||
queryFn: () => dashboardAPI.getPackages().then(res => res.data),
|
||||
refetchInterval: 300000, // Refresh every 5 minutes instead of 1 minute
|
||||
staleTime: 120000, // Consider data stale after 2 minutes
|
||||
staleTime: 5 * 60 * 1000, // Data stays fresh for 5 minutes
|
||||
refetchOnWindowFocus: false, // Don't refetch when window regains focus
|
||||
})
|
||||
|
||||
// Fetch hosts data to get total packages count
|
||||
const { data: hosts } = useQuery({
|
||||
queryKey: ['hosts'],
|
||||
queryFn: () => dashboardAPI.getHosts().then(res => res.data),
|
||||
refetchInterval: 300000, // Refresh every 5 minutes instead of 1 minute
|
||||
staleTime: 120000, // Consider data stale after 2 minutes
|
||||
staleTime: 5 * 60 * 1000, // Data stays fresh for 5 minutes
|
||||
refetchOnWindowFocus: false, // Don't refetch when window regains focus
|
||||
})
|
||||
|
||||
// Filter and sort packages
|
||||
@@ -330,6 +330,26 @@ const Packages = () => {
|
||||
|
||||
return (
|
||||
<div className="h-[calc(100vh-7rem)] flex flex-col overflow-hidden">
|
||||
{/* Page Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold text-secondary-900 dark:text-white">Packages</h1>
|
||||
<p className="text-sm text-secondary-600 dark:text-secondary-400 mt-1">
|
||||
Manage package updates and security patches
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => refetch()}
|
||||
disabled={isFetching}
|
||||
className="btn-outline flex items-center gap-2"
|
||||
title="Refresh packages data"
|
||||
>
|
||||
<RefreshCw className={`h-4 w-4 ${isFetching ? 'animate-spin' : ''}`} />
|
||||
{isFetching ? 'Refreshing...' : 'Refresh'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Summary Stats */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-4 gap-4 mb-6 flex-shrink-0">
|
||||
|
||||
@@ -148,8 +148,8 @@ const Repositories = () => {
|
||||
(filterType === 'insecure' && !isSecure);
|
||||
|
||||
const matchesStatus = filterStatus === 'all' ||
|
||||
(filterStatus === 'active' && repo.isActive === true) ||
|
||||
(filterStatus === 'inactive' && repo.isActive === false);
|
||||
(filterStatus === 'active' && repo.is_active === true) ||
|
||||
(filterStatus === 'inactive' && repo.is_active === false);
|
||||
|
||||
console.log('Filter results:', {
|
||||
matchesSearch,
|
||||
@@ -171,8 +171,8 @@ const Repositories = () => {
|
||||
aValue = a.isSecure ? 'Secure' : 'Insecure';
|
||||
bValue = b.isSecure ? 'Secure' : 'Insecure';
|
||||
} else if (sortField === 'status') {
|
||||
aValue = a.isActive ? 'Active' : 'Inactive';
|
||||
bValue = b.isActive ? 'Active' : 'Inactive';
|
||||
aValue = a.is_active ? 'Active' : 'Inactive';
|
||||
bValue = b.is_active ? 'Active' : 'Inactive';
|
||||
}
|
||||
|
||||
if (typeof aValue === 'string') {
|
||||
@@ -426,11 +426,11 @@ const Repositories = () => {
|
||||
case 'status':
|
||||
return (
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
repo.isActive
|
||||
repo.is_active
|
||||
? 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300'
|
||||
: 'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300'
|
||||
}`}>
|
||||
{repo.isActive ? 'Active' : 'Inactive'}
|
||||
{repo.is_active ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
)
|
||||
case 'hostCount':
|
||||
|
||||
@@ -45,7 +45,7 @@ const RepositoryDetail = () => {
|
||||
setFormData({
|
||||
name: repository.name,
|
||||
description: repository.description || '',
|
||||
isActive: repository.isActive,
|
||||
is_active: repository.is_active,
|
||||
priority: repository.priority || ''
|
||||
});
|
||||
setEditMode(true);
|
||||
@@ -139,11 +139,11 @@ const RepositoryDetail = () => {
|
||||
{repository.name}
|
||||
</h1>
|
||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
||||
repository.isActive
|
||||
repository.is_active
|
||||
? 'bg-green-100 dark:bg-green-900/20 text-green-800 dark:text-green-300'
|
||||
: 'bg-red-100 dark:bg-red-900/20 text-red-800 dark:text-red-300'
|
||||
}`}>
|
||||
{repository.isActive ? 'Active' : 'Inactive'}
|
||||
{repository.is_active ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-secondary-500 dark:text-secondary-300 mt-1">
|
||||
@@ -228,12 +228,12 @@ const RepositoryDetail = () => {
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="isActive"
|
||||
checked={formData.isActive}
|
||||
onChange={(e) => setFormData({ ...formData, isActive: e.target.checked })}
|
||||
id="is_active"
|
||||
checked={formData.is_active}
|
||||
onChange={(e) => setFormData({ ...formData, is_active: e.target.checked })}
|
||||
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-secondary-300 rounded"
|
||||
/>
|
||||
<label htmlFor="isActive" className="ml-2 block text-sm text-secondary-900 dark:text-white">
|
||||
<label htmlFor="is_active" className="ml-2 block text-sm text-secondary-900 dark:text-white">
|
||||
Repository is active
|
||||
</label>
|
||||
</div>
|
||||
@@ -295,7 +295,7 @@ const RepositoryDetail = () => {
|
||||
<div className="flex items-center mt-1">
|
||||
<Calendar className="h-4 w-4 text-secondary-400 mr-2" />
|
||||
<span className="text-secondary-900 dark:text-white">
|
||||
{new Date(repository.createdAt).toLocaleDateString()}
|
||||
{new Date(repository.created_at).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -339,7 +339,7 @@ const RepositoryDetail = () => {
|
||||
to={`/hosts/${hostRepo.host.id}`}
|
||||
className="text-primary-600 hover:text-primary-700 font-medium"
|
||||
>
|
||||
{hostRepo.host.friendlyName}
|
||||
{hostRepo.host.friendly_name}
|
||||
</Link>
|
||||
<div className="flex items-center gap-4 text-sm text-secondary-500 dark:text-secondary-400 mt-1">
|
||||
<span>IP: {hostRepo.host.ip}</span>
|
||||
|
||||
@@ -732,7 +732,7 @@ const Settings = () => {
|
||||
</div>
|
||||
)}
|
||||
<p className="text-xs text-secondary-400 dark:text-secondary-400 mt-1">
|
||||
Created: {new Date(version.createdAt).toLocaleDateString()}
|
||||
Created: {new Date(version.created_at).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -140,7 +140,7 @@ const Users = () => {
|
||||
<Shield className="h-3 w-3 mr-1" />
|
||||
{user.role.charAt(0).toUpperCase() + user.role.slice(1).replace('_', ' ')}
|
||||
</span>
|
||||
{user.isActive ? (
|
||||
{user.is_active ? (
|
||||
<CheckCircle className="ml-2 h-4 w-4 text-green-500" />
|
||||
) : (
|
||||
<XCircle className="ml-2 h-4 w-4 text-red-500" />
|
||||
@@ -152,11 +152,11 @@ const Users = () => {
|
||||
</div>
|
||||
<div className="flex items-center mt-1 text-sm text-secondary-500 dark:text-secondary-300">
|
||||
<Calendar className="h-4 w-4 mr-1" />
|
||||
Created: {new Date(user.createdAt).toLocaleDateString()}
|
||||
{user.lastLogin && (
|
||||
Created: {new Date(user.created_at).toLocaleDateString()}
|
||||
{user.last_login && (
|
||||
<>
|
||||
<span className="mx-2">•</span>
|
||||
Last login: {new Date(user.lastLogin).toLocaleDateString()}
|
||||
Last login: {new Date(user.last_login).toLocaleDateString()}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -174,11 +174,11 @@ const Users = () => {
|
||||
onClick={() => handleResetPassword(user)}
|
||||
className="text-blue-400 hover:text-blue-600 dark:text-blue-500 dark:hover:text-blue-300 disabled:text-gray-300 disabled:cursor-not-allowed"
|
||||
title={
|
||||
!user.isActive
|
||||
!user.is_active
|
||||
? "Cannot reset password for inactive user"
|
||||
: "Reset password"
|
||||
}
|
||||
disabled={!user.isActive}
|
||||
disabled={!user.is_active}
|
||||
>
|
||||
<Key className="h-4 w-4" />
|
||||
</button>
|
||||
@@ -394,7 +394,7 @@ const EditUserModal = ({ user, isOpen, onClose, onUserUpdated, roles }) => {
|
||||
username: user?.username || '',
|
||||
email: user?.email || '',
|
||||
role: user?.role || 'user',
|
||||
isActive: user?.isActive ?? true
|
||||
is_active: user?.is_active ?? true
|
||||
})
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
@@ -486,8 +486,8 @@ const EditUserModal = ({ user, isOpen, onClose, onUserUpdated, roles }) => {
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="isActive"
|
||||
checked={formData.isActive}
|
||||
name="is_active"
|
||||
checked={formData.is_active}
|
||||
onChange={handleInputChange}
|
||||
className="h-4 w-4 text-primary-600 focus:ring-primary-500 border-secondary-300 rounded"
|
||||
/>
|
||||
|
||||
@@ -63,8 +63,8 @@ export const adminHostsAPI = {
|
||||
regenerateCredentials: (hostId) => api.post(`/hosts/${hostId}/regenerate-credentials`),
|
||||
updateGroup: (hostId, hostGroupId) => api.put(`/hosts/${hostId}/group`, { hostGroupId }),
|
||||
bulkUpdateGroup: (hostIds, hostGroupId) => api.put('/hosts/bulk/group', { hostIds, hostGroupId }),
|
||||
toggleAutoUpdate: (hostId, autoUpdate) => api.patch(`/hosts/${hostId}/auto-update`, { autoUpdate }),
|
||||
updateFriendlyName: (hostId, friendlyName) => api.patch(`/hosts/${hostId}/friendly-name`, { friendlyName })
|
||||
toggleAutoUpdate: (hostId, autoUpdate) => api.patch(`/hosts/${hostId}/auto-update`, { auto_update: autoUpdate }),
|
||||
updateFriendlyName: (hostId, friendlyName) => api.patch(`/hosts/${hostId}/friendly-name`, { friendly_name: friendlyName })
|
||||
}
|
||||
|
||||
// Host Groups API
|
||||
@@ -156,7 +156,7 @@ export const hostsAPI = {
|
||||
'X-API-KEY': apiKey
|
||||
}
|
||||
}),
|
||||
toggleAutoUpdate: (id, autoUpdate) => api.patch(`/hosts/${id}/auto-update`, { autoUpdate })
|
||||
toggleAutoUpdate: (id, autoUpdate) => api.patch(`/hosts/${id}/auto-update`, { auto_update: autoUpdate })
|
||||
}
|
||||
|
||||
// Packages API
|
||||
|
||||
Reference in New Issue
Block a user