use javascript chart instead of svg
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
101
server/handler/chart_builders_test.go
Normal file
101
server/handler/chart_builders_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
41
server/handler/chart_config.go
Normal file
41
server/handler/chart_config.go
Normal file
@@ -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)
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user