EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
Monitoring.jsx
Go to the documentation of this file.
1import { useState, useEffect } from 'react';
2import { apiFetch } from '../lib/api';
3import DownloadAgentModal from "../components/DownloadAgentModal";
4import AssignDeviceModal from "../components/AssignDeviceModal";
5
6import { useNavigate, Link } from 'react-router-dom';
7import MainLayout from '../components/Layout/MainLayout';
8// import removed: TenantSelector
9import '../styles/data-table.css';
10import './Tickets.css';
11import './Monitoring.css';
12
13function Monitoring() {
14 // All useState hooks should be at the top for proper initialization
15 const navigate = useNavigate();
16 const [agents, setAgents] = useState([]);
17 const [loading, setLoading] = useState(false);
18 const [error, setError] = useState('');
19 const [page, setPage] = useState(1);
20 const [totalPages, setTotalPages] = useState(1);
21 const [search, setSearch] = useState('');
22 const [showFilters, setShowFilters] = useState(false);
23 const tenantsMap = {};
24 const [showDownload, setShowDownload] = useState(false);
25 const [showAssignModal, setShowAssignModal] = useState(false);
26 const [selectedDeviceToAssign, setSelectedDeviceToAssign] = useState(null);
27 const [isMSP, setIsMSP] = useState(false);
28 // Declare token before using it
29 const token = localStorage.getItem('token');
30 let tenantId = '';
31 if (token) {
32 try {
33 const payload = token.split('.')[1];
34 const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
35 tenantId = decoded.tenant_id || decoded.tenantId || '';
36 } catch {}
37 }
38
39 // Check if user is MSP/admin
40 useEffect(() => {
41 if (token) {
42 try {
43 const payload = token.split('.')[1];
44 const decoded = JSON.parse(atob(payload.replace(/-/g, '+').replace(/_/g, '/')));
45 const userRole = decoded.role || '';
46 const userTenantId = decoded.tenant_id || decoded.tenantId || '';
47 setIsMSP(userRole === 'admin' || userRole === 'msp' || userTenantId === '00000000-0000-0000-0000-000000000001');
48 } catch {}
49 }
50 }, [token]);
51 const [monitoringStats, setMonitoringStats] = useState({
52 online: 0,
53 offline: 0,
54 total: 0
55 });
56 // Ensure filters state is defined and used consistently
57 const [filters, setFilters] = useState({
58 type: 'all',
59 status: 'all'
60 });
61 const perPage = 20;
62 // ...existing code...
63 // Reload monitoring data when tenantId changes
64 useEffect(() => {
65 fetchMonitoringData();
66 }, [tenantId]);
67 // Unified fetch for monitoring stats and agents
68 const fetchMonitoringData = async () => {
69 setLoading(true);
70 try {
71 const params = new URLSearchParams();
72 params.set('page', page);
73 params.set('limit', perPage);
74 if (search) params.set('search', search);
75 // Do not pass tenantId or customerId; backend will determine context from JWT
76 const res = await apiFetch(`/monitoring?${params.toString()}`);
77 if (!res.ok) throw new Error('Failed to load monitoring data');
78 const data = await res.json();
79 // Set stats
80 setMonitoringStats({
81 online: data.overview?.agentsOnline || 0,
82 total: data.overview?.totalAgents || 0,
83 offline: (data.overview?.totalAgents || 0) - (data.overview?.agentsOnline || 0)
84 });
85 // Set agents
86 setAgents(data.agents || []);
87 setTotalPages(Math.ceil((data.total || (data.agents?.length || 0)) / perPage));
88 setError('');
89 console.log('[Monitoring] monitoring data fetched:', data);
90 } catch (err) {
91 console.error('[Monitoring] Failed to fetch monitoring data:', err);
92 setMonitoringStats({ online: 0, offline: 0, total: 0 });
93 setAgents([]);
94 setError(err.message);
95 } finally {
96 setLoading(false);
97 }
98 };
99
100 const handleAssignDevice = (device) => {
101 setSelectedDeviceToAssign(device);
102 setShowAssignModal(true);
103 };
104
105 const handleDeviceAssigned = (result) => {
106 console.log('Device assigned successfully:', result);
107 // Refresh the monitoring data to show updated assignment
108 fetchMonitoringData();
109 setShowAssignModal(false);
110 setSelectedDeviceToAssign(null);
111 };
112
113
114 // ...existing code...
115
116 // ...no localStorage for filters...
117
118 // Root tenant control logic removed; handled in MainLayout
119
120
121 useEffect(() => {
122 fetchMonitoringData();
123 // Refresh every 30 seconds
124 const interval = setInterval(() => {
125 fetchMonitoringData();
126 }, 30000);
127 // Refresh when page becomes visible (user returns to tab)
128 const handleVisibilityChange = () => {
129 if (!document.hidden) {
130 fetchMonitoringData();
131 }
132 };
133 document.addEventListener('visibilitychange', handleVisibilityChange);
134 return () => {
135 clearInterval(interval);
136 document.removeEventListener('visibilitychange', handleVisibilityChange);
137 };
138 }, [page, search, tenantId]);
139
140 const getStatusClass = (agent) => {
141 // Use meshcentral_connected field from database for accurate real-time status
142 if (agent?.meshcentral_connected === true) return 'status-online';
143 return 'status-offline';
144 };
145
146 const getStatusText = (agent) => {
147 // Use meshcentral_connected field from database for accurate real-time status
148 if (agent?.meshcentral_connected === true) return 'Online';
149 return 'Offline';
150 };
151
152 const formatLastSeen = (lastSeen) => {
153 if (!lastSeen) return 'Never';
154 const date = new Date(lastSeen);
155 const now = new Date();
156 const diffMs = now - date;
157 const diffMinutes = Math.floor(diffMs / 1000 / 60);
158 const diffHours = Math.floor(diffMinutes / 60);
159 const diffDays = Math.floor(diffHours / 24);
160
161 if (diffMinutes < 1) return 'Just now';
162 if (diffMinutes < 60) return `${diffMinutes}m ago`;
163 if (diffHours < 24) return `${diffHours}h ago`;
164 if (diffDays < 7) return `${diffDays}d ago`;
165 return date.toLocaleDateString();
166 };
167
168 const getDeviceIcon = (os) => {
169 if (!os) return 'computer';
170 const osLower = os.toLowerCase();
171 if (osLower.includes('windows')) return 'desktop_windows';
172 if (osLower.includes('linux')) return 'dns';
173 if (osLower.includes('mac')) return 'laptop_mac';
174 return 'computer';
175 };
176
177 const handleFilterChange = (key, value) => {
178 setFilters(prev => ({ ...prev, [key]: value }));
179 setPage(1);
180 };
181
182 const handleClearFilters = () => {
183 const defaultFilters = { type: 'all', status: 'all' };
184 setFilters(defaultFilters);
185 setSearch('');
186 setPage(1);
187 };
188
189 const hasActiveFilters = () => {
190 return filters.type !== 'all' || filters.status !== 'all' || search;
191 };
192
193 // Apply client-side filtering
194 const filteredAgents = agents.filter(agent => {
195 // Exclude default/stale agents
196 if (
197 !agent.agent_uuid ||
198 agent.agent_uuid === '00000000-0000-0000-0000-000000000001'
199 ) return false;
200
201 // Allow untagged devices for MSP users
202 if (agent.is_untagged && isMSP) {
203 return true; // Skip other filters for untagged devices
204 }
205
206 // For non-untagged devices, require enrolled status
207 if (agent.status !== 'enrolled') {
208 return false;
209 }
210
211 // Type filter
212 if (filters.type !== 'all') {
213 const os = (agent.os || '').toLowerCase();
214 if (filters.type === 'windows' && !os.includes('windows')) return false;
215 if (filters.type === 'linux' && !os.includes('linux')) return false;
216 if (filters.type === 'mac' && !os.includes('mac')) return false;
217 }
218
219 // Status filter
220 if (filters.status !== 'all') {
221 const statusClass = getStatusClass(agent);
222 if (filters.status === 'online' && statusClass !== 'status-online') return false;
223 if (filters.status === 'offline' && statusClass !== 'status-offline') return false;
224 }
225
226 return true;
227 });
228
229 const onlineCount = agents.filter(a => getStatusClass(a) === 'status-online').length;
230
231 // Determine if any agent has tenant_id property
232 const showTenantColumn = filteredAgents.some(agent => agent.tenant_id !== undefined && agent.tenant_id !== null);
233
234 return (
235 <MainLayout>
236 <div className="page-content">
237 <div className="page-header">
238 <h2 style={{margin:0}}>Monitoring</h2>
239 <div className="monitoring-stats">
240 <div className="stat-item">
241 <span className="stat-icon online">
242 <span className="material-symbols-outlined">check_circle</span>
243 </span>
244 <div className="stat-details">
245 <div className="stat-value">{monitoringStats.online}</div>
246 <div className="stat-label">Online</div>
247 </div>
248 </div>
249 <div className="stat-item">
250 <span className="stat-icon offline">
251 <span className="material-symbols-outlined">cancel</span>
252 </span>
253 <div className="stat-details">
254 <div className="stat-value">{monitoringStats.offline}</div>
255 <div className="stat-label">Offline</div>
256 </div>
257 </div>
258 <div className="stat-item">
259 <span className="stat-icon total">
260 <span className="material-symbols-outlined">devices</span>
261 </span>
262 <div className="stat-details">
263 <div className="stat-value">{monitoringStats.total}</div>
264 <div className="stat-label">Total Agents</div>
265 </div>
266 </div>
267 </div>
268 </div>
269
270 <button
271 className="btn primary"
272 onClick={() => setShowDownload(true)}
273 title="Deploy Agent or SNMP Device"
274 style={{ display: 'inline-flex', alignItems: 'center', gap: 4 }}
275 >
276 <span className="material-symbols-outlined" style={{ fontSize: '18px' }}>
277 download
278 </span>
279 Deploy Agent / SNMP
280 </button>
281
282 <div className="toolbar">
283 <div className="toolbar-left">
284 <div className="search-box">
285 <span className="material-symbols-outlined">search</span>
286 <input
287 placeholder="Search by name, hostname, customer, or OS..."
288 value={search}
289 onChange={e => { setSearch(e.target.value); setPage(1); }}
290 />
291 </div>
292 </div>
293 <div className="toolbar-right">
294 <button className="btn" onClick={fetchMonitoringData}>
295 <span className="material-symbols-outlined">refresh</span>
296 Refresh
297 </button>
298 <button
299 className={`btn ${showFilters ? 'active' : ''}`}
300 onClick={() => setShowFilters(!showFilters)}
301 title="Toggle filters"
302 >
303 <span className="material-symbols-outlined">tune</span>
304 Filters
305 {hasActiveFilters() && (
306 <span className="badge">{Object.values(filters).filter(v => v && v !== 'all').length + (search ? 1 : 0)}</span>
307 )}
308 </button>
309 {hasActiveFilters() && (
310 <button className="btn" onClick={handleClearFilters}>
311 <span className="material-symbols-outlined">clear_all</span>
312 Clear All
313 </button>
314 )}
315 </div>
316 </div>
317
318 {showFilters && (
319 <div className="filter-panel">
320 <h3>Filter Options</h3>
321 <div className="filter-grid">
322 <div className="filter-group">
323 <label>OS Type</label>
324 <select
325 value={filters.type}
326 onChange={e => handleFilterChange('type', e.target.value)}
327 >
328 <option value="all">All Types</option>
329 <option value="windows">Windows</option>
330 <option value="linux">Linux</option>
331 <option value="mac">macOS</option>
332 </select>
333 </div>
334
335 <div className="filter-group">
336 <label>Status</label>
337 <select
338 value={filters.status}
339 onChange={e => handleFilterChange('status', e.target.value)}
340 >
341 <option value="all">All Statuses</option>
342 <option value="online">Online</option>
343 <option value="offline">Offline</option>
344 </select>
345 </div>
346 </div>
347
348 {hasActiveFilters() && (
349 <div className="active-filters">
350 <div className="active-filters-header">
351 <span className="filter-label">Active Filters:</span>
352 </div>
353 <div className="filter-tags">
354 {filters.type !== 'all' && (
355 <span className="filter-tag">
356 Type: {filters.type}
357 <button onClick={(e) => { e.stopPropagation(); handleFilterChange('type', 'all'); }}>×</button>
358 </span>
359 )}
360 {filters.status !== 'all' && (
361 <span className="filter-tag">
362 Status: {filters.status}
363 <button onClick={(e) => { e.stopPropagation(); handleFilterChange('status', 'all'); }}>×</button>
364 </span>
365 )}
366 {search && (
367 <span className="filter-tag">
368 Search: "{search}"
369 <button onClick={(e) => { e.stopPropagation(); setSearch(''); }}>×</button>
370 </span>
371 )}
372 </div>
373 </div>
374 )}
375 </div>
376 )}
377
378 {error && <div className="error-message">{error}</div>}
379
380 {loading ? (
381 <div className="loading">Loading agents...</div>
382 ) : (
383 <>
384 <div className="table-container">
385 <table className="data-table">
386 <thead>
387 <tr>
388 <th>Status</th>
389 <th>Name</th>
390 {showTenantColumn && <th>Tenant</th>}
391 <th>Customer</th>
392 <th>OS</th>
393 <th>Version</th>
394 <th>Contract</th>
395 <th>Last Seen</th>
396 {isMSP && <th>Actions</th>}
397 </tr>
398 </thead>
399 <tbody>
400 {filteredAgents.length === 0 ? (
401 <tr>
402 <td colSpan={isMSP ? (showTenantColumn ? "9" : "8") : (showTenantColumn ? "8" : "7")} style={{ textAlign: 'center', padding: '2rem', color: 'var(--text-secondary)' }}>
403 No agents found
404 </td>
405 </tr>
406 ) : (
407 filteredAgents.map((agent) => (
408 <tr key={agent.agent_uuid || agent.id}>
409 <td>
410 <span
411 className={`material-symbols-outlined device-icon ${getStatusClass(agent)}`}
412 title={getStatusClass(agent) === 'status-online' ? 'Online (connected)' : 'Offline'}
413 style={{ color: getStatusClass(agent) === 'status-online' ? '#2ecc40' : undefined }}
414 >
415 {getDeviceIcon(agent.os)}
416 </span>
417 </td>
418 <td>
419 <div className="agent-name-cell">
420 <div className="agent-name">
421 <Link to={`/agents/${agent.agent_uuid}`}>
422 {agent.name || agent.hostname || 'Unnamed'}
423 </Link>
424 </div>
425 <div className="agent-hostname">{agent.hostname}</div>
426 </div>
427 </td>
428
429 {showTenantColumn && (
430 <td title={agent.tenant_id ? `Tenant #${agent.tenant_id}` : 'Not assigned'}>
431 {agent.is_untagged ? (
432 <span className="status-badge inactive" style={{ background: '#ff9800', color: 'white' }}>
433 Untagged
434 </span>
435 ) : agent.tenant_id === 1 ? (
436 'Root Tenant'
437 ) : (
438 agent.tenant_name || tenantsMap?.[agent.tenant_id] || (agent.tenant_id ? `#${agent.tenant_id}` : '—')
439 )}
440 </td>
441 )}
442
443 <td>
444 <span className="customer-name">
445 {agent.customer_name || (agent.customer_id ? `#${agent.customer_id}` : 'No Customer')}
446 </span>
447 </td>
448
449 <td><span className="os-badge">{agent.os || '—'}</span></td>
450
451 <td><span className="os-version">{agent.os_version || '—'}</span></td>
452
453 <td>
454 {agent.contract_title ? (
455 <span className="status-badge active">{agent.contract_title}</span>
456 ) : (
457 <span className="status-badge inactive">No Contract</span>
458 )}
459 </td>
460
461 <td>
462 <div className="last-seen-cell">
463 {agent.meshcentral_connected ? (
464 <span className="status-text status-online">
465 Online
466 </span>
467 ) : (
468 <span className="time-ago">
469 {formatLastSeen(agent.last_seen)}
470 </span>
471 )}
472 </div>
473 </td>
474
475 {isMSP && (
476 <td>
477 {agent.is_untagged ? (
478 <button
479 className="btn btn-sm primary"
480 onClick={() => handleAssignDevice(agent)}
481 title="Assign this device to a tenant"
482 style={{ fontSize: '0.875rem', padding: '0.25rem 0.75rem' }}
483 >
484 <span className="material-symbols-outlined" style={{ fontSize: '16px', marginRight: '4px' }}>
485 label
486 </span>
487 Assign
488 </button>
489 ) : (
490 <span style={{ color: 'var(--text-secondary)', fontSize: '0.875rem' }}>—</span>
491 )}
492 </td>
493 )}
494 </tr>
495 ))
496 )}
497 </tbody>
498 </table>
499 </div>
500 <div className="pagination">
501 <button className="btn" disabled={page === 1} onClick={() => setPage(p => Math.max(1, p - 1))}>
502 <span className="material-symbols-outlined">navigate_before</span>
503 </button>
504
505 <span>Page {page} of {totalPages}</span>
506
507 <button className="btn" disabled={page === totalPages} onClick={() => setPage(p => Math.min(totalPages, p + 1))}>
508 <span className="material-symbols-outlined">navigate_next</span>
509 </button>
510 </div>
511 </>
512 )}
513 {showDownload && (
514 <DownloadAgentModal
515 isOpen={showDownload}
516 onClose={() => setShowDownload(false)}
517 tenantId={tenantId}
518 customerId={null}
519 />
520 )}
521 {showAssignModal && selectedDeviceToAssign && (
522 <AssignDeviceModal
523 isOpen={showAssignModal}
524 onClose={() => {
525 setShowAssignModal(false);
526 setSelectedDeviceToAssign(null);
527 }}
528 device={selectedDeviceToAssign}
529 onAssigned={handleDeviceAssigned}
530 />
531 )}
532 </div>
533 </MainLayout>
534 );
535}
536
537export default Monitoring;