feat: add system metrics dashboard
This commit is contained in:
199
components/SystemResources.vue
Normal file
199
components/SystemResources.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<div class="resources-card card-glow">
|
||||
<div class="card-header">
|
||||
<h2 class="card-title">Ressources</h2>
|
||||
<span class="font-mono text-[10px] uppercase tracking-widest text-m-muted">CPU / RAM</span>
|
||||
</div>
|
||||
|
||||
<div class="metrics-list">
|
||||
<div
|
||||
v-for="metric in metrics"
|
||||
:key="metric.label"
|
||||
class="metric-row"
|
||||
>
|
||||
<div class="metric-copy">
|
||||
<div class="metric-head">
|
||||
<span class="font-display text-sm font-semibold text-m-text">{{ metric.label }}</span>
|
||||
<span class="font-mono text-xs text-m-muted">{{ metric.detail }}</span>
|
||||
</div>
|
||||
|
||||
<template v-if="isLoading">
|
||||
<div class="metric-skeleton animate-shimmer" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="metric-bar">
|
||||
<div
|
||||
class="metric-bar-fill"
|
||||
:class="metric.toneClass"
|
||||
:style="{ width: `${metric.percent}%` }"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="metric-value-area">
|
||||
<template v-if="isLoading">
|
||||
<div class="value-skeleton animate-shimmer" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<span class="metric-value font-mono">{{ metric.percent }}%</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed} from "vue"
|
||||
|
||||
type SystemMetrics = {
|
||||
cpuPercent: number
|
||||
memoryPercent: number
|
||||
totalMemory: number
|
||||
usedMemory: number
|
||||
incomingMbps: number
|
||||
outgoingMbps: number
|
||||
sampledAt: string
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
metrics: SystemMetrics | null
|
||||
loading: boolean
|
||||
}>()
|
||||
|
||||
const formatMemory = (value: number) => {
|
||||
if (!Number.isFinite(value) || value <= 0) {
|
||||
return "0 GB"
|
||||
}
|
||||
|
||||
return `${(value / 1024 / 1024 / 1024).toFixed(1)} GB`
|
||||
}
|
||||
|
||||
const toneClass = (percent: number) => {
|
||||
if (percent >= 85) return "tone-error"
|
||||
if (percent >= 65) return "tone-warning"
|
||||
return "tone-success"
|
||||
}
|
||||
|
||||
const isLoading = computed(() => props.loading || !props.metrics)
|
||||
|
||||
const metrics = computed(() => [
|
||||
{
|
||||
label: "CPU",
|
||||
percent: props.metrics?.cpuPercent ?? 0,
|
||||
detail: "Charge instantanee",
|
||||
toneClass: toneClass(props.metrics?.cpuPercent ?? 0)
|
||||
},
|
||||
{
|
||||
label: "RAM",
|
||||
percent: props.metrics?.memoryPercent ?? 0,
|
||||
detail: `${formatMemory(props.metrics?.usedMemory ?? 0)} / ${formatMemory(props.metrics?.totalMemory ?? 0)}`,
|
||||
toneClass: toneClass(props.metrics?.memoryPercent ?? 0)
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.resources-card {
|
||||
background: rgb(var(--m-secondary));
|
||||
border-radius: 12px;
|
||||
padding: 1.25rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
transition: background-color 0.4s ease;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: rgb(var(--m-text));
|
||||
}
|
||||
|
||||
.metrics-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.875rem;
|
||||
}
|
||||
|
||||
.metric-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
padding: 0.875rem 1rem;
|
||||
border-radius: 10px;
|
||||
background: rgb(var(--m-tertiary));
|
||||
border: 1px solid rgb(var(--m-accent) / 0.06);
|
||||
}
|
||||
|
||||
.metric-copy {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.metric-head {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 0.625rem;
|
||||
}
|
||||
|
||||
.metric-bar {
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
overflow: hidden;
|
||||
background: rgb(var(--m-bg) / 0.45);
|
||||
}
|
||||
|
||||
.metric-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: inherit;
|
||||
transition: width 0.35s ease, background-color 0.35s ease;
|
||||
}
|
||||
|
||||
.metric-value-area {
|
||||
min-width: 54px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.metric-value {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 700;
|
||||
color: rgb(var(--m-text));
|
||||
}
|
||||
|
||||
.metric-skeleton {
|
||||
height: 10px;
|
||||
width: 100%;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.value-skeleton {
|
||||
width: 48px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.tone-success {
|
||||
background: rgb(var(--m-success));
|
||||
}
|
||||
|
||||
.tone-warning {
|
||||
background: rgb(var(--m-warning));
|
||||
}
|
||||
|
||||
.tone-error {
|
||||
background: rgb(var(--m-error));
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user