wip-devtools
This commit is contained in:
5 files changed
+188
No files matched your search
@@ -38,6 +38,11 @@ func init() {
|
||||
"service_status.counts": {"%d up · %d degraded · %d down", "%d 正常 · %d 降级 · %d 中断"},
|
||||
"service_status.empty": {"No services", "没有服务"},
|
||||
|
||||
"uptime_bar.ok": {"Operational", "正常"},
|
||||
"uptime_bar.degraded": {"Degraded", "降级"},
|
||||
"uptime_bar.down": {"Outage", "中断"},
|
||||
"uptime_bar.nodata": {"No data", "无数据"},
|
||||
|
||||
"alert_list.empty": {"No alerts", "没有告警"},
|
||||
})
|
||||
}
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
@@ -0,0 +1,96 @@
|
||||
package mujica
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"mujicaui/internal"
|
||||
)
|
||||
|
||||
// UptimeDay is one day's availability, 0 to 1; NaN means no data.
|
||||
type UptimeDay struct {
|
||||
Date Date
|
||||
Uptime float64
|
||||
// Note says what happened, as "12 min outage".
|
||||
Note string
|
||||
}
|
||||
|
||||
// UptimeBarOptions configure an UptimeBar; Frame.Height 0 means 36.
|
||||
type UptimeBarOptions struct {
|
||||
Frame ChartFrame
|
||||
// Degraded and Down are the uptime limits below which a day is marked; zero means 0.999 and 0.95.
|
||||
Degraded, Down float64
|
||||
}
|
||||
|
||||
// UptimeBar draws one bar per day by availability; hover or arrows read a day's detail.
|
||||
func UptimeBar(c *ui.Context, days []UptimeDay, opts UptimeBarOptions) ChartResult {
|
||||
deg, down := opts.Degraded, opts.Down
|
||||
if deg == 0 {
|
||||
deg = 0.999
|
||||
}
|
||||
if down == 0 {
|
||||
down = 0.95
|
||||
}
|
||||
if !(down < deg && deg <= 1 && down > 0) {
|
||||
panic("mujica: UptimeBar needs 0 < Down < Degraded <= 1")
|
||||
}
|
||||
for i, d := range days {
|
||||
if !math.IsNaN(d.Uptime) && (d.Uptime < 0 || d.Uptime > 1) {
|
||||
panic(fmt.Sprintf("mujica: UptimeBar day %d uptime %v outside 0..1", i, d.Uptime))
|
||||
}
|
||||
}
|
||||
if opts.Frame.Height == 0 {
|
||||
opts.Frame.Height = 36
|
||||
}
|
||||
k := Tokens(c)
|
||||
ch := newChart(c, opts.Frame)
|
||||
n := len(days)
|
||||
band := func(r ui.Rect) internal.Band { return internal.Band{N: n, R0: r.X, R1: r.X + r.W, Pad: 0.25} }
|
||||
level := func(v float64) int { // 0 ok, 1 degraded, 2 down, 3 no data
|
||||
switch {
|
||||
case math.IsNaN(v):
|
||||
return 3
|
||||
case v < down:
|
||||
return 2
|
||||
case v < deg:
|
||||
return 1
|
||||
}
|
||||
return 0
|
||||
}
|
||||
words := [...]string{msg(c, "uptime_bar.ok"), msg(c, "uptime_bar.degraded"), msg(c, "uptime_bar.down"), msg(c, "uptime_bar.nodata")}
|
||||
res, _ := ch.build(chartSpec{
|
||||
n: n,
|
||||
draw: func(p *ui.Painter, r ui.Rect, active int) {
|
||||
b := band(r)
|
||||
for i, d := range days {
|
||||
x, w := internal.Snap(p, b.Start(i)), max(b.Width(), 1)
|
||||
lv := level(d.Uptime)
|
||||
col := [...]ui.Color{k.Success, k.Warning, k.Danger, k.Border}[lv]
|
||||
// Shorter bars mark bad days, so color is not the only cue.
|
||||
h := r.H * [...]float32{1, 0.75, 0.5, 1}[lv]
|
||||
p.Fill(ui.Rect{X: x, Y: r.Y + r.H - h, W: w, H: h}, col, 1)
|
||||
if i == active {
|
||||
p.Stroke(ui.Rect{X: x - 2, Y: r.Y - 2, W: w + 4, H: r.H + 4}, k.Focus, 2, 1.5)
|
||||
}
|
||||
}
|
||||
},
|
||||
pick: func(r ui.Rect, x, _ float32) int {
|
||||
b := band(r)
|
||||
return max(0, min(n-1, int((x-r.X)/b.Step())))
|
||||
},
|
||||
readout: func(i int) string {
|
||||
d := days[i]
|
||||
s := fmt.Sprintf("%s · %s", d.Date.String(), words[level(d.Uptime)])
|
||||
if !math.IsNaN(d.Uptime) {
|
||||
s += fmt.Sprintf(" · %.2f%%", d.Uptime*100)
|
||||
}
|
||||
if d.Note != "" {
|
||||
s += " · " + d.Note
|
||||
}
|
||||
return s
|
||||
},
|
||||
})
|
||||
return res
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package mujica
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
)
|
||||
|
||||
func uptimeSample(n int) []UptimeDay {
|
||||
days := make([]UptimeDay, n)
|
||||
start := Date{Year: 2026, Month: 9, Day: 1}
|
||||
for i := range days {
|
||||
days[i] = UptimeDay{Date: start.AddDays(i), Uptime: 1}
|
||||
}
|
||||
days[3] = UptimeDay{Date: start.AddDays(3), Uptime: 0.9975, Note: "slow search"}
|
||||
days[7] = UptimeDay{Date: start.AddDays(7), Uptime: 0.91, Note: "2 h outage"}
|
||||
days[9].Uptime = math.NaN()
|
||||
return days
|
||||
}
|
||||
|
||||
func uptimeTester(days []UptimeDay, frame ChartFrame, points *[]int) *ui.Tester {
|
||||
if frame.Label == "" {
|
||||
frame.Label = "API uptime"
|
||||
}
|
||||
return ui.NewTester(func(c *ui.Context) {
|
||||
Use(c, Settings{})
|
||||
ui.Column(c).Padding(10).Children(func() {
|
||||
r := UptimeBar(c, days, UptimeBarOptions{Frame: frame})
|
||||
*points = append(*points, r.Point)
|
||||
})
|
||||
}, 520, 140)
|
||||
}
|
||||
|
||||
func TestUptimeBarKeyboardReadout(t *testing.T) {
|
||||
var pts []int
|
||||
tt := uptimeTester(uptimeSample(30), ChartFrame{}, &pts)
|
||||
tt.Key(0, ui.KeyTab)
|
||||
tt.Key(0, ui.KeyRight)
|
||||
for range 7 {
|
||||
tt.Key(0, ui.KeyRight)
|
||||
}
|
||||
if !tt.HasText("2026-09-08 · Outage · 91.00% · 2 h outage") {
|
||||
t.Fatalf("readout: %q", tt.Texts())
|
||||
}
|
||||
tt.Key(0, ui.KeyRight)
|
||||
tt.Key(0, ui.KeyRight)
|
||||
if !tt.HasText("2026-09-10 · No data") {
|
||||
t.Fatalf("no data readout: %q", tt.Texts())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUptimeBarHover(t *testing.T) {
|
||||
var pts []int
|
||||
tt := uptimeTester(uptimeSample(30), ChartFrame{}, &pts)
|
||||
// The 4th of 30 bars across 500 DIPs starting at x = 10.
|
||||
tt.Move(10+500*3.5/30, 30)
|
||||
if pts[len(pts)-1] != 3 || !tt.HasText("2026-09-04 · Degraded · 99.75% · slow search") {
|
||||
t.Fatalf("hover point %d: %q", pts[len(pts)-1], tt.Texts())
|
||||
}
|
||||
}
|
||||
|
||||
func TestUptimeBarStatesAndRules(t *testing.T) {
|
||||
var pts []int
|
||||
tt := uptimeTester(nil, ChartFrame{}, &pts)
|
||||
if !tt.HasText("No data") {
|
||||
t.Fatalf("empty: %q", tt.Texts())
|
||||
}
|
||||
tt = uptimeTester(uptimeSample(12), ChartFrame{Status: DataLoading}, &pts)
|
||||
if !tt.HasText("Loading") {
|
||||
t.Fatalf("loading: %q", tt.Texts())
|
||||
}
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
t.Fatal("uptime above 1 did not panic")
|
||||
}
|
||||
}()
|
||||
uptimeTester([]UptimeDay{{Uptime: 1.2}}, ChartFrame{}, &pts)
|
||||
}
|
||||
|
||||
func TestUptimeBarSnapshot(t *testing.T) {
|
||||
snapshot(t, "uptime_bar", 520, 110, func(c *ui.Context) {
|
||||
ui.Column(c).Padding(10).Children(func() {
|
||||
UptimeBar(c, uptimeSample(45), UptimeBarOptions{Frame: ChartFrame{Label: "API uptime"}})
|
||||
})
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user