add vcenter totals line graph
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2026-01-16 12:36:53 +11:00
parent 268919219e
commit 871904f63e
14 changed files with 1841 additions and 132 deletions

View File

@@ -1,6 +1,7 @@
package views
import (
"fmt"
"vctp/components/core"
)
@@ -11,6 +12,47 @@ type SnapshotEntry struct {
Group string
}
type VcenterLink struct {
Name string
Link string
}
type VcenterTotalsEntry struct {
Snapshot string
RawTime int64
VmCount int64
VcpuTotal int64
RamTotalGB int64
}
type VcenterTotalsMeta struct {
ViewType string
TypeLabel string
HourlyLink string
DailyLink string
MonthlyLink string
HourlyClass string
DailyClass string
MonthlyClass string
}
type VcenterChartData struct {
PointsVm string
PointsVcpu string
PointsRam string
Width int
Height int
GridX []float64
GridY []float64
YTicks []ChartTick
XTicks []ChartTick
}
type ChartTick struct {
Pos float64
Label string
}
templ SnapshotHourlyList(entries []SnapshotEntry) {
@SnapshotListPage("Hourly Inventory Snapshots", "inventory snapshots captured hourly", entries)
}
@@ -84,3 +126,160 @@ templ SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) {
@core.Footer()
</html>
}
templ VcenterList(links []VcenterLink) {
<!DOCTYPE html>
<html lang="en">
@core.Header()
<body class="flex flex-col min-h-screen web2-bg">
<main class="flex-grow web2-shell space-y-8">
<section class="web2-header">
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<div class="web2-pill">vCenter Inventory</div>
<h1 class="mt-3 text-4xl font-bold">Monitored vCenters</h1>
<p class="mt-2 text-sm text-slate-600">Select a vCenter to view snapshot totals over time.</p>
</div>
<a class="web2-button" href="/">Back to Dashboard</a>
</div>
</section>
<section class="web2-card">
<div class="flex items-center justify-between gap-3 mb-4 flex-wrap">
<h2 class="text-lg font-semibold">vCenters</h2>
<span class="web2-badge">{len(links)} total</span>
</div>
<div class="overflow-hidden border border-slate-200 rounded">
<table class="web2-table">
<thead>
<tr>
<th>vCenter</th>
<th class="text-right">Totals</th>
</tr>
</thead>
<tbody>
for _, link := range links {
<tr>
<td class="font-semibold text-slate-700">{link.Name}</td>
<td class="text-right">
<a class="web2-link" href={link.Link}>View Totals</a>
</td>
</tr>
}
</tbody>
</table>
</div>
</section>
</main>
</body>
@core.Footer()
</html>
}
templ VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart VcenterChartData, meta VcenterTotalsMeta) {
<!DOCTYPE html>
<html lang="en">
@core.Header()
<body class="flex flex-col min-h-screen web2-bg">
<main class="flex-grow web2-shell space-y-8 max-w-screen-2xl mx-auto" style="max-width: 1400px;">
<section class="web2-header">
<div class="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
<div>
<div class="web2-pill">vCenter Totals</div>
<h1 class="mt-3 text-4xl font-bold">Totals for {vcenter}</h1>
<p class="mt-2 text-sm text-slate-600">{meta.TypeLabel} snapshots of VM count, vCPU, and RAM over time.</p>
</div>
<div class="flex gap-3">
<a class="web2-button secondary" href="/vcenters">All vCenters</a>
<a class="web2-button" href="/">Dashboard</a>
</div>
</div>
<div class="web3-button-group mt-8 mb-3">
<a class={meta.HourlyClass} href={meta.HourlyLink}>Hourly</a>
<a class={meta.DailyClass} href={meta.DailyLink}>Daily</a>
<a class={meta.MonthlyClass} href={meta.MonthlyLink}>Monthly</a>
</div>
</section>
<section class="web2-card">
<div class="flex items-center justify-between gap-3 mb-4 flex-wrap">
<h2 class="text-lg font-semibold">{meta.TypeLabel} Snapshots</h2>
<span class="web2-badge">{len(entries)} records</span>
</div>
if chart.PointsVm != "" {
<div class="mb-6 overflow-auto">
<svg width="100%" height="360" viewBox={"0 0 " + fmt.Sprintf("%d", chart.Width) + " 320"} role="img" aria-label="Totals over time">
<defs>
<linearGradient id="grid" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#e2e8f0" stop-opacity="0.6"></stop>
</linearGradient>
</defs>
<rect x="40" y="10" width={fmt.Sprintf("%d", chart.Width-60)} height="220" fill="white" stroke="#e2e8f0"></rect>
<!-- grid lines -->
<g stroke="#e2e8f0" stroke-width="1" stroke-dasharray="2,4">
for _, y := range chart.GridY {
<line x1="40" y1={fmt.Sprintf("%.1f", y)} x2={fmt.Sprintf("%d", chart.Width-20)} y2={fmt.Sprintf("%.1f", y)} />
}
for _, x := range chart.GridX {
<line x1={fmt.Sprintf("%.1f", x)} y1="10" x2={fmt.Sprintf("%.1f", x)} y2="230" />
}
</g>
<!-- axes -->
<line x1="40" y1="230" x2={fmt.Sprintf("%d", chart.Width-20)} y2="230" stroke="#94a3b8" stroke-width="1.5"></line>
<line x1="40" y1="10" x2="40" y2="230" stroke="#94a3b8" stroke-width="1.5"></line>
<!-- data -->
<polyline points={chart.PointsVm} fill="none" stroke="#2563eb" stroke-width="2.5"></polyline>
<polyline points={chart.PointsVcpu} fill="none" stroke="#16a34a" stroke-width="2.5"></polyline>
<polyline points={chart.PointsRam} fill="none" stroke="#ea580c" stroke-width="2.5"></polyline>
<!-- tick labels -->
<g font-size="10" fill="#475569" text-anchor="end">
for _, tick := range chart.YTicks {
<text x="36" y={fmt.Sprintf("%.1f", tick.Pos+3)}>{tick.Label}</text>
}
</g>
<g font-size="10" fill="#475569" text-anchor="middle">
for _, tick := range chart.XTicks {
<text x={fmt.Sprintf("%.1f", tick.Pos)} y="244">{tick.Label}</text>
}
</g>
<!-- legend -->
<g font-size="12" fill="#475569" transform={"translate(40 300)"}>
<rect x="0" y="0" width="14" height="8" fill="#2563eb"></rect><text x="22" y="12">VMs</text>
<rect x="90" y="0" width="14" height="8" fill="#16a34a"></rect><text x="112" y="12">vCPU</text>
<rect x="180" y="0" width="14" height="8" fill="#ea580c"></rect><text x="202" y="12">RAM (GB)</text>
</g>
<!-- axis labels -->
<text x="15" y="20" transform={"rotate(-90 15 20)"} font-size="12" fill="#475569">Totals</text>
<text x={fmt.Sprintf("%d", chart.Width/2)} y="310" font-size="12" fill="#475569">Snapshot sequence (newest right)</text>
</svg>
</div>
}
<div class="overflow-hidden border border-slate-200 rounded">
<table class="web2-table">
<thead>
<tr>
<th>Snapshot Time</th>
<th class="text-right">VMs</th>
<th class="text-right">vCPUs</th>
<th class="text-right">RAM (GB)</th>
</tr>
</thead>
<tbody>
for _, entry := range entries {
<tr>
<td>{entry.Snapshot}</td>
<td class="text-right">{entry.VmCount}</td>
<td class="text-right">{entry.VcpuTotal}</td>
<td class="text-right">{entry.RamTotalGB}</td>
</tr>
}
</tbody>
</table>
</div>
</section>
</main>
</body>
@core.Footer()
</html>
}