From a993aedf7976422c175b18add4ce60e959454016 Mon Sep 17 00:00:00 2001 From: Nathan Coad Date: Fri, 6 Feb 2026 16:42:48 +1100 Subject: [PATCH] use javascript chart instead of svg --- .gitignore | 2 + components/core/header.templ | 1 + components/core/header_templ.go | 23 +- components/views/snapshots.templ | 162 +++---- components/views/snapshots_templ.go | 481 ++----------------- components/views/vm_trace.templ | 66 +-- components/views/vm_trace_templ.go | 654 +++++--------------------- dist/assets/css/web3.css | 68 +++ dist/assets/js/web3-charts.js | 521 ++++++++++++++++++++ server/handler/chart_builders_test.go | 101 ++++ server/handler/chart_config.go | 41 ++ server/handler/vcenters.go | 127 ++--- server/handler/vmTrace.go | 195 ++++---- 13 files changed, 1152 insertions(+), 1290 deletions(-) create mode 100644 dist/assets/js/web3-charts.js create mode 100644 server/handler/chart_builders_test.go create mode 100644 server/handler/chart_config.go diff --git a/.gitignore b/.gitignore index 0f7e2c6..57b6aa5 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,5 @@ Network Trash Folder Temporary Items .apdisk #/db/queries/*.go + +.gocache/ \ No newline at end of file diff --git a/components/core/header.templ b/components/core/header.templ index 979fcf4..5df5df7 100644 --- a/components/core/header.templ +++ b/components/core/header.templ @@ -12,6 +12,7 @@ templ Header() { + vCTP API") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/components/views/snapshots.templ b/components/views/snapshots.templ index cd9cd7e..047de1d 100644 --- a/components/views/snapshots.templ +++ b/components/views/snapshots.templ @@ -1,9 +1,6 @@ package views -import ( - "fmt" - "vctp/components/core" -) +import "vctp/components/core" type SnapshotEntry struct { Label string @@ -18,10 +15,10 @@ type VcenterLink struct { } type VcenterTotalsEntry struct { - Snapshot string - RawTime int64 - VmCount int64 - VcpuTotal int64 + Snapshot string + RawTime int64 + VmCount int64 + VcpuTotal int64 RamTotalGB int64 } @@ -37,20 +34,7 @@ type VcenterTotalsMeta struct { } 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 + ConfigJSON string } templ SnapshotHourlyList(entries []SnapshotEntry) { @@ -75,17 +59,16 @@ templ SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) {
Snapshot Library
-

{title}

-

{subtitle}

+

{ title }

+

{ subtitle }

Back to Dashboard
-

Available Exports

- {len(entries)} files + { len(entries) } files
@@ -100,20 +83,20 @@ templ SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) { for i, entry := range entries { if entry.Group != "" && (i == 0 || entries[i-1].Group != entry.Group) { - + } } @@ -143,11 +126,10 @@ templ VcenterList(links []VcenterLink) { Back to Dashboard -

vCenters

- {len(links)} total + { len(links) } total
{entry.Group}{ entry.Group }
- {entry.Label} + { entry.Label }
- {entry.Count} records + { entry.Count } records - Download XLSX + Download XLSX
@@ -160,9 +142,9 @@ templ VcenterList(links []VcenterLink) { for _, link := range links { - + } @@ -177,84 +159,48 @@ templ VcenterList(links []VcenterLink) { } templ VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart VcenterChartData, meta VcenterTotalsMeta) { - - - @core.Header() - -
+ + + @core.Header() + +
-
vCenter Totals
-

Totals for {vcenter}

-

{meta.TypeLabel} snapshots of VM count, vCPU, and RAM over time.

-
- +
vCenter Totals
+

Totals for { vcenter }

+

{ meta.TypeLabel } snapshots of VM count, vCPU, and RAM over time.

-
- -
-
-

{meta.TypeLabel} Snapshots

- {len(entries)} records -
- if chart.PointsVm != "" { + +
+ Hourly + Daily + Monthly +
+
+
+
+

{ meta.TypeLabel } Snapshots

+ { len(entries) } records +
+ if chart.ConfigJSON != "" {
- - - - - - - - - - for _, y := range chart.GridY { - - } - for _, x := range chart.GridX { - - } - - - - - - - - - - - for _, tick := range chart.YTicks { - {tick.Label} - } - - - for _, tick := range chart.XTicks { - {tick.Label} - } - - - - VMs - vCPU - RAM (GB) - - - Totals - Snapshot sequence (newest right) - +
+ + +
+
} -
{link.Name}{ link.Name } - View Totals + View Totals
@@ -268,10 +214,10 @@ templ VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcen for _, entry := range entries { - - - - + + + + } diff --git a/components/views/snapshots_templ.go b/components/views/snapshots_templ.go index 24559d6..926ff43 100644 --- a/components/views/snapshots_templ.go +++ b/components/views/snapshots_templ.go @@ -8,10 +8,7 @@ package views import "github.com/a-h/templ" import templruntime "github.com/a-h/templ/runtime" -import ( - "fmt" - "vctp/components/core" -) +import "vctp/components/core" type SnapshotEntry struct { Label string @@ -45,20 +42,7 @@ type VcenterTotalsMeta struct { } 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 + ConfigJSON string } func SnapshotHourlyList(entries []SnapshotEntry) templ.Component { @@ -184,7 +168,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te var templ_7745c5c3_Var5 string templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 78, Col: 49} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 62, Col: 50} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) if templ_7745c5c3_Err != nil { @@ -197,7 +181,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te var templ_7745c5c3_Var6 string templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(subtitle) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 79, Col: 55} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 63, Col: 56} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { @@ -210,7 +194,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te var templ_7745c5c3_Var7 string templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(len(entries)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 88, Col: 44} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 71, Col: 45} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { @@ -229,7 +213,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te var templ_7745c5c3_Var8 string templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Group) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 103, Col: 76} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 86, Col: 77} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) if templ_7745c5c3_Err != nil { @@ -247,7 +231,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te var templ_7745c5c3_Var9 string templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Label) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 109, Col: 75} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 92, Col: 76} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) if templ_7745c5c3_Err != nil { @@ -260,7 +244,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te var templ_7745c5c3_Var10 string templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Count) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 113, Col: 48} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 96, Col: 49} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { @@ -273,7 +257,7 @@ func SnapshotListPage(title string, subtitle string, entries []SnapshotEntry) te var templ_7745c5c3_Var11 templ.SafeURL templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinURLErrs(entry.Link) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 116, Col: 48} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 99, Col: 49} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) if templ_7745c5c3_Err != nil { @@ -336,7 +320,7 @@ func VcenterList(links []VcenterLink) templ.Component { var templ_7745c5c3_Var13 string templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(len(links)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 150, Col: 42} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 132, Col: 43} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) if templ_7745c5c3_Err != nil { @@ -354,7 +338,7 @@ func VcenterList(links []VcenterLink) templ.Component { var templ_7745c5c3_Var14 string templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(link.Name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 163, Col: 61} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 145, Col: 62} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { @@ -367,7 +351,7 @@ func VcenterList(links []VcenterLink) templ.Component { var templ_7745c5c3_Var15 templ.SafeURL templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinURLErrs(link.Link) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 165, Col: 47} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 147, Col: 48} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) if templ_7745c5c3_Err != nil { @@ -423,14 +407,14 @@ func VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcent if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
vCenter Totals

Totals for ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "
vCenter Totals

Totals for ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var17 string templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(vcenter) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 189, Col: 63} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 171, Col: 63} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) if templ_7745c5c3_Err != nil { @@ -443,7 +427,7 @@ func VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcent var templ_7745c5c3_Var18 string templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(meta.TypeLabel) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 190, Col: 62} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 172, Col: 62} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) if templ_7745c5c3_Err != nil { @@ -478,7 +462,7 @@ func VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcent var templ_7745c5c3_Var21 templ.SafeURL templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinURLErrs(meta.HourlyLink) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 198, Col: 56} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 180, Col: 58} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21)) if templ_7745c5c3_Err != nil { @@ -513,7 +497,7 @@ func VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcent var templ_7745c5c3_Var24 templ.SafeURL templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinURLErrs(meta.DailyLink) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 199, Col: 54} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 181, Col: 56} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24)) if templ_7745c5c3_Err != nil { @@ -548,7 +532,7 @@ func VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcent var templ_7745c5c3_Var27 templ.SafeURL templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinURLErrs(meta.MonthlyLink) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 200, Col: 58} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 182, Col: 60} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) if templ_7745c5c3_Err != nil { @@ -561,7 +545,7 @@ func VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcent var templ_7745c5c3_Var28 string templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(meta.TypeLabel) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 206, Col: 56} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 187, Col: 56} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28)) if templ_7745c5c3_Err != nil { @@ -574,7 +558,7 @@ func VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcent var templ_7745c5c3_Var29 string templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(len(entries)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 207, Col: 45} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 188, Col: 45} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29)) if templ_7745c5c3_Err != nil { @@ -584,449 +568,88 @@ func VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcent if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if chart.PointsVm != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "

{entry.Snapshot}{entry.VmCount}{entry.VcpuTotal}{entry.RamTotalGB}{ entry.Snapshot }{ entry.VmCount }{ entry.VcpuTotal }{ entry.RamTotalGB }
") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, entry := range entries { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "
Snapshot TimeVMsvCPUsRAM (GB)
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var31 string - templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs("0 0 " + fmt.Sprintf("%d", chart.Width) + " " + fmt.Sprintf("%d", chart.Height+70)) + templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Snapshot) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 211, Col: 160} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 217, Col: 30} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "\" role=\"img\" aria-label=\"Totals over time\"> ") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var32 string - templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Width-60)) + templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(entry.VmCount) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 217, Col: 68} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 218, Col: 48} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "\" height=\"") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var33 string - templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Height)) + templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(entry.VcpuTotal) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 217, Col: 109} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 219, Col: 50} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\" fill=\"white\" stroke=\"#e2e8f0\">") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - for _, y := range chart.GridY { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } + var templ_7745c5c3_Var34 string + templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(entry.RamTotalGB) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 220, Col: 51} } - for _, x := range chart.GridX { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - for _, tick := range chart.YTicks { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var48 string - templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(tick.Label) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 237, Col: 71} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - for _, tick := range chart.XTicks { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var51 string - templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinStringErrs(tick.Label) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 242, Col: 101} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 66, "VMs vCPU RAM (GB)Totals Snapshot sequence (newest right)") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - for _, entry := range entries { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 72, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "
Snapshot TimeVMsvCPUsRAM (GB)
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var56 string - templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Snapshot) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 271, Col: 29} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 73, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var57 string - templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinStringErrs(entry.VmCount) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 272, Col: 47} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var58 string - templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinStringErrs(entry.VcpuTotal) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 273, Col: 49} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var59 string - templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.JoinStringErrs(entry.RamTotalGB) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/snapshots.templ`, Line: 274, Col: 50} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var59)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "
") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -1034,7 +657,7 @@ func VcenterTotalsPage(vcenter string, entries []VcenterTotalsEntry, chart Vcent if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/components/views/vm_trace.templ b/components/views/vm_trace.templ index 8277dbe..35781a3 100644 --- a/components/views/vm_trace.templ +++ b/components/views/vm_trace.templ @@ -21,18 +21,7 @@ type VmTraceEntry struct { } type VmTraceChart struct { - PointsVcpu string - PointsRam string - PointsTin string - PointsBronze string - PointsSilver string - PointsGold string - Width int - Height int - GridX []float64 - GridY []float64 - XTicks []ChartTick - YTicks []ChartTick + ConfigJSON string } templ VmTracePage(query string, display_query string, vm_id string, vm_uuid string, vm_name string, creationLabel string, deletionLabel string, creationApprox bool, entries []VmTraceEntry, chart VmTraceChart) { @@ -40,7 +29,7 @@ templ VmTracePage(query string, display_query string, vm_id string, vm_uuid stri @core.Header() -
+
@@ -77,47 +66,18 @@ templ VmTracePage(query string, display_query string, vm_id string, vm_uuid stri

Snapshot Timeline

{len(entries)} samples
- if chart.PointsVcpu != "" { + if chart.ConfigJSON != "" {
- - - - for _, y := range chart.GridY { - - } - for _, x := range chart.GridX { - - } - - - - - - - - - - - for _, tick := range chart.YTicks { - {tick.Label} - } - - - for _, tick := range chart.XTicks { - {tick.Label} - } - - - vCPU - RAM (GB) - Tin - Bronze - Silver - Gold - - Resources / Pool - Snapshots (oldest left, newest right) - +
+ + +
+
}
diff --git a/components/views/vm_trace_templ.go b/components/views/vm_trace_templ.go index 60d1328..76da532 100644 --- a/components/views/vm_trace_templ.go +++ b/components/views/vm_trace_templ.go @@ -29,18 +29,7 @@ type VmTraceEntry struct { } type VmTraceChart struct { - PointsVcpu string - PointsRam string - PointsTin string - PointsBronze string - PointsSilver string - PointsGold string - Width int - Height int - GridX []float64 - GridY []float64 - XTicks []ChartTick - YTicks []ChartTick + ConfigJSON string } func VmTracePage(query string, display_query string, vm_id string, vm_uuid string, vm_name string, creationLabel string, deletionLabel string, creationApprox bool, entries []VmTraceEntry, chart VmTraceChart) templ.Component { @@ -72,14 +61,14 @@ func VmTracePage(query string, display_query string, vm_id string, vm_uuid strin if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
VM Trace

Snapshot history") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
VM Trace

Snapshot history") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var2 string templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(display_query) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 48, Col: 74} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 37, Col: 74} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) if templ_7745c5c3_Err != nil { @@ -92,7 +81,7 @@ func VmTracePage(query string, display_query string, vm_id string, vm_uuid strin var templ_7745c5c3_Var3 string templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(vm_id) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 58, Col: 123} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 47, Col: 123} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) if templ_7745c5c3_Err != nil { @@ -105,7 +94,7 @@ func VmTracePage(query string, display_query string, vm_id string, vm_uuid strin var templ_7745c5c3_Var4 string templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(vm_uuid) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 62, Col: 129} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 51, Col: 129} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) if templ_7745c5c3_Err != nil { @@ -118,7 +107,7 @@ func VmTracePage(query string, display_query string, vm_id string, vm_uuid strin var templ_7745c5c3_Var5 string templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(vm_name) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 66, Col: 123} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 55, Col: 123} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) if templ_7745c5c3_Err != nil { @@ -131,7 +120,7 @@ func VmTracePage(query string, display_query string, vm_id string, vm_uuid strin var templ_7745c5c3_Var6 string templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(len(entries)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 78, Col: 44} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 67, Col: 44} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { @@ -141,576 +130,189 @@ func VmTracePage(query string, display_query string, vm_id string, vm_uuid strin if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - if chart.PointsVcpu != "" { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var8 string - templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Width-60)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 83, Col: 68} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "

Creation time

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(creationLabel) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 86, Col: 76} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + if creationApprox { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "

Approximate (earliest snapshot)

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\" height=\"") + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "

Deletion time

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(deletionLabel) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 93, Col: 76} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "

") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + for _, e := range entries { + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "

Creation time

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var35 string - templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(creationLabel) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 126, Col: 76} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - if creationApprox { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "

Approximate (earliest snapshot)

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "

Deletion time

") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var36 string - templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(deletionLabel) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 133, Col: 76} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, "

SnapshotVM NameVmIdVmUuidVcenterResource PoolvCPUsRAM (GB)Disk
") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var9 string - templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Height)) + var templ_7745c5c3_Var10 string + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(e.Snapshot) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 83, Col: 109} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 114, Col: 25} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" fill=\"white\" stroke=\"#e2e8f0\"> ") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - for _, y := range chart.GridY { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(e.Name) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 115, Col: 21} } - for _, x := range chart.GridX { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, " ") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(e.VmId) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 116, Col: 21} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var13 string + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(e.VmUuid) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 117, Col: 23} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var14 string + templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(e.Vcenter) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 118, Col: 24} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "") + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + var templ_7745c5c3_Var15 string + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(e.ResourcePool) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 119, Col: 29} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) + if templ_7745c5c3_Err != nil { + return templ_7745c5c3_Err + } + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var16 string - templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Height+10)) + templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(e.VcpuCount) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 92, Col: 60} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 120, Col: 45} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" x2=\"") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var17 string - templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Width-20)) + templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(e.RamGB) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 92, Col: 99} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 121, Col: 41} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" y2=\"") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } var templ_7745c5c3_Var18 string - templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", chart.Height+10)) + templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", e.ProvisionedDisk)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 92, Col: 139} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 122, Col: 72} } _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" stroke=\"#94a3b8\" stroke-width=\"1.5\"> ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - for _, tick := range chart.YTicks { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var27 string - templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(tick.Label) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 102, Col: 70} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " ") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - for _, tick := range chart.XTicks { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var30 string - templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(tick.Label) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 107, Col: 100} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, " vCPU RAM (GB) Tin Bronze Silver Gold Resources / Pool Snapshots (oldest left, newest right)") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - for _, e := range entries { - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "
SnapshotVM NameVmIdVmUuidVcenterResource PoolvCPUsRAM (GB)Disk
") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var37 string - templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(e.Snapshot) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 154, Col: 25} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var38 string - templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(e.Name) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 155, Col: 21} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var39 string - templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(e.VmId) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 156, Col: 21} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var40 string - templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(e.VmUuid) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 157, Col: 23} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var41 string - templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(e.Vcenter) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 158, Col: 24} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var42 string - templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(e.ResourcePool) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 159, Col: 29} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var43 string - templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(e.VcpuCount) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 160, Col: 45} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var44 string - templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(e.RamGB) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 161, Col: 41} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "") - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - var templ_7745c5c3_Var45 string - templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", e.ProvisionedDisk)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/vm_trace.templ`, Line: 162, Col: 72} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "

") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "

") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -718,7 +320,7 @@ func VmTracePage(query string, display_query string, vm_id string, vm_uuid strin if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "") + templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/dist/assets/css/web3.css b/dist/assets/css/web3.css index bcfe36e..147a79d 100644 --- a/dist/assets/css/web3.css +++ b/dist/assets/css/web3.css @@ -17,6 +17,9 @@ body { margin: 0 auto; padding: 2rem 1.5rem 4rem; } +.web2-shell-wide { + max-width: 1400px; +} .web2-header { background: var(--web2-card); border: 1px solid var(--web2-border); @@ -176,3 +179,68 @@ body { color: var(--web2-muted); background: #f8fafc; } +.web3-chart-frame { + position: relative; + min-width: 760px; + width: 100%; +} +.web3-chart-canvas { + display: block; + width: 100%; + height: 360px; + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 6px; +} +.web3-chart-tooltip { + position: absolute; + left: 0; + top: 0; + opacity: 0; + pointer-events: none; + background: rgba(15, 23, 42, 0.95); + color: #f8fafc; + padding: 0.55rem 0.65rem; + border-radius: 6px; + font-size: 0.75rem; + line-height: 1.35; + min-width: 170px; + box-shadow: 0 10px 30px rgba(2, 6, 23, 0.25); + z-index: 20; + transition: opacity 0.08s linear; +} +.web3-chart-tooltip.visible { + opacity: 1; +} +.web3-chart-tooltip-title { + font-weight: 700; + color: #e2e8f0; + margin-bottom: 0.35rem; +} +.web3-chart-tooltip-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.65rem; +} +.web3-chart-tooltip-label { + display: inline-flex; + align-items: center; + color: #cbd5e1; +} +.web3-chart-tooltip-value { + font-weight: 700; + color: #f8fafc; +} +.web3-chart-tooltip-swatch { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 999px; + margin-right: 0.35rem; +} +@media (max-width: 900px) { + .web3-chart-frame { + min-width: 640px; + } +} diff --git a/dist/assets/js/web3-charts.js b/dist/assets/js/web3-charts.js new file mode 100644 index 0000000..87f36dc --- /dev/null +++ b/dist/assets/js/web3-charts.js @@ -0,0 +1,521 @@ +(function () { + "use strict"; + + function clamp(value, min, max) { + if (value < min) { + return min; + } + if (value > max) { + return max; + } + return value; + } + + function toNumber(value) { + var num = Number(value); + return Number.isFinite(num) ? num : null; + } + + function escapeHTML(value) { + return String(value) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } + + function formatValue(value, format) { + if (value === null || value === undefined || Number.isNaN(value)) { + return "-"; + } + switch (format) { + case "int": + return String(Math.round(value)); + case "float1": + return Number(value).toFixed(1); + case "float2": + return Number(value).toFixed(2); + default: + return String(value); + } + } + + function pickTickIndices(total, desired) { + if (total <= 0) { + return []; + } + if (total === 1) { + return [0]; + } + var target = Math.max(2, Math.min(total, desired || 6)); + if (target >= total) { + var all = []; + for (var i = 0; i < total; i++) { + all.push(i); + } + return all; + } + var indices = [0]; + var step = (total - 1) / (target - 1); + for (var j = 1; j < target - 1; j++) { + indices.push(Math.round(j * step)); + } + indices.push(total - 1); + var seen = {}; + var deduped = []; + for (var k = 0; k < indices.length; k++) { + var idx = indices[k]; + if (!seen[idx]) { + seen[idx] = true; + deduped.push(idx); + } + } + deduped.sort(function (a, b) { + return a - b; + }); + return deduped; + } + + function getPlotBounds(width, height) { + return { + left: 52, + top: 16, + right: width - 20, + bottom: height - 78, + }; + } + + function buildScales(config, plot) { + var maxY = 0; + for (var i = 0; i < config.series.length; i++) { + var values = config.series[i].values || []; + for (var j = 0; j < values.length; j++) { + var value = toNumber(values[j]); + if (value !== null && value > maxY) { + maxY = value; + } + } + } + if (maxY <= 0) { + maxY = 1; + } + var count = config.labels.length; + var xSpan = plot.right - plot.left; + var ySpan = plot.bottom - plot.top; + + return { + maxY: maxY, + xForIndex: function (index) { + if (count <= 1) { + return plot.left; + } + return plot.left + (index / (count - 1)) * xSpan; + }, + yForValue: function (value) { + var numeric = toNumber(value); + if (numeric === null) { + return null; + } + return plot.bottom - (numeric / maxY) * ySpan; + }, + }; + } + + function drawGrid(ctx, plot, config, scales) { + ctx.save(); + ctx.strokeStyle = "#e2e8f0"; + ctx.lineWidth = 1; + ctx.setLineDash([2, 4]); + + var yTickCount = Math.max(2, config.yTicks || 5); + for (var i = 0; i < yTickCount; i++) { + var yRatio = i / (yTickCount - 1); + var y = plot.top + yRatio * (plot.bottom - plot.top); + ctx.beginPath(); + ctx.moveTo(plot.left, y); + ctx.lineTo(plot.right, y); + ctx.stroke(); + } + + var xIndices = pickTickIndices(config.labels.length, config.xTicks || 6); + for (var j = 0; j < xIndices.length; j++) { + var x = scales.xForIndex(xIndices[j]); + ctx.beginPath(); + ctx.moveTo(x, plot.top); + ctx.lineTo(x, plot.bottom); + ctx.stroke(); + } + ctx.restore(); + } + + function drawAxes(ctx, plot) { + ctx.save(); + ctx.strokeStyle = "#94a3b8"; + ctx.lineWidth = 1.5; + ctx.setLineDash([]); + + ctx.beginPath(); + ctx.moveTo(plot.left, plot.bottom); + ctx.lineTo(plot.right, plot.bottom); + ctx.stroke(); + + ctx.beginPath(); + ctx.moveTo(plot.left, plot.top); + ctx.lineTo(plot.left, plot.bottom); + ctx.stroke(); + ctx.restore(); + } + + function drawLabels(ctx, plot, config, scales) { + ctx.save(); + ctx.fillStyle = "#475569"; + ctx.font = "10px sans-serif"; + + var yTickCount = Math.max(2, config.yTicks || 5); + for (var i = 0; i < yTickCount; i++) { + var ratio = i / (yTickCount - 1); + var y = plot.top + ratio * (plot.bottom - plot.top); + var value = scales.maxY * (1 - ratio); + ctx.textAlign = "right"; + ctx.textBaseline = "middle"; + ctx.fillText(formatValue(value, "int"), plot.left - 8, y); + } + + var xIndices = pickTickIndices(config.labels.length, config.xTicks || 6); + for (var j = 0; j < xIndices.length; j++) { + var idx = xIndices[j]; + var tick = (config.tickLabels && config.tickLabels[idx]) || config.labels[idx] || ""; + ctx.textAlign = "center"; + ctx.textBaseline = "top"; + ctx.fillText(tick, scales.xForIndex(idx), plot.bottom + 12); + } + + if (config.yLabel) { + ctx.save(); + ctx.translate(16, plot.top + (plot.bottom-plot.top)/2); + ctx.rotate(-Math.PI / 2); + ctx.textAlign = "center"; + ctx.textBaseline = "top"; + ctx.font = "12px sans-serif"; + ctx.fillText(config.yLabel, 0, 0); + ctx.restore(); + } + + if (config.xLabel) { + ctx.textAlign = "center"; + ctx.textBaseline = "top"; + ctx.font = "12px sans-serif"; + ctx.fillText(config.xLabel, plot.left + (plot.right-plot.left)/2, plot.bottom + 48); + } + ctx.restore(); + } + + function drawSeries(ctx, plot, config, scales) { + for (var i = 0; i < config.series.length; i++) { + var series = config.series[i]; + var values = series.values || []; + if (!values.length) { + continue; + } + ctx.save(); + ctx.strokeStyle = series.color || "#2563eb"; + ctx.lineWidth = series.lineWidth || 2.5; + ctx.setLineDash(Array.isArray(series.dash) ? series.dash : []); + ctx.beginPath(); + var moved = false; + for (var j = 0; j < values.length; j++) { + var y = scales.yForValue(values[j]); + if (y === null) { + continue; + } + var x = scales.xForIndex(j); + if (!moved) { + ctx.moveTo(x, y); + moved = true; + } else { + ctx.lineTo(x, y); + } + } + ctx.stroke(); + ctx.restore(); + } + } + + function drawLegend(ctx, config, width, height) { + var x = 52; + var y = height - 32; + + ctx.save(); + ctx.font = "12px sans-serif"; + ctx.textBaseline = "middle"; + for (var i = 0; i < config.series.length; i++) { + var series = config.series[i]; + var label = series.name || "Series"; + ctx.strokeStyle = series.color || "#2563eb"; + ctx.fillStyle = "#475569"; + ctx.lineWidth = series.lineWidth || 2.5; + ctx.setLineDash(Array.isArray(series.dash) ? series.dash : []); + ctx.beginPath(); + ctx.moveTo(x, y); + ctx.lineTo(x + 16, y); + ctx.stroke(); + ctx.setLineDash([]); + ctx.fillText(label, x + 22, y); + x += 22 + ctx.measureText(label).width + 18; + if (x > width - 160) { + x = 52; + y += 18; + } + } + ctx.restore(); + } + + function updateTooltip(state, config) { + if (!state.tooltip) { + return; + } + if (state.hoverIndex === null) { + state.tooltip.classList.remove("visible"); + state.tooltip.setAttribute("aria-hidden", "true"); + return; + } + + var idx = state.hoverIndex; + var rows = []; + rows.push('
' + escapeHTML(config.labels[idx] || "") + "
"); + for (var i = 0; i < config.series.length; i++) { + var series = config.series[i]; + if (series.tooltipHidden) { + continue; + } + var values = series.values || []; + var value = toNumber(values[idx]); + var valueLabel = formatValue(value, series.tooltipFormat || "int"); + rows.push( + '
' + + '' + + escapeHTML(series.name || "Series") + + "" + + '' + escapeHTML(valueLabel) + "" + + "
" + ); + } + + var hoverRows = config.hoverRows || []; + for (var j = 0; j < hoverRows.length; j++) { + var hover = hoverRows[j]; + var values = hover.values || []; + var label = values[idx] || "-"; + rows.push( + '
' + + '' + escapeHTML(hover.name || "Value") + "" + + '' + escapeHTML(label) + "" + + "
" + ); + } + + state.tooltip.innerHTML = rows.join(""); + state.tooltip.classList.add("visible"); + state.tooltip.setAttribute("aria-hidden", "false"); + + var box = state.wrapper.getBoundingClientRect(); + var tooltipBox = state.tooltip.getBoundingClientRect(); + var left = clamp(state.mouseX + 14, 4, box.width - tooltipBox.width - 4); + var top = clamp(state.mouseY + 14, 4, box.height - tooltipBox.height - 4); + state.tooltip.style.left = left + "px"; + state.tooltip.style.top = top + "px"; + } + + function drawHover(ctx, plot, config, scales, hoverIndex) { + if (hoverIndex === null) { + return; + } + var x = scales.xForIndex(hoverIndex); + ctx.save(); + ctx.strokeStyle = "#94a3b8"; + ctx.lineWidth = 1; + ctx.setLineDash([3, 4]); + ctx.beginPath(); + ctx.moveTo(x, plot.top); + ctx.lineTo(x, plot.bottom); + ctx.stroke(); + ctx.setLineDash([]); + + for (var i = 0; i < config.series.length; i++) { + var series = config.series[i]; + var values = series.values || []; + var y = scales.yForValue(values[hoverIndex]); + if (y === null) { + continue; + } + ctx.fillStyle = "#ffffff"; + ctx.strokeStyle = series.color || "#2563eb"; + ctx.lineWidth = 1.5; + ctx.beginPath(); + ctx.arc(x, y, 3.5, 0, Math.PI * 2); + ctx.fill(); + ctx.stroke(); + } + ctx.restore(); + } + + function renderLineChart(options) { + if (!options || !options.canvasId || !options.config) { + return; + } + var canvas = document.getElementById(options.canvasId); + if (!canvas) { + return; + } + var config = options.config; + if (!Array.isArray(config.labels) || config.labels.length === 0 || !Array.isArray(config.series) || config.series.length === 0) { + return; + } + var wrapper = canvas.parentElement; + var tooltip = options.tooltipId ? document.getElementById(options.tooltipId) : null; + var ctx = canvas.getContext("2d"); + if (!ctx) { + return; + } + + var state = { + canvas: canvas, + wrapper: wrapper, + tooltip: tooltip, + hoverIndex: null, + mouseX: 0, + mouseY: 0, + scales: null, + plot: null, + cssWidth: 0, + cssHeight: config.height || 360, + }; + + function redraw() { + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.clearRect(0, 0, canvas.width, canvas.height); + + var dpr = window.devicePixelRatio || 1; + ctx.setTransform(dpr, 0, 0, dpr, 0, 0); + ctx.fillStyle = "#ffffff"; + ctx.fillRect(0, 0, state.cssWidth, state.cssHeight); + + drawGrid(ctx, state.plot, config, state.scales); + drawAxes(ctx, state.plot); + drawSeries(ctx, state.plot, config, state.scales); + drawLabels(ctx, state.plot, config, state.scales); + drawLegend(ctx, config, state.cssWidth, state.cssHeight); + drawHover(ctx, state.plot, config, state.scales, state.hoverIndex); + updateTooltip(state, config); + } + + function resize() { + var rect = canvas.getBoundingClientRect(); + var width = Math.max(320, Math.floor(rect.width)); + var height = config.height || 360; + var dpr = window.devicePixelRatio || 1; + + canvas.width = Math.round(width * dpr); + canvas.height = Math.round(height * dpr); + canvas.style.height = height + "px"; + + state.cssWidth = width; + state.cssHeight = height; + state.plot = getPlotBounds(width, height); + state.scales = buildScales(config, state.plot); + redraw(); + } + + canvas.addEventListener("mousemove", function (event) { + var rect = canvas.getBoundingClientRect(); + var x = event.clientX - rect.left; + var y = event.clientY - rect.top; + state.mouseX = x; + state.mouseY = y; + + if (!state.plot || config.labels.length === 0) { + return; + } + if (x < state.plot.left || x > state.plot.right || y < state.plot.top || y > state.plot.bottom) { + state.hoverIndex = null; + redraw(); + return; + } + + var ratio = (x - state.plot.left) / (state.plot.right - state.plot.left); + var idx = Math.round(ratio * (config.labels.length - 1)); + state.hoverIndex = clamp(idx, 0, config.labels.length - 1); + redraw(); + }); + + canvas.addEventListener("mouseleave", function () { + state.hoverIndex = null; + redraw(); + }); + + window.addEventListener("resize", resize); + if (window.ResizeObserver) { + var observer = new ResizeObserver(function () { + resize(); + }); + observer.observe(wrapper); + } + + resize(); + } + + function renderFromScript(options) { + if (!options || !options.configId) { + return; + } + var configNode = document.getElementById(options.configId); + if (!configNode) { + return; + } + var payload = configNode.textContent || ""; + if (!payload.trim()) { + return; + } + try { + var config = JSON.parse(payload); + renderLineChart({ + canvasId: options.canvasId, + tooltipId: options.tooltipId, + config: config, + }); + } catch (error) { + // Leave page functional even when chart config is malformed. + } + } + + function renderFromDataset(options) { + if (!options || !options.canvasId) { + return; + } + var canvas = document.getElementById(options.canvasId); + if (!canvas) { + return; + } + var payload = canvas.dataset.chartConfig || ""; + if (!payload.trim()) { + return; + } + try { + var config = JSON.parse(payload); + renderLineChart({ + canvasId: options.canvasId, + tooltipId: options.tooltipId, + config: config, + }); + } catch (error) { + // Leave page functional even when chart config is malformed. + } + } + + window.Web3Charts = { + renderLineChart: renderLineChart, + renderFromScript: renderFromScript, + renderFromDataset: renderFromDataset, + }; +})(); diff --git a/server/handler/chart_builders_test.go b/server/handler/chart_builders_test.go new file mode 100644 index 0000000..65069e0 --- /dev/null +++ b/server/handler/chart_builders_test.go @@ -0,0 +1,101 @@ +package handler + +import ( + "encoding/json" + "testing" + "time" + "vctp/components/views" +) + +func TestBuildVcenterChartEncodesClientConfig(t *testing.T) { + entries := []views.VcenterTotalsEntry{ + { + RawTime: 2_000, + VmCount: 30, + VcpuTotal: 80, + RamTotalGB: 120, + }, + { + RawTime: 1_000, + VmCount: 20, + VcpuTotal: 60, + RamTotalGB: 90, + }, + } + + chart := buildVcenterChart(entries) + if chart.ConfigJSON == "" { + t.Fatal("expected config json for non-empty vcenter chart") + } + + var cfg lineChartConfig + if err := json.Unmarshal([]byte(chart.ConfigJSON), &cfg); err != nil { + t.Fatalf("failed to decode chart config json: %v", err) + } + + if len(cfg.Labels) != 2 { + t.Fatalf("expected 2 labels, got %d", len(cfg.Labels)) + } + expectedFirst := time.Unix(1_000, 0).Local().Format("2006-01-02 15:04:05") + if cfg.Labels[0] != expectedFirst { + t.Fatalf("expected oldest label first %q, got %q", expectedFirst, cfg.Labels[0]) + } + if len(cfg.Series) != 3 { + t.Fatalf("expected 3 series, got %d", len(cfg.Series)) + } + if cfg.Series[0].Values[0] != 20 { + t.Fatalf("expected first VM value 20, got %v", cfg.Series[0].Values[0]) + } +} + +func TestBuildVmTraceChartEncodesPoolState(t *testing.T) { + entries := []views.VmTraceEntry{ + { + RawTime: 1_000, + ResourcePool: "Tin", + VcpuCount: 4, + RamGB: 16, + }, + { + RawTime: 2_000, + ResourcePool: "Gold", + VcpuCount: 8, + RamGB: 24, + }, + } + + chart := buildVmTraceChart(entries) + if chart.ConfigJSON == "" { + t.Fatal("expected config json for non-empty vm trace chart") + } + + var cfg lineChartConfig + if err := json.Unmarshal([]byte(chart.ConfigJSON), &cfg); err != nil { + t.Fatalf("failed to decode vm trace chart config: %v", err) + } + + if len(cfg.Series) != 6 { + t.Fatalf("expected 6 series, got %d", len(cfg.Series)) + } + if len(cfg.HoverRows) != 1 || cfg.HoverRows[0].Name != "Resource Pool" { + t.Fatalf("expected resource pool hover row, got %#v", cfg.HoverRows) + } + if cfg.HoverRows[0].Values[0] != "Tin" || cfg.HoverRows[0].Values[1] != "Gold" { + t.Fatalf("unexpected hover row values: %#v", cfg.HoverRows[0].Values) + } + if cfg.Series[2].Values[0] == 0 || cfg.Series[2].Values[1] != 0 { + t.Fatalf("tin series should be active only for first point: %#v", cfg.Series[2].Values) + } + if cfg.Series[5].Values[0] != 0 || cfg.Series[5].Values[1] == 0 { + t.Fatalf("gold series should be active only for second point: %#v", cfg.Series[5].Values) + } +} + +func TestBuildChartsEmptyInput(t *testing.T) { + if chart := buildVcenterChart(nil); chart.ConfigJSON != "" { + t.Fatalf("expected empty config for empty vcenter input, got %q", chart.ConfigJSON) + } + if chart := buildVmTraceChart(nil); chart.ConfigJSON != "" { + t.Fatalf("expected empty config for empty vm trace input, got %q", chart.ConfigJSON) + } +} diff --git a/server/handler/chart_config.go b/server/handler/chart_config.go new file mode 100644 index 0000000..110087c --- /dev/null +++ b/server/handler/chart_config.go @@ -0,0 +1,41 @@ +package handler + +import "encoding/json" + +type lineChartConfig struct { + Height int `json:"height,omitempty"` + XTicks int `json:"xTicks,omitempty"` + YTicks int `json:"yTicks,omitempty"` + YLabel string `json:"yLabel,omitempty"` + XLabel string `json:"xLabel,omitempty"` + Labels []string `json:"labels"` + TickLabels []string `json:"tickLabels,omitempty"` + Series []lineChartSeries `json:"series"` + HoverRows []lineChartHoverRow `json:"hoverRows,omitempty"` +} + +type lineChartSeries struct { + Name string `json:"name"` + Color string `json:"color"` + Values []float64 `json:"values"` + Dash []float64 `json:"dash,omitempty"` + LineWidth float64 `json:"lineWidth,omitempty"` + TooltipFormat string `json:"tooltipFormat,omitempty"` + TooltipHidden bool `json:"tooltipHidden,omitempty"` +} + +type lineChartHoverRow struct { + Name string `json:"name"` + Values []string `json:"values"` +} + +func encodeLineChartConfig(cfg lineChartConfig) string { + if len(cfg.Labels) == 0 || len(cfg.Series) == 0 { + return "" + } + out, err := json.Marshal(cfg) + if err != nil { + return "" + } + return string(out) +} diff --git a/server/handler/vcenters.go b/server/handler/vcenters.go index ad5ab78..c48f01f 100644 --- a/server/handler/vcenters.go +++ b/server/handler/vcenters.go @@ -141,91 +141,54 @@ func buildVcenterChart(entries []views.VcenterTotalsEntry) views.VcenterChartDat plot = append(plot, entries[i]) } - width := 1200.0 - height := 260.0 - plotWidth := width - 60.0 - startX := 40.0 - maxVal := float64(0) + labels := make([]string, 0, len(plot)) + tickLabels := make([]string, 0, len(plot)) + vmValues := make([]float64, 0, len(plot)) + vcpuValues := make([]float64, 0, len(plot)) + ramValues := make([]float64, 0, len(plot)) + for _, e := range plot { - if float64(e.VmCount) > maxVal { - maxVal = float64(e.VmCount) - } - if float64(e.VcpuTotal) > maxVal { - maxVal = float64(e.VcpuTotal) - } - if float64(e.RamTotalGB) > maxVal { - maxVal = float64(e.RamTotalGB) - } + t := time.Unix(e.RawTime, 0).Local() + labels = append(labels, t.Format("2006-01-02 15:04:05")) + tickLabels = append(tickLabels, t.Format("01-02 15:04")) + vmValues = append(vmValues, float64(e.VmCount)) + vcpuValues = append(vcpuValues, float64(e.VcpuTotal)) + ramValues = append(ramValues, float64(e.RamTotalGB)) } - if maxVal == 0 { - maxVal = 1 - } - stepX := plotWidth - if len(plot) > 1 { - stepX = plotWidth / float64(len(plot)-1) - } - pointsVm := "" - pointsVcpu := "" - pointsRam := "" - for i, e := range plot { - x := startX + float64(i)*stepX - yVm := 10 + (1-(float64(e.VmCount)/maxVal))*height - yVcpu := 10 + (1-(float64(e.VcpuTotal)/maxVal))*height - yRam := 10 + (1-(float64(e.RamTotalGB)/maxVal))*height - if i == 0 { - pointsVm = fmt.Sprintf("%.1f,%.1f", x, yVm) - pointsVcpu = fmt.Sprintf("%.1f,%.1f", x, yVcpu) - pointsRam = fmt.Sprintf("%.1f,%.1f", x, yRam) - } else { - pointsVm = pointsVm + " " + fmt.Sprintf("%.1f,%.1f", x, yVm) - pointsVcpu = pointsVcpu + " " + fmt.Sprintf("%.1f,%.1f", x, yVcpu) - pointsRam = pointsRam + " " + fmt.Sprintf("%.1f,%.1f", x, yRam) - } - } - gridX := []float64{} - if len(plot) > 1 { - for i := 0; i < len(plot); i++ { - gridX = append(gridX, startX+float64(i)*stepX) - } - } - gridY := []float64{} - for i := 0; i <= 4; i++ { - gridY = append(gridY, 10+float64(i)*(height/4)) - } - yTicks := []views.ChartTick{} - for i := 0; i <= 4; i++ { - val := maxVal * float64(4-i) / 4 - pos := 10 + float64(i)*(height/4) - yTicks = append(yTicks, views.ChartTick{Pos: pos, Label: fmt.Sprintf("%.0f", val)}) - } - xTicks := []views.ChartTick{} - maxTicks := 6 - stepIdx := 1 - if len(plot) > 1 { - stepIdx = (len(plot)-1)/maxTicks + 1 - } - for idx := 0; idx < len(plot); idx += stepIdx { - x := startX + float64(idx)*stepX - label := time.Unix(plot[idx].RawTime, 0).Local().Format("01-02 15:04") - xTicks = append(xTicks, views.ChartTick{Pos: x, Label: label}) - } - if len(plot) > 1 { - lastIdx := len(plot) - 1 - xLast := startX + float64(lastIdx)*stepX - labelLast := time.Unix(plot[lastIdx].RawTime, 0).Local().Format("01-02 15:04") - if len(xTicks) == 0 || xTicks[len(xTicks)-1].Pos != xLast { - xTicks = append(xTicks, views.ChartTick{Pos: xLast, Label: labelLast}) - } + + cfg := lineChartConfig{ + Height: 360, + XTicks: 6, + YTicks: 5, + YLabel: "Totals", + XLabel: "Snapshots (oldest left, newest right)", + Labels: labels, + TickLabels: tickLabels, + Series: []lineChartSeries{ + { + Name: "VMs", + Color: "#2563eb", + Values: vmValues, + TooltipFormat: "int", + LineWidth: 2.5, + }, + { + Name: "vCPU", + Color: "#16a34a", + Values: vcpuValues, + TooltipFormat: "int", + LineWidth: 2.5, + }, + { + Name: "RAM (GB)", + Color: "#ea580c", + Values: ramValues, + TooltipFormat: "int", + LineWidth: 2.5, + }, + }, } return views.VcenterChartData{ - PointsVm: pointsVm, - PointsVcpu: pointsVcpu, - PointsRam: pointsRam, - Width: int(width), - Height: int(height), - GridX: gridX, - GridY: gridY, - YTicks: yTicks, - XTicks: xTicks, + ConfigJSON: encodeLineChartConfig(cfg), } } diff --git a/server/handler/vmTrace.go b/server/handler/vmTrace.go index ca53858..eb5d11c 100644 --- a/server/handler/vmTrace.go +++ b/server/handler/vmTrace.go @@ -108,114 +108,135 @@ func buildVmTraceChart(entries []views.VmTraceEntry) views.VmTraceChart { if len(entries) == 0 { return views.VmTraceChart{} } - width := 1200.0 - height := 220.0 - plotWidth := width - 60.0 - startX := 40.0 - maxVal := float64(0) + maxResource := float64(0) for _, e := range entries { - if float64(e.VcpuCount) > maxVal { - maxVal = float64(e.VcpuCount) + if float64(e.VcpuCount) > maxResource { + maxResource = float64(e.VcpuCount) } - if float64(e.RamGB) > maxVal { - maxVal = float64(e.RamGB) + if float64(e.RamGB) > maxResource { + maxResource = float64(e.RamGB) } } - if maxVal == 0 { - maxVal = 1 + if maxResource == 0 { + maxResource = 1 } - stepX := plotWidth - if len(entries) > 1 { - stepX = plotWidth / float64(len(entries)-1) - } - scale := height / maxVal - var ptsVcpu, ptsRam, ptsTin, ptsBronze, ptsSilver, ptsGold string - appendPt := func(s string, x, y float64) string { - if s == "" { - return fmt.Sprintf("%.1f,%.1f", x, y) + + tinLevel := maxResource + bronzeLevel := maxResource * 0.9 + silverLevel := maxResource * 0.8 + goldLevel := maxResource * 0.7 + + labels := make([]string, 0, len(entries)) + tickLabels := make([]string, 0, len(entries)) + vcpuValues := make([]float64, 0, len(entries)) + ramValues := make([]float64, 0, len(entries)) + tinValues := make([]float64, 0, len(entries)) + bronzeValues := make([]float64, 0, len(entries)) + silverValues := make([]float64, 0, len(entries)) + goldValues := make([]float64, 0, len(entries)) + poolNames := make([]string, 0, len(entries)) + + for _, e := range entries { + t := time.Unix(e.RawTime, 0).Local() + labels = append(labels, t.Format("2006-01-02 15:04:05")) + tickLabels = append(tickLabels, t.Format("01-02 15:04")) + vcpuValues = append(vcpuValues, float64(e.VcpuCount)) + ramValues = append(ramValues, float64(e.RamGB)) + + pool := strings.TrimSpace(e.ResourcePool) + if pool == "" { + pool = "Unknown" } - return s + " " + fmt.Sprintf("%.1f,%.1f", x, y) - } - for i, e := range entries { - x := startX + float64(i)*stepX - yVcpu := 10 + height - float64(e.VcpuCount)*scale - yRam := 10 + height - float64(e.RamGB)*scale - ptsVcpu = appendPt(ptsVcpu, x, yVcpu) - ptsRam = appendPt(ptsRam, x, yRam) - poolY := map[string]float64{ - "tin": 10 + height - scale*maxVal, - "bronze": 10 + height - scale*maxVal*0.9, - "silver": 10 + height - scale*maxVal*0.8, - "gold": 10 + height - scale*maxVal*0.7, - } - lower := strings.ToLower(e.ResourcePool) + poolNames = append(poolNames, pool) + + lower := strings.ToLower(pool) if lower == "tin" { - ptsTin = appendPt(ptsTin, x, poolY["tin"]) + tinValues = append(tinValues, tinLevel) } else { - ptsTin = appendPt(ptsTin, x, 10+height) + tinValues = append(tinValues, 0) } if lower == "bronze" { - ptsBronze = appendPt(ptsBronze, x, poolY["bronze"]) + bronzeValues = append(bronzeValues, bronzeLevel) } else { - ptsBronze = appendPt(ptsBronze, x, 10+height) + bronzeValues = append(bronzeValues, 0) } if lower == "silver" { - ptsSilver = appendPt(ptsSilver, x, poolY["silver"]) + silverValues = append(silverValues, silverLevel) } else { - ptsSilver = appendPt(ptsSilver, x, 10+height) + silverValues = append(silverValues, 0) } if lower == "gold" { - ptsGold = appendPt(ptsGold, x, poolY["gold"]) + goldValues = append(goldValues, goldLevel) } else { - ptsGold = appendPt(ptsGold, x, 10+height) + goldValues = append(goldValues, 0) } } - gridY := []float64{} - for i := 0; i <= 4; i++ { - gridY = append(gridY, 10+float64(i)*(height/4)) - } - gridX := []float64{} - for i := 0; i < len(entries); i++ { - gridX = append(gridX, startX+float64(i)*stepX) - } - yTicks := []views.ChartTick{} - for i := 0; i <= 4; i++ { - val := maxVal * float64(4-i) / 4 - pos := 10 + float64(i)*(height/4) - yTicks = append(yTicks, views.ChartTick{Pos: pos, Label: fmt.Sprintf("%.0f", val)}) - } - xTicks := []views.ChartTick{} - maxTicks := 8 - stepIdx := 1 - if len(entries) > 1 { - stepIdx = (len(entries)-1)/maxTicks + 1 - } - for idx := 0; idx < len(entries); idx += stepIdx { - x := startX + float64(idx)*stepX - label := time.Unix(entries[idx].RawTime, 0).Local().Format("01-02 15:04") - xTicks = append(xTicks, views.ChartTick{Pos: x, Label: label}) - } - if len(entries) > 1 { - lastIdx := len(entries) - 1 - xLast := startX + float64(lastIdx)*stepX - labelLast := time.Unix(entries[lastIdx].RawTime, 0).Local().Format("01-02 15:04") - if len(xTicks) == 0 || xTicks[len(xTicks)-1].Pos != xLast { - xTicks = append(xTicks, views.ChartTick{Pos: xLast, Label: labelLast}) - } + + cfg := lineChartConfig{ + Height: 360, + XTicks: 8, + YTicks: 5, + YLabel: "Resources / Pool", + XLabel: "Snapshots (oldest left, newest right)", + Labels: labels, + TickLabels: tickLabels, + Series: []lineChartSeries{ + { + Name: "vCPU", + Color: "#2563eb", + Values: vcpuValues, + TooltipFormat: "int", + LineWidth: 2.5, + }, + { + Name: "RAM (GB)", + Color: "#16a34a", + Values: ramValues, + TooltipFormat: "int", + LineWidth: 2.5, + }, + { + Name: "Tin", + Color: "#0ea5e9", + Values: tinValues, + Dash: []float64{4, 4}, + LineWidth: 1.5, + TooltipHidden: true, + }, + { + Name: "Bronze", + Color: "#a855f7", + Values: bronzeValues, + Dash: []float64{4, 4}, + LineWidth: 1.5, + TooltipHidden: true, + }, + { + Name: "Silver", + Color: "#94a3b8", + Values: silverValues, + Dash: []float64{4, 4}, + LineWidth: 1.5, + TooltipHidden: true, + }, + { + Name: "Gold", + Color: "#f59e0b", + Values: goldValues, + Dash: []float64{4, 4}, + LineWidth: 1.5, + TooltipHidden: true, + }, + }, + HoverRows: []lineChartHoverRow{ + { + Name: "Resource Pool", + Values: poolNames, + }, + }, } return views.VmTraceChart{ - PointsVcpu: ptsVcpu, - PointsRam: ptsRam, - PointsTin: ptsTin, - PointsBronze: ptsBronze, - PointsSilver: ptsSilver, - PointsGold: ptsGold, - Width: int(width), - Height: int(height), - GridX: gridX, - GridY: gridY, - XTicks: xTicks, - YTicks: yTicks, + ConfigJSON: encodeLineChartConfig(cfg), } }