wip-devtools
This commit is contained in:
6 files changed
+182
-2
No files matched your search
@@ -0,0 +1,95 @@
|
||||
package mujica
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"mujicaui/icons"
|
||||
)
|
||||
|
||||
// DashboardFilters is what a dashboard is filtered by; the zero value filters nothing.
|
||||
type DashboardFilters struct {
|
||||
// Range is how far back to look; zero means the caller's default.
|
||||
Range time.Duration
|
||||
Env string
|
||||
Tags []string
|
||||
}
|
||||
|
||||
// DashboardFilterBarOptions configure a DashboardFilterBar.
|
||||
type DashboardFilterBarOptions struct {
|
||||
// Ranges are the offered look-backs; nil uses 15 min, 1 h, 24 h and 7 d.
|
||||
Ranges []time.Duration
|
||||
// Envs and Tags are the offered environments and tags.
|
||||
Envs, Tags []string
|
||||
Disabled bool
|
||||
}
|
||||
|
||||
var dashboardRanges = []time.Duration{15 * time.Minute, time.Hour, 24 * time.Hour, 7 * 24 * time.Hour}
|
||||
|
||||
// DashboardFilterBar filters by time range, environment and tags, with a clear button.
|
||||
func DashboardFilterBar(c *ui.Context, f *DashboardFilters, opts DashboardFilterBarOptions) ChoiceResult {
|
||||
if f == nil {
|
||||
panic("mujica: DashboardFilterBar needs filters")
|
||||
}
|
||||
ranges := opts.Ranges
|
||||
if ranges == nil {
|
||||
ranges = dashboardRanges
|
||||
}
|
||||
if f.Env != "" && !slices.Contains(opts.Envs, f.Env) {
|
||||
panic("mujica: DashboardFilterBar Env is not offered")
|
||||
}
|
||||
u := Density(c).Unit()
|
||||
var r ChoiceResult
|
||||
r.Element = ui.Row(c).Shrink(0).Wrap().Gap(u * 3).AlignItems(ui.Center).Disabled(opts.Disabled).
|
||||
Label(msg(c, "dashboard_filter_bar.label"))
|
||||
locked := r.Element.IsDisabled()
|
||||
r.Element.Children(func() {
|
||||
rs := []SelectOption[time.Duration]{{Value: 0, Label: msg(c, "dashboard_filter_bar.default_range")}}
|
||||
for _, d := range ranges {
|
||||
rs = append(rs, SelectOption[time.Duration]{Value: d, Label: fmt.Sprintf(msg(c, "dashboard_filter_bar.last"), dashboardRangeLabel(c, d))})
|
||||
}
|
||||
if f.Range != 0 && !slices.Contains(ranges, f.Range) {
|
||||
panic("mujica: DashboardFilterBar Range is not offered")
|
||||
}
|
||||
r.changed = Select(c, &f.Range, rs, SelectOptions{Label: msg(c, "dashboard_filter_bar.range"), Disabled: locked}).Changed()
|
||||
envs := []SelectOption[string]{{Value: "", Label: msg(c, "dashboard_filter_bar.all_envs")}}
|
||||
for _, e := range opts.Envs {
|
||||
envs = append(envs, SelectOption[string]{Value: e, Label: e})
|
||||
}
|
||||
r.changed = Select(c, &f.Env, envs, SelectOptions{Label: msg(c, "dashboard_filter_bar.env"), Disabled: locked}).Changed() || r.changed
|
||||
if len(opts.Tags) > 0 {
|
||||
tags := make([]SelectOption[string], len(opts.Tags))
|
||||
for i, t := range opts.Tags {
|
||||
tags[i] = SelectOption[string]{Value: t, Label: t}
|
||||
}
|
||||
r.changed = MultiSelect(c, &f.Tags, tags, MultiSelectOptions{Placeholder: msg(c, "dashboard_filter_bar.tags"),
|
||||
Label: msg(c, "dashboard_filter_bar.tags"), Disabled: locked}).Changed() || r.changed
|
||||
}
|
||||
empty := f.Range == 0 && f.Env == "" && len(f.Tags) == 0
|
||||
if Button(c, msg(c, "dashboard_filter_bar.clear"), ButtonOptions{Variant: Ghost, Icon: icons.Must("x"), Disabled: locked || empty}).Clicked() && !locked {
|
||||
*f, r.changed = DashboardFilters{}, true
|
||||
}
|
||||
})
|
||||
if r.changed {
|
||||
slog.Debug("mujica: dashboard_filter_bar change", "range", f.Range, "env", f.Env, "tags", len(f.Tags))
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
// dashboardRangeLabel names a look-back, as "15 min" or "7 d".
|
||||
func dashboardRangeLabel(c *ui.Context, d time.Duration) string {
|
||||
switch {
|
||||
case d <= 0:
|
||||
case d%(24*time.Hour) == 0:
|
||||
return fmt.Sprintf(msg(c, "dashboard_filter_bar.days"), int(d/(24*time.Hour)))
|
||||
case d%time.Hour == 0:
|
||||
return fmt.Sprintf(msg(c, "dashboard_filter_bar.hours"), int(d/time.Hour))
|
||||
case d%time.Minute == 0:
|
||||
return fmt.Sprintf(msg(c, "dashboard_filter_bar.minutes"), int(d/time.Minute))
|
||||
}
|
||||
panic("mujica: DashboardFilterBar ranges must be positive whole minutes")
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package mujica
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
)
|
||||
|
||||
func filterTester(f *DashboardFilters, opts DashboardFilterBarOptions, changes *int) *ui.Tester {
|
||||
return ui.NewTester(func(c *ui.Context) {
|
||||
Use(c, Settings{})
|
||||
ui.Column(c).Padding(8).Children(func() {
|
||||
if DashboardFilterBar(c, f, opts).Changed() {
|
||||
*changes++
|
||||
}
|
||||
})
|
||||
}, 900, 420)
|
||||
}
|
||||
|
||||
var filterOpts = DashboardFilterBarOptions{Envs: []string{"production", "staging"}, Tags: []string{"api", "web", "db"}}
|
||||
|
||||
func TestDashboardFilterBarChooseAndClear(t *testing.T) {
|
||||
var f DashboardFilters
|
||||
n := 0
|
||||
tt := filterTester(&f, filterOpts, &n)
|
||||
_ = tt.Click("Time range")
|
||||
_ = tt.Click("Last 1 d")
|
||||
if f.Range != 24*time.Hour || n != 1 {
|
||||
t.Fatalf("range %v changes %d", f.Range, n)
|
||||
}
|
||||
_ = tt.Click("Environment")
|
||||
_ = tt.Click("staging")
|
||||
if f.Env != "staging" {
|
||||
t.Fatalf("env %q", f.Env)
|
||||
}
|
||||
_ = tt.Click("Tags")
|
||||
_ = tt.Click("db")
|
||||
if len(f.Tags) != 1 || f.Tags[0] != "db" {
|
||||
t.Fatalf("tags %v", f.Tags)
|
||||
}
|
||||
tt.Key(0, ui.KeyEscape)
|
||||
_ = tt.Click("Clear filters")
|
||||
if f.Range != 0 || f.Env != "" || len(f.Tags) != 0 || !tt.HasText("All environments") {
|
||||
t.Fatalf("clear: %+v", f)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDashboardFilterBarDisabledAndRules(t *testing.T) {
|
||||
f := DashboardFilters{Env: "production"}
|
||||
n := 0
|
||||
tt := filterTester(&f, DashboardFilterBarOptions{Envs: filterOpts.Envs, Disabled: true}, &n)
|
||||
_ = tt.Click("Clear filters")
|
||||
_ = tt.Click("Time range")
|
||||
_ = tt.Click("Last 1 h")
|
||||
if n != 0 || f.Env != "production" || f.Range != 0 {
|
||||
t.Fatalf("disabled changed: %+v", f)
|
||||
}
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
t.Fatal("unoffered env did not panic")
|
||||
}
|
||||
}()
|
||||
bad := DashboardFilters{Env: "moon"}
|
||||
filterTester(&bad, filterOpts, &n)
|
||||
}
|
||||
|
||||
func TestDashboardFilterBarSnapshot(t *testing.T) {
|
||||
snapshot(t, "dashboard_filter_bar", 860, 90, func(c *ui.Context) {
|
||||
f := DashboardFilters{Range: time.Hour, Env: "production", Tags: []string{"api"}}
|
||||
ui.Column(c).Padding(16).Children(func() { DashboardFilterBar(c, &f, filterOpts) })
|
||||
})
|
||||
}
|
||||
@@ -54,6 +54,18 @@ func init() {
|
||||
"refresh_interval_selector.minutes": {"Every %d min", "每 %d 分钟"},
|
||||
"refresh_interval_selector.label": {"Refresh interval", "刷新间隔"},
|
||||
|
||||
"dashboard_filter_bar.label": {"Dashboard filters", "仪表盘过滤"},
|
||||
"dashboard_filter_bar.range": {"Time range", "时间范围"},
|
||||
"dashboard_filter_bar.default_range": {"Default range", "默认范围"},
|
||||
"dashboard_filter_bar.last": {"Last %s", "最近 %s"},
|
||||
"dashboard_filter_bar.minutes": {"%d min", "%d 分钟"},
|
||||
"dashboard_filter_bar.hours": {"%d h", "%d 小时"},
|
||||
"dashboard_filter_bar.days": {"%d d", "%d 天"},
|
||||
"dashboard_filter_bar.env": {"Environment", "环境"},
|
||||
"dashboard_filter_bar.all_envs": {"All environments", "全部环境"},
|
||||
"dashboard_filter_bar.tags": {"Tags", "标签"},
|
||||
"dashboard_filter_bar.clear": {"Clear filters", "清除过滤"},
|
||||
|
||||
"alert_list.empty": {"No alerts", "没有告警"},
|
||||
})
|
||||
}
|
||||
@@ -44,8 +44,8 @@ func RefreshIntervalSelector(c *ui.Context, interval *time.Duration, opts Refres
|
||||
options := []SelectOption[time.Duration]{{Value: 0, Label: msg(c, "refresh_interval_selector.off")}}
|
||||
known := *interval == 0
|
||||
for _, d := range choices {
|
||||
if d < time.Second {
|
||||
panic("mujica: RefreshIntervalSelector intervals must be at least a second")
|
||||
if d < time.Second || d%time.Second != 0 {
|
||||
panic("mujica: RefreshIntervalSelector intervals must be whole seconds")
|
||||
}
|
||||
known = known || d == *interval
|
||||
label := fmt.Sprintf(msg(c, "refresh_interval_selector.seconds"), int(d/time.Second))
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 6.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
Reference in new issue
Block a user