feat : new ui et message discord
This commit is contained in:
284
pages/index.vue
284
pages/index.vue
@@ -1,35 +1,56 @@
|
||||
<template>
|
||||
<NuxtLayout name="default">
|
||||
<template #sidebar>
|
||||
<div class="flex flex-col">
|
||||
<DiagramStorage
|
||||
v-for="item in diagramItems"
|
||||
:key="item.key"
|
||||
:loading="loading"
|
||||
:host-name="item.hostName"
|
||||
:status-color-class="item.statusColorClass"
|
||||
:chart-radius="chartRadius"
|
||||
:chart-circumference="chartCircumference"
|
||||
:chart-offset="item.chartOffset"
|
||||
:remaining-percent-text="item.remainingPercentText"
|
||||
:used-text="item.usedText"
|
||||
:total-text="item.totalText"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div class="dashboard-container">
|
||||
<header class="dashboard-header">
|
||||
<div>
|
||||
<h1 class="font-display text-3xl font-bold tracking-tight text-m-text">
|
||||
Monitoring
|
||||
</h1>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<p class="font-bold text-4xl my-6 mx-4">Écran de monitoring</p>
|
||||
<div class="content-grid">
|
||||
<div class="content-main">
|
||||
<section class="storage-section">
|
||||
<div class="storage-section-header">
|
||||
<h2 class="font-display text-xl font-semibold text-m-text">Stockage</h2>
|
||||
<span class="font-mono text-[10px] uppercase tracking-widest text-m-muted">Volumes</span>
|
||||
</div>
|
||||
<div class="storage-grid">
|
||||
<DiagramStorage
|
||||
v-for="(item, idx) in diagramItems"
|
||||
:key="item.key"
|
||||
:item="item"
|
||||
:style="{ animationDelay: `${idx * 150}ms` }"
|
||||
class="animate-fade-in-up"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div class="flex">
|
||||
<div class="flex flex-col gap-4">
|
||||
<StatusSite />
|
||||
<BackupButtonSee @select="selectedBackup = $event" />
|
||||
</div>
|
||||
<div class="dashboard-grid">
|
||||
<div class="grid-left">
|
||||
<StatusSite class="animate-fade-in-up" style="animation-delay: 100ms" />
|
||||
<BackupButtonSee
|
||||
class="animate-fade-in-up backup-selector"
|
||||
style="animation-delay: 200ms"
|
||||
@select="selectedBackup = $event"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<Speedtest />
|
||||
<BackupList :folder="selectedBackup" />
|
||||
<MessageDiscord/>
|
||||
<div class="grid-middle">
|
||||
<Speedtest class="animate-fade-in-up speedtest-card-mobile" style="animation-delay: 150ms" />
|
||||
<BackupList
|
||||
class="animate-fade-in-up backup-list-mobile"
|
||||
style="animation-delay: 250ms"
|
||||
:folder="selectedBackup"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-aside">
|
||||
<MessageDiscord class="animate-fade-in-up" style="animation-delay: 300ms" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</NuxtLayout>
|
||||
@@ -39,18 +60,32 @@
|
||||
definePageMeta({layout: false})
|
||||
import {computed, onMounted, ref} from "vue"
|
||||
|
||||
type SourceKey = "remote" | "local"
|
||||
type DiskCommandResult = { ok: boolean; output: string }
|
||||
type DiskSourceResult = {
|
||||
key: string
|
||||
label: string
|
||||
ok: boolean
|
||||
output: string
|
||||
}
|
||||
|
||||
type DiskApiResponse = {
|
||||
remote?: string | DiskCommandResult
|
||||
local?: string | DiskCommandResult
|
||||
results: DiskSourceResult[]
|
||||
}
|
||||
|
||||
type DiagramItem = {
|
||||
key: string
|
||||
loading: boolean
|
||||
hostName: string
|
||||
statusColorClass: string
|
||||
chartRadius: number
|
||||
chartCircumference: number
|
||||
chartOffset: number
|
||||
remainingPercentText: string
|
||||
usedText: string
|
||||
totalText: string
|
||||
}
|
||||
|
||||
const selectedBackup = ref<string | null>(null)
|
||||
const rawResults = ref<Record<SourceKey, string>>({
|
||||
remote: "",
|
||||
local: ""
|
||||
})
|
||||
const rawResults = ref<DiskSourceResult[]>([])
|
||||
const loading = ref(false)
|
||||
const chartRadius = 52
|
||||
const chartCircumference = 2 * Math.PI * chartRadius
|
||||
@@ -93,21 +128,9 @@ const getDiskValues = (output: string) => {
|
||||
return {availableGb, usedGb, totalGb}
|
||||
}
|
||||
|
||||
const getOutputText = (entry: unknown) => {
|
||||
if (typeof entry === "string") return entry
|
||||
if (entry && typeof entry === "object" && "output" in entry) {
|
||||
const output = (entry as DiskCommandResult).output
|
||||
return typeof output === "string" ? output : String(output)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
const diagramItems = computed(() => {
|
||||
return [
|
||||
{ key: "remote" as const, fallbackHost: "Serveur distant", output: rawResults.value.remote },
|
||||
{ key: "local" as const, fallbackHost: "Machine locale", output: rawResults.value.local }
|
||||
].map((item) => {
|
||||
const diskValues = getDiskValues(item.output)
|
||||
const diagramItems = computed<DiagramItem[]>(() => {
|
||||
return rawResults.value.map((result) => {
|
||||
const diskValues = getDiskValues(result.output)
|
||||
const remainingPercent =
|
||||
diskValues === null
|
||||
? null
|
||||
@@ -116,15 +139,15 @@ const diagramItems = computed(() => {
|
||||
Math.min(100, Math.round((diskValues.availableGb / diskValues.totalGb) * 100))
|
||||
)
|
||||
|
||||
const chartOffset = chartCircumference - ((remainingPercent ?? 0) / 100) * chartCircumference
|
||||
const statusColorClass =
|
||||
remainingPercent !== null && remainingPercent <= 30 ? "m-error" : "m-success"
|
||||
|
||||
return {
|
||||
key: item.key,
|
||||
hostName: getHostName(item.output, item.fallbackHost),
|
||||
statusColorClass,
|
||||
chartOffset,
|
||||
key: result.key,
|
||||
loading: loading.value,
|
||||
hostName: getHostName(result.output, result.label),
|
||||
statusColorClass:
|
||||
remainingPercent !== null && remainingPercent <= 30 ? "status-error" : "status-success",
|
||||
chartRadius,
|
||||
chartCircumference,
|
||||
chartOffset: chartCircumference - ((remainingPercent ?? 0) / 100) * chartCircumference,
|
||||
remainingPercentText:
|
||||
loading.value ? "..." : remainingPercent === null ? "--%" : `${remainingPercent}%`,
|
||||
usedText: loading.value ? "..." : diskValues ? `${diskValues.usedGb.toFixed(2)} GB` : "--",
|
||||
@@ -133,40 +156,139 @@ const diagramItems = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
const runScript = async () => {
|
||||
loading.value = true
|
||||
rawResults.value = {
|
||||
remote: "",
|
||||
local: ""
|
||||
}
|
||||
rawResults.value = []
|
||||
|
||||
try {
|
||||
const output = await $fetch<DiskApiResponse | string>("/api/disk")
|
||||
|
||||
if (typeof output === "string") {
|
||||
rawResults.value = {
|
||||
remote: output,
|
||||
local: "Erreur: sortie locale indisponible"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
rawResults.value = {
|
||||
remote: getOutputText(output.remote),
|
||||
local: getOutputText(output.local)
|
||||
}
|
||||
const output = await $fetch<DiskApiResponse>("/api/disk")
|
||||
rawResults.value = output.results
|
||||
} catch (error) {
|
||||
const message = `Erreur: ${error instanceof Error ? error.message : String(error)}`
|
||||
rawResults.value = {
|
||||
remote: message,
|
||||
local: message
|
||||
}
|
||||
rawResults.value = [
|
||||
{
|
||||
key: "error",
|
||||
label: "Source indisponible",
|
||||
ok: false,
|
||||
output: message
|
||||
}
|
||||
]
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
runScript()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.dashboard-container {
|
||||
padding: 2rem 2.5rem;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1.5rem;
|
||||
border-bottom: 1px solid rgba(80, 140, 255, 0.08);
|
||||
}
|
||||
|
||||
.storage-section {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.storage-section-header {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.storage-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
|
||||
gap: 1rem;
|
||||
border-radius: 12px;
|
||||
background: rgb(var(--m-secondary));
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.content-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 320px;
|
||||
gap: 1.5rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.content-main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.content-aside {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 280px minmax(0, 1fr);
|
||||
gap: 1.5rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.grid-left,
|
||||
.grid-middle,
|
||||
.grid-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.content-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.content-aside {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.dashboard-container {
|
||||
padding: 4.5rem 1.25rem 1.25rem;
|
||||
}
|
||||
|
||||
.dashboard-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.storage-grid,
|
||||
.content-grid,
|
||||
.dashboard-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.backup-selector {
|
||||
order: 2;
|
||||
}
|
||||
|
||||
.backup-list-mobile {
|
||||
order: 3;
|
||||
}
|
||||
|
||||
.speedtest-card-mobile {
|
||||
order: 4;
|
||||
}
|
||||
|
||||
.content-aside {
|
||||
grid-column: auto;
|
||||
order: 5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user