main
vue 291 lines 9.21 KB
Raw
1 <template>
2 <div class="metrics-dashboard">
3 <!-- Filters -->
4 <div class="header mb-6 flex flex-wrap items-end gap-4">
5 <div>
6 <label class="mb-1 block text-xs" :style="{ color: 'var(--fg-secondary-color)' }">Host</label>
7 <n-select
8 v-model:value="selectedHost"
9 :options="hostOptions"
10 :loading="hostsLoading"
11 placeholder="Select host"
12 class="w-52!"
13 size="small"
14 @update:value="refresh"
15 />
16 </div>
17 <div>
18 <label class="mb-1 block text-xs" :style="{ color: 'var(--fg-secondary-color)' }">Time Range</label>
19 <n-select
20 v-model:value="selectedRange"
21 :options="rangeOptions"
22 class="w-44!"
23 size="small"
24 @update:value="refresh"
25 />
26 </div>
27 <n-button size="small" type="primary" secondary :loading :disabled="!selectedHost" @click="refresh">
28 <template #icon>
29 <Icon :name="RefreshIcon" :size="15" />
30 </template>
31 </n-button>
32 </div>
33
34 <!-- Empty state -->
35 <n-card v-if="!selectedHost" class="p-12 text-center">
36 <n-empty description="Select a host to view performance metrics">
37 <template #icon>
38 <Icon :name="ChartIcon" :size="40" />
39 </template>
40 </n-empty>
41 </n-card>
42
43 <!-- Loading -->
44 <n-spin v-else-if="loading && !loaded" class="flex min-h-80 items-center justify-center" show />
45
46 <!-- Dashboard -->
47 <div v-else-if="loaded">
48 <!-- Summary Section -->
49 <section class="mb-8">
50 <h3 class="mb-4 text-lg font-semibold" :style="{ color: 'var(--primary-color)' }">Metrics Summary</h3>
51 <div class="mb-4 grid grid-cols-2 gap-4 md:grid-cols-4 lg:grid-cols-7">
52 <MetricsStatCard label="Uptime" :value="summary.uptime" format="uptime" />
53 <MetricsStatCard label="Total Memory" :value="summary.total_mem" format="bytes" />
54 <MetricsStatCard label="CPUs" :value="summary.cpus" />
55 <MetricsStatCard label="Processes" :value="summary.total_processes" />
56 <MetricsStatCard
57 label="CPU Idle"
58 :value="summary.cpu_idle"
59 format="percent"
60 :decimals="1"
61 :color-thresholds="{ low: 20, mid: 50 }"
62 />
63 <MetricsStatCard label="Swap Free" :value="summary.swap_free" format="bytes" />
64 <MetricsStatCard label="Users Logged In" :value="summary.logged_on_users" />
65 </div>
66 <div class="grid grid-cols-1 gap-4 lg:grid-cols-3">
67 <n-card>
68 <MetricsGauge :value="summary.cpu_idle ?? 0" title="CPU Idle" :height="220" />
69 </n-card>
70 <n-card class="lg:col-span-2">
71 <MetricsChart title="System Load" :series="summary.load || {}" :height="220" />
72 </n-card>
73 </div>
74 </section>
75
76 <!-- CPU Section -->
77 <section class="mb-8">
78 <h3 class="mb-4 text-lg font-semibold" :style="{ color: 'var(--primary-color)' }">CPU</h3>
79 <div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
80 <n-card>
81 <MetricsChart title="CPU System %" :series="cpu.cpu_usage_system || {}" y-axis-name="%" />
82 </n-card>
83 <n-card>
84 <MetricsChart title="CPU User %" :series="cpu.cpu_usage_user || {}" y-axis-name="%" />
85 </n-card>
86 <n-card>
87 <MetricsChart title="I/O Wait %" :series="cpu.cpu_iowait || {}" y-axis-name="%" />
88 </n-card>
89 <n-card>
90 <MetricsChart title="Soft IRQ %" :series="cpu.cpu_softirq || {}" y-axis-name="%" />
91 </n-card>
92 </div>
93 </section>
94
95 <!-- Memory Section -->
96 <section class="mb-8">
97 <h3 class="mb-4 text-lg font-semibold" :style="{ color: 'var(--primary-color)' }">Memory</h3>
98 <div class="mb-4 grid grid-cols-2 gap-4">
99 <MetricsStatCard label="Swap Total" :value="memory.swap_total" format="bytes" />
100 <MetricsStatCard label="Swap Free" :value="memory.swap_free" format="bytes" />
101 </div>
102 <n-card>
103 <MetricsChart
104 title="Memory Usage"
105 :series="memory.mem_used || {}"
106 :height="280"
107 y-axis-name="bytes"
108 use-format-bytes
109 />
110 </n-card>
111 </section>
112
113 <!-- Kernel Section -->
114 <section class="mb-8">
115 <h3 class="mb-4 text-lg font-semibold" :style="{ color: 'var(--primary-color)' }">Kernel</h3>
116 <div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
117 <n-card>
118 <MetricsChart title="Interrupts/sec" :series="kernel.interrupts || {}" y-axis-name="/s" />
119 </n-card>
120 <n-card>
121 <MetricsChart
122 title="Processes Forked/sec"
123 :series="kernel.processes_forked || {}"
124 y-axis-name="/s"
125 />
126 </n-card>
127 </div>
128 </section>
129
130 <!-- Disks Section -->
131 <section class="mb-8">
132 <h3 class="mb-4 text-lg font-semibold" :style="{ color: 'var(--primary-color)' }">Disks</h3>
133 <div class="mb-4">
134 <MetricsStatCard label="Total Disk Size" :value="disks.disk_total" format="bytes" />
135 </div>
136 <div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
137 <n-card>
138 <MetricsChart title="Disk Usage %" :series="disks.disk_usage || {}" y-axis-name="%" />
139 </n-card>
140 <n-card>
141 <MetricsChart
142 title="Disk I/O (bytes/s)"
143 :series="disks.disk_io || {}"
144 y-axis-name="B/s"
145 use-format-bytes
146 />
147 </n-card>
148 </div>
149 </section>
150
151 <!-- Processes Section -->
152 <section class="mb-8">
153 <h3 class="mb-4 text-lg font-semibold" :style="{ color: 'var(--primary-color)' }">Processes</h3>
154 <div class="mb-4 grid grid-cols-2 gap-4 md:grid-cols-4">
155 <MetricsStatCard label="Running" :value="processes.running" />
156 <MetricsStatCard label="Sleeping" :value="processes.sleeping" />
157 <MetricsStatCard label="Unknown" :value="processes.unknown" />
158 <MetricsStatCard label="Zombies" :value="processes.zombies" />
159 </div>
160 <n-card>
161 <MetricsChart title="Process Status" :series="processes.status || {}" :height="280" />
162 </n-card>
163 </section>
164
165 <!-- Network Section -->
166 <section class="mb-8">
167 <h3 class="mb-4 text-lg font-semibold" :style="{ color: 'var(--primary-color)' }">Network</h3>
168 <div class="mb-4">
169 <MetricsStatCard label="TCP Sessions Established" :value="network.tcp_established" />
170 </div>
171 <div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
172 <n-card>
173 <MetricsChart
174 title="Network Traffic (bytes/s)"
175 :series="network.traffic || {}"
176 :height="280"
177 y-axis-name="B/s"
178 use-format-bytes
179 />
180 </n-card>
181 <n-card>
182 <MetricsChart title="Interface Errors" :series="network.interface_errors || {}" :height="280" />
183 </n-card>
184 </div>
185 </section>
186 </div>
187 </div>
188 </template>
189
190 <script setup lang="ts">
191 import { NButton, NCard, NEmpty, NSelect, NSpin, useMessage } from "naive-ui"
192 import { computed, onBeforeMount, ref } from "vue"
193 import Api from "@/api"
194 import Icon from "@/components/common/Icon.vue"
195 import MetricsChart from "./MetricsChart.vue"
196 import MetricsGauge from "./MetricsGauge.vue"
197 import MetricsStatCard from "./MetricsStatCard.vue"
198
199 const RefreshIcon = "carbon:update-now"
200 const ChartIcon = "carbon:chart-line"
201
202 const message = useMessage()
203 const loading = ref(false)
204 const loaded = ref(false)
205 const hostsLoading = ref(false)
206 const hosts = ref<string[]>([])
207 const selectedHost = ref<string | null>(null)
208 const selectedRange = ref("1")
209
210 const summary = ref<Record<string, any>>({})
211 const cpu = ref<Record<string, any>>({})
212 const memory = ref<Record<string, any>>({})
213 const kernel = ref<Record<string, any>>({})
214 const disks = ref<Record<string, any>>({})
215 const processes = ref<Record<string, any>>({})
216 const network = ref<Record<string, any>>({})
217
218 const hostOptions = computed(() => hosts.value.map(h => ({ label: h, value: h })))
219
220 const rangeOptions = [
221 { label: "Last 1 Hour", value: "1" },
222 { label: "Last 3 Hours", value: "3" },
223 { label: "Last 6 Hours", value: "6" },
224 { label: "Last 12 Hours", value: "12" },
225 { label: "Last 24 Hours", value: "24" },
226 { label: "Last 2 Days", value: "48" },
227 { label: "Last 7 Days", value: "168" }
228 ]
229
230 async function loadHosts() {
231 hostsLoading.value = true
232 try {
233 const res = await Api.metrics.getHosts()
234 if (res.data.success) {
235 hosts.value = res.data.hosts || []
236 } else {
237 message.warning(res.data?.message || "Failed to load hosts")
238 }
239 } catch (err: any) {
240 message.error(err.response?.data?.message || "Failed to load hosts")
241 } finally {
242 hostsLoading.value = false
243 }
244 }
245
246 async function refresh() {
247 if (!selectedHost.value) return
248 loading.value = true
249
250 const host = selectedHost.value
251 const range = selectedRange.value
252
253 try {
254 const [sumRes, cpuRes, memRes, kerRes, dskRes, prcRes, netRes] = await Promise.all([
255 Api.metrics.getSummary(host, range),
256 Api.metrics.getCpu(host, range),
257 Api.metrics.getMemory(host, range),
258 Api.metrics.getKernel(host, range),
259 Api.metrics.getDisks(host, range),
260 Api.metrics.getProcesses(host, range),
261 Api.metrics.getNetwork(host, range)
262 ])
263
264 for (const res of [sumRes, cpuRes, memRes, kerRes, dskRes, prcRes, netRes]) {
265 if (!res.data.success) {
266 message.warning(res.data?.message || "Error fetching metrics")
267 loading.value = false
268 return
269 }
270 }
271
272 summary.value = sumRes.data.data || {}
273 cpu.value = cpuRes.data.data || {}
274 memory.value = memRes.data.data || {}
275 kernel.value = kerRes.data.data || {}
276 disks.value = dskRes.data.data || {}
277 processes.value = prcRes.data.data || {}
278 network.value = netRes.data.data || {}
279
280 loaded.value = true
281 } catch (err: any) {
282 message.error(err.response?.data?.message || "Error fetching metrics")
283 } finally {
284 loading.value = false
285 }
286 }
287
288 onBeforeMount(() => {
289 loadHosts()
290 })
291 </script>