DashboardView.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686
  1. <template>
  2. <div class="dashboard">
  3. <!-- Alert Banner -->
  4. <div v-if="activeAlerts.length > 0" class="alerts-section">
  5. <div
  6. v-for="alert in activeAlerts"
  7. :key="alert.id"
  8. :class="['alert-banner', `alert-${alert.severity}`]"
  9. >
  10. <div class="alert-content">
  11. <div class="alert-icon">{{ alert.severity === 'critical' ? '🔴' : '⚠️' }}</div>
  12. <div class="alert-text">
  13. <div class="alert-title">{{ alert.title }}</div>
  14. <div class="alert-message">{{ alert.message }}</div>
  15. <div class="alert-time">{{ formatTime(alert.created_at) }}</div>
  16. </div>
  17. </div>
  18. <div class="alert-actions">
  19. <button
  20. v-if="!alert.acknowledged"
  21. @click="acknowledgeAlert(alert.id)"
  22. class="btn-acknowledge"
  23. >
  24. Acknowledge
  25. </button>
  26. <button @click="dismissAlert(alert.id)" class="btn-dismiss">
  27. </button>
  28. </div>
  29. </div>
  30. </div>
  31. <!-- Page Header -->
  32. <div class="page-header">
  33. <h1>System Monitoring Dashboard</h1>
  34. <p>Real-time host metrics and device status</p>
  35. </div>
  36. <!-- Device Statistics -->
  37. <div class="stats-grid">
  38. <div class="stat-card">
  39. <div class="stat-icon">📡</div>
  40. <div class="stat-content">
  41. <div class="stat-value">{{ deviceStats.total }}</div>
  42. <div class="stat-label">Total Devices</div>
  43. </div>
  44. </div>
  45. <div class="stat-card stat-online">
  46. <div class="stat-icon">✅</div>
  47. <div class="stat-content">
  48. <div class="stat-value">{{ deviceStats.online }}</div>
  49. <div class="stat-label">Online</div>
  50. </div>
  51. </div>
  52. <div class="stat-card stat-offline">
  53. <div class="stat-icon">⭕</div>
  54. <div class="stat-content">
  55. <div class="stat-value">{{ deviceStats.offline }}</div>
  56. <div class="stat-label">Offline</div>
  57. </div>
  58. </div>
  59. <div class="stat-card stat-error">
  60. <div class="stat-icon">❌</div>
  61. <div class="stat-content">
  62. <div class="stat-value">{{ deviceStats.error }}</div>
  63. <div class="stat-label">Error</div>
  64. </div>
  65. </div>
  66. </div>
  67. <!-- Host Metrics Charts -->
  68. <div class="metrics-section">
  69. <h2>Host Metrics</h2>
  70. <div class="charts-grid">
  71. <!-- CPU Chart -->
  72. <div class="chart-card">
  73. <h3>CPU Usage</h3>
  74. <Line v-if="cpuChartData" :data="cpuChartData" :options="cpuChartOptions" />
  75. <div v-else class="chart-loading">Loading...</div>
  76. </div>
  77. <!-- Memory Chart -->
  78. <div class="chart-card">
  79. <h3>Memory Usage</h3>
  80. <Line v-if="memoryChartData" :data="memoryChartData" :options="memoryChartOptions" />
  81. <div v-else class="chart-loading">Loading...</div>
  82. </div>
  83. <!-- Load Average Chart -->
  84. <div class="chart-card">
  85. <h3>Load Average</h3>
  86. <Line v-if="loadChartData" :data="loadChartData" :options="loadChartOptions" />
  87. <div v-else class="chart-loading">Loading...</div>
  88. </div>
  89. <!-- Disk I/O Chart -->
  90. <div class="chart-card">
  91. <h3>Disk I/O (IOPS)</h3>
  92. <Line v-if="diskChartData" :data="diskChartData" :options="diskChartOptions" />
  93. <div v-else class="chart-loading">Loading...</div>
  94. </div>
  95. <!-- Network Chart -->
  96. <div class="chart-card">
  97. <h3>Network Throughput (MB/s)</h3>
  98. <Line v-if="networkChartData" :data="networkChartData" :options="networkChartOptions" />
  99. <div v-else class="chart-loading">Loading...</div>
  100. </div>
  101. <!-- Disk Usage Chart -->
  102. <div class="chart-card">
  103. <h3>Disk Usage</h3>
  104. <Line v-if="diskUsageChartData" :data="diskUsageChartData" :options="diskUsageChartOptions" />
  105. <div v-else class="chart-loading">Loading...</div>
  106. </div>
  107. </div>
  108. </div>
  109. </div>
  110. </template>
  111. <script setup>
  112. import { ref, onMounted, onUnmounted } from 'vue'
  113. import { Line } from 'vue-chartjs'
  114. import {
  115. Chart as ChartJS,
  116. CategoryScale,
  117. LinearScale,
  118. PointElement,
  119. LineElement,
  120. Title,
  121. Tooltip,
  122. Legend,
  123. Filler
  124. } from 'chart.js'
  125. import axios from '@/api/client'
  126. // Register Chart.js components
  127. ChartJS.register(
  128. CategoryScale,
  129. LinearScale,
  130. PointElement,
  131. LineElement,
  132. Title,
  133. Tooltip,
  134. Legend,
  135. Filler
  136. )
  137. // Data
  138. const activeAlerts = ref([])
  139. const deviceStats = ref({ total: 0, online: 0, offline: 0, error: 0 })
  140. const hostMetrics = ref([])
  141. // Chart data
  142. const cpuChartData = ref(null)
  143. const memoryChartData = ref(null)
  144. const loadChartData = ref(null)
  145. const diskChartData = ref(null)
  146. const networkChartData = ref(null)
  147. const diskUsageChartData = ref(null)
  148. // Chart options with red threshold highlighting
  149. const createChartOptions = (title, yMax = 100, thresholdValue = null, unit = '%') => ({
  150. responsive: true,
  151. maintainAspectRatio: false,
  152. plugins: {
  153. legend: {
  154. display: true,
  155. position: 'top'
  156. },
  157. tooltip: {
  158. mode: 'index',
  159. intersect: false,
  160. callbacks: {
  161. label: (context) => {
  162. return `${context.dataset.label}: ${context.parsed.y.toFixed(2)}${unit}`
  163. }
  164. }
  165. }
  166. },
  167. scales: {
  168. y: {
  169. beginAtZero: true,
  170. max: yMax,
  171. ticks: {
  172. callback: (value) => `${value}${unit}`
  173. }
  174. },
  175. x: {
  176. ticks: {
  177. maxRotation: 45,
  178. minRotation: 45
  179. }
  180. }
  181. },
  182. elements: {
  183. line: {
  184. tension: 0.4
  185. }
  186. }
  187. })
  188. const cpuChartOptions = createChartOptions('CPU Usage', 100, 90, '%')
  189. const memoryChartOptions = createChartOptions('Memory Usage', 100, 90, '%')
  190. const loadChartOptions = createChartOptions('Load Average', null, null, '')
  191. const diskChartOptions = createChartOptions('Disk IOPS', null, null, ' IOPS')
  192. const networkChartOptions = createChartOptions('Network', null, null, ' MB/s')
  193. const diskUsageChartOptions = createChartOptions('Disk Usage', 100, 90, '%')
  194. let pollingInterval = null
  195. // Functions
  196. async function loadAlerts() {
  197. try {
  198. const { data } = await axios.get('/superadmin/monitoring/alerts', {
  199. params: { limit: 10, dismissed: false }
  200. })
  201. activeAlerts.value = data.alerts
  202. } catch (error) {
  203. console.error('Failed to load alerts:', error)
  204. }
  205. }
  206. async function acknowledgeAlert(alertId) {
  207. try {
  208. await axios.post(`/superadmin/monitoring/alerts/${alertId}/acknowledge`)
  209. await loadAlerts()
  210. } catch (error) {
  211. console.error('Failed to acknowledge alert:', error)
  212. }
  213. }
  214. async function dismissAlert(alertId) {
  215. try {
  216. await axios.post(`/superadmin/monitoring/alerts/${alertId}/dismiss`)
  217. activeAlerts.value = activeAlerts.value.filter(a => a.id !== alertId)
  218. } catch (error) {
  219. console.error('Failed to dismiss alert:', error)
  220. }
  221. }
  222. async function loadDeviceStats() {
  223. try {
  224. const { data } = await axios.get('/superadmin/devices')
  225. const devices = data.devices
  226. deviceStats.value = {
  227. total: devices.length,
  228. online: devices.filter(d => d.status === 'online').length,
  229. offline: devices.filter(d => d.status === 'offline').length,
  230. error: devices.filter(d => d.status === 'error').length
  231. }
  232. } catch (error) {
  233. console.error('Failed to load device stats:', error)
  234. }
  235. }
  236. async function loadHostMetrics() {
  237. try {
  238. const { data } = await axios.get('/superadmin/monitoring/host-metrics/recent', {
  239. params: { limit: 30 }
  240. })
  241. hostMetrics.value = data.metrics.reverse() // Oldest first
  242. updateCharts()
  243. } catch (error) {
  244. console.error('Failed to load host metrics:', error)
  245. }
  246. }
  247. function updateCharts() {
  248. if (hostMetrics.value.length === 0) return
  249. const labels = hostMetrics.value.map(m =>
  250. new Date(m.timestamp).toLocaleTimeString('en-US', {
  251. hour: '2-digit',
  252. minute: '2-digit'
  253. })
  254. )
  255. // CPU Chart
  256. cpuChartData.value = {
  257. labels,
  258. datasets: [
  259. {
  260. label: 'CPU %',
  261. data: hostMetrics.value.map(m => m.cpu_percent),
  262. borderColor: 'rgb(99, 102, 241)',
  263. backgroundColor: 'rgba(99, 102, 241, 0.1)',
  264. fill: true,
  265. segment: {
  266. borderColor: ctx => {
  267. const value = ctx.p1.parsed.y
  268. return value >= 90 ? 'rgb(239, 68, 68)' : 'rgb(99, 102, 241)'
  269. }
  270. }
  271. }
  272. ]
  273. }
  274. // Memory Chart
  275. memoryChartData.value = {
  276. labels,
  277. datasets: [
  278. {
  279. label: 'Memory %',
  280. data: hostMetrics.value.map(m => m.memory_percent),
  281. borderColor: 'rgb(34, 197, 94)',
  282. backgroundColor: 'rgba(34, 197, 94, 0.1)',
  283. fill: true,
  284. segment: {
  285. borderColor: ctx => {
  286. const value = ctx.p1.parsed.y
  287. return value >= 90 ? 'rgb(239, 68, 68)' : 'rgb(34, 197, 94)'
  288. }
  289. }
  290. }
  291. ]
  292. }
  293. // Load Average Chart
  294. const cpuCount = hostMetrics.value[0]?.cpu_count || 1
  295. loadChartData.value = {
  296. labels,
  297. datasets: [
  298. {
  299. label: 'Load 1m',
  300. data: hostMetrics.value.map(m => m.load_1),
  301. borderColor: 'rgb(244, 114, 182)',
  302. backgroundColor: 'rgba(244, 114, 182, 0.1)',
  303. fill: false
  304. },
  305. {
  306. label: 'Load 5m',
  307. data: hostMetrics.value.map(m => m.load_5),
  308. borderColor: 'rgb(251, 146, 60)',
  309. backgroundColor: 'rgba(251, 146, 60, 0.1)',
  310. fill: false
  311. },
  312. {
  313. label: 'Load 15m',
  314. data: hostMetrics.value.map(m => m.load_15),
  315. borderColor: 'rgb(234, 179, 8)',
  316. backgroundColor: 'rgba(234, 179, 8, 0.1)',
  317. fill: false
  318. },
  319. {
  320. label: `Threshold (${cpuCount} cores)`,
  321. data: Array(labels.length).fill(cpuCount),
  322. borderColor: 'rgba(239, 68, 68, 0.5)',
  323. borderDash: [5, 5],
  324. fill: false,
  325. pointRadius: 0
  326. }
  327. ]
  328. }
  329. // Disk I/O Chart (IOPS)
  330. diskChartData.value = {
  331. labels,
  332. datasets: [
  333. {
  334. label: 'Read IOPS',
  335. data: hostMetrics.value.map(m => m.disk_read_iops),
  336. borderColor: 'rgb(59, 130, 246)',
  337. backgroundColor: 'rgba(59, 130, 246, 0.1)',
  338. fill: false
  339. },
  340. {
  341. label: 'Write IOPS',
  342. data: hostMetrics.value.map(m => m.disk_write_iops),
  343. borderColor: 'rgb(168, 85, 247)',
  344. backgroundColor: 'rgba(168, 85, 247, 0.1)',
  345. fill: false
  346. }
  347. ]
  348. }
  349. // Network Chart (MB/s)
  350. networkChartData.value = {
  351. labels,
  352. datasets: [
  353. {
  354. label: 'In MB/s',
  355. data: hostMetrics.value.map(m => m.net_in_mbps),
  356. borderColor: 'rgb(14, 165, 233)',
  357. backgroundColor: 'rgba(14, 165, 233, 0.1)',
  358. fill: false
  359. },
  360. {
  361. label: 'Out MB/s',
  362. data: hostMetrics.value.map(m => m.net_out_mbps),
  363. borderColor: 'rgb(245, 158, 11)',
  364. backgroundColor: 'rgba(245, 158, 11, 0.1)',
  365. fill: false
  366. }
  367. ]
  368. }
  369. // Disk Usage Chart
  370. diskUsageChartData.value = {
  371. labels,
  372. datasets: [
  373. {
  374. label: 'Disk Usage %',
  375. data: hostMetrics.value.map(m => m.disk_usage_percent),
  376. borderColor: 'rgb(236, 72, 153)',
  377. backgroundColor: 'rgba(236, 72, 153, 0.1)',
  378. fill: true,
  379. segment: {
  380. borderColor: ctx => {
  381. const value = ctx.p1.parsed.y
  382. return value >= 90 ? 'rgb(239, 68, 68)' : 'rgb(236, 72, 153)'
  383. }
  384. }
  385. }
  386. ]
  387. }
  388. }
  389. function formatTime(timestamp) {
  390. const date = new Date(timestamp)
  391. const now = new Date()
  392. const diffMs = now - date
  393. const diffMins = Math.floor(diffMs / 60000)
  394. if (diffMins < 1) return 'just now'
  395. if (diffMins < 60) return `${diffMins} minutes ago`
  396. const diffHours = Math.floor(diffMins / 60)
  397. if (diffHours < 24) return `${diffHours} hours ago`
  398. return date.toLocaleString()
  399. }
  400. async function refreshData() {
  401. await Promise.all([
  402. loadAlerts(),
  403. loadDeviceStats(),
  404. loadHostMetrics()
  405. ])
  406. }
  407. // Lifecycle
  408. onMounted(async () => {
  409. await refreshData()
  410. // Poll every 30 seconds
  411. pollingInterval = setInterval(refreshData, 30000)
  412. })
  413. onUnmounted(() => {
  414. if (pollingInterval) {
  415. clearInterval(pollingInterval)
  416. }
  417. })
  418. </script>
  419. <style scoped>
  420. .dashboard {
  421. padding: 32px;
  422. max-width: 1600px;
  423. margin: 0 auto;
  424. }
  425. /* Alert Banner */
  426. .alerts-section {
  427. margin-bottom: 24px;
  428. }
  429. .alert-banner {
  430. display: flex;
  431. align-items: center;
  432. justify-content: space-between;
  433. padding: 16px 20px;
  434. border-radius: 8px;
  435. margin-bottom: 12px;
  436. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  437. }
  438. .alert-critical {
  439. background: #fee2e2;
  440. border-left: 4px solid #dc2626;
  441. }
  442. .alert-warning {
  443. background: #fef3c7;
  444. border-left: 4px solid #f59e0b;
  445. }
  446. .alert-info {
  447. background: #dbeafe;
  448. border-left: 4px solid #3b82f6;
  449. }
  450. .alert-content {
  451. display: flex;
  452. align-items: flex-start;
  453. gap: 12px;
  454. flex: 1;
  455. }
  456. .alert-icon {
  457. font-size: 24px;
  458. flex-shrink: 0;
  459. }
  460. .alert-text {
  461. flex: 1;
  462. }
  463. .alert-title {
  464. font-weight: 600;
  465. font-size: 16px;
  466. color: #1a202c;
  467. margin-bottom: 4px;
  468. }
  469. .alert-message {
  470. font-size: 14px;
  471. color: #4a5568;
  472. margin-bottom: 4px;
  473. }
  474. .alert-time {
  475. font-size: 12px;
  476. color: #718096;
  477. }
  478. .alert-actions {
  479. display: flex;
  480. gap: 8px;
  481. align-items: center;
  482. }
  483. .btn-acknowledge {
  484. padding: 8px 16px;
  485. background: white;
  486. border: 1px solid #d1d5db;
  487. border-radius: 6px;
  488. font-size: 14px;
  489. cursor: pointer;
  490. transition: all 0.2s;
  491. }
  492. .btn-acknowledge:hover {
  493. background: #f9fafb;
  494. border-color: #9ca3af;
  495. }
  496. .btn-dismiss {
  497. padding: 8px 12px;
  498. background: transparent;
  499. border: none;
  500. font-size: 18px;
  501. cursor: pointer;
  502. color: #6b7280;
  503. transition: color 0.2s;
  504. }
  505. .btn-dismiss:hover {
  506. color: #1f2937;
  507. }
  508. /* Page Header */
  509. .page-header {
  510. margin-bottom: 32px;
  511. }
  512. .page-header h1 {
  513. font-size: 32px;
  514. font-weight: 700;
  515. color: #1a202c;
  516. margin-bottom: 8px;
  517. }
  518. .page-header p {
  519. color: #718096;
  520. font-size: 16px;
  521. }
  522. /* Device Stats */
  523. .stats-grid {
  524. display: grid;
  525. grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
  526. gap: 16px;
  527. margin-bottom: 32px;
  528. }
  529. .stat-card {
  530. background: white;
  531. border-radius: 8px;
  532. padding: 12px 16px;
  533. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  534. display: flex;
  535. align-items: center;
  536. gap: 12px;
  537. transition: transform 0.2s;
  538. }
  539. .stat-card:hover {
  540. transform: translateY(-2px);
  541. box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  542. }
  543. .stat-online {
  544. border-left: 3px solid #10b981;
  545. }
  546. .stat-offline {
  547. border-left: 3px solid #6b7280;
  548. }
  549. .stat-error {
  550. border-left: 3px solid #ef4444;
  551. }
  552. .stat-icon {
  553. font-size: 24px;
  554. }
  555. .stat-content {
  556. flex: 1;
  557. }
  558. .stat-value {
  559. font-size: 20px;
  560. font-weight: 700;
  561. color: #1a202c;
  562. margin-bottom: 2px;
  563. }
  564. .stat-label {
  565. font-size: 12px;
  566. color: #718096;
  567. font-weight: 500;
  568. }
  569. /* Metrics Section */
  570. .metrics-section {
  571. background: white;
  572. border-radius: 12px;
  573. padding: 24px;
  574. box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
  575. }
  576. .metrics-section h2 {
  577. font-size: 24px;
  578. font-weight: 600;
  579. color: #1a202c;
  580. margin-bottom: 24px;
  581. }
  582. .charts-grid {
  583. display: grid;
  584. grid-template-columns: repeat(auto-fit, minmax(500px, 1fr));
  585. gap: 24px;
  586. }
  587. .chart-card {
  588. background: #f9fafb;
  589. border-radius: 8px;
  590. padding: 20px;
  591. min-height: 300px;
  592. }
  593. .chart-card h3 {
  594. font-size: 16px;
  595. font-weight: 600;
  596. color: #374151;
  597. margin-bottom: 16px;
  598. }
  599. .chart-card canvas {
  600. height: 250px !important;
  601. }
  602. .chart-loading {
  603. display: flex;
  604. align-items: center;
  605. justify-content: center;
  606. height: 250px;
  607. color: #9ca3af;
  608. font-size: 14px;
  609. }
  610. </style>