diff --git a/messages_devtools.go b/messages_devtools.go index bd12df1..ca629ba 100644 --- a/messages_devtools.go +++ b/messages_devtools.go @@ -43,6 +43,10 @@ func init() { "uptime_bar.down": {"Outage", "中断"}, "uptime_bar.nodata": {"No data", "无数据"}, + "resource_gauge.normal": {"Normal", "正常"}, + "resource_gauge.high": {"High", "偏高"}, + "resource_gauge.critical": {"Critical", "危险"}, + "alert_list.empty": {"No alerts", "没有告警"}, }) } diff --git a/resource_gauge.go b/resource_gauge.go new file mode 100644 index 0000000..14a6f54 --- /dev/null +++ b/resource_gauge.go @@ -0,0 +1,69 @@ +package mujica + +import ( + "fmt" + "math" + + "github.com/egoist/mygo/ui" +) + +// ResourceReading is one resource's current use. +type ResourceReading struct { + // Name is required, as "CPU". + Name string + Used, Total float64 + // Unit follows the amounts, as "GB"; empty shows percent only. + Unit string +} + +// ResourceGaugeOptions configure a ResourceGauge; thresholds are fractions of Total. +type ResourceGaugeOptions struct { + // Warn and Critical default to 0.7 and 0.9. + Warn, Critical float64 + // Height of each dial; zero means 150. + Height float32 +} + +// ResourceGauge shows CPU, memory and disk dials with named normal, high and critical bands. +func ResourceGauge(c *ui.Context, readings []ResourceReading, opts ResourceGaugeOptions) *ui.Element { + warn, crit := opts.Warn, opts.Critical + if warn == 0 { + warn = 0.7 + } + if crit == 0 { + crit = 0.9 + } + if !(0 < warn && warn < crit && crit < 1) || opts.Height < 0 { + panic("mujica: ResourceGauge needs 0 < Warn < Critical < 1 and Height >= 0") + } + h := opts.Height + if h == 0 { + h = 150 + } + u := Density(c).Unit() + row := ui.Row(c).Wrap().Gap(u * 4).Shrink(0) + row.Children(func() { + for i, r := range readings { + if r.Name == "" || !(r.Total > 0) || r.Used < 0 || r.Used > r.Total || math.IsNaN(r.Used) { + panic(fmt.Sprintf("mujica: ResourceGauge reading %d needs a Name and 0 <= Used <= Total", i)) + } + frac := r.Used / r.Total + ui.Column(c).Key(r.Name).Width(200).Gap(u).AlignItems(ui.Center).Children(func() { + ui.Text(c, r.Name).FontWeight(600) + Gauge(c, frac*100, GaugeOptions{ + Frame: ChartFrame{Label: r.Name, Height: h}, Min: 0, Max: 100, Unit: "%", + Format: func(v float64) string { return fmt.Sprintf("%.0f", v) }, + Segments: []GaugeSegment{ + {To: warn * 100, Label: msg(c, "resource_gauge.normal"), Tone: GaugeGood}, + {To: crit * 100, Label: msg(c, "resource_gauge.high"), Tone: GaugeWarning}, + {To: 100, Label: msg(c, "resource_gauge.critical"), Tone: GaugeDanger}, + }, + }) + if r.Unit != "" { + agentCaption(c, fmt.Sprintf("%s / %s %s", sandboxNum(r.Used), sandboxNum(r.Total), r.Unit)) + } + }) + } + }) + return row +} diff --git a/resource_gauge_test.go b/resource_gauge_test.go new file mode 100644 index 0000000..bc03728 --- /dev/null +++ b/resource_gauge_test.go @@ -0,0 +1,56 @@ +package mujica + +import ( + "testing" + + "github.com/egoist/mygo/ui" +) + +var resourceSample = []ResourceReading{ + {Name: "CPU", Used: 45, Total: 100}, + {Name: "Memory", Used: 6.4, Total: 8, Unit: "GB"}, + {Name: "Disk", Used: 470, Total: 500, Unit: "GB"}, +} + +func TestResourceGaugeThresholdsAndKeyboard(t *testing.T) { + tt := ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ResourceGauge(c, resourceSample, ResourceGaugeOptions{}) + }, 700, 300) + if !tt.HasText("6.4 / 8 GB") || !tt.HasText("470 / 500 GB") { + t.Fatalf("texts %q", tt.Texts()) + } + tt.Key(0, ui.KeyTab) // CPU dial + tt.Key(0, ui.KeyRight) + if !tt.HasText("45 % · Normal") { + t.Fatalf("CPU readout: %q", tt.Texts()) + } + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyRight) + if !tt.HasText("80 % · High") { + t.Fatalf("memory readout: %q", tt.Texts()) + } + tt.Key(0, ui.KeyTab) + tt.Key(0, ui.KeyRight) + if !tt.HasText("94 % · Critical") { + t.Fatalf("disk readout: %q", tt.Texts()) + } +} + +func TestResourceGaugeRejectsBadReading(t *testing.T) { + defer func() { + if recover() == nil { + t.Fatal("no panic") + } + }() + ui.NewTester(func(c *ui.Context) { + Use(c, Settings{}) + ResourceGauge(c, []ResourceReading{{Name: "CPU", Used: 5, Total: 4}}, ResourceGaugeOptions{}) + }, 200, 200) +} + +func TestResourceGaugeSnapshot(t *testing.T) { + snapshot(t, "resource_gauge", 680, 250, func(c *ui.Context) { + ui.Column(c).Padding(16).Children(func() { ResourceGauge(c, resourceSample, ResourceGaugeOptions{}) }) + }) +} diff --git a/testdata/snapshots/darwin/resource_gauge-dark.png b/testdata/snapshots/darwin/resource_gauge-dark.png new file mode 100644 index 0000000..1996758 Binary files /dev/null and b/testdata/snapshots/darwin/resource_gauge-dark.png differ diff --git a/testdata/snapshots/darwin/resource_gauge-light.png b/testdata/snapshots/darwin/resource_gauge-light.png new file mode 100644 index 0000000..0bad7e2 Binary files /dev/null and b/testdata/snapshots/darwin/resource_gauge-light.png differ