2

Refine the dashboard and requests log UX

This commit is contained in:
2026-03-12 17:40:13 +01:00
parent 0a14dd1df9
commit 0dfa30973e
8 changed files with 423 additions and 180 deletions

View File

@@ -52,17 +52,24 @@ func TestHandlerServesOverviewAndManualActions(t *testing.T) {
if recorder.Code != http.StatusOK {
t.Fatalf("unexpected filtered overview status: %d", recorder.Code)
}
if app.lastOverviewOptions.ShowKnownBots || app.lastOverviewOptions.ShowAllowed {
t.Fatalf("overview filter options were not forwarded correctly: %+v", app.lastOverviewOptions)
if !app.lastOverviewOptions.ShowKnownBots || !app.lastOverviewOptions.ShowAllowed {
t.Fatalf("overview should always use the unfiltered dashboard data: %+v", app.lastOverviewOptions)
}
recorder = httptest.NewRecorder()
request = httptest.NewRequest(http.MethodGet, "/api/events?hours=24&limit=250&show_known_bots=false&show_allowed=false&review_only=true", nil)
request = httptest.NewRequest(http.MethodGet, "/api/events?hours=24&limit=250&page=2&show_known_bots=false&show_allowed=false&review_only=true", nil)
handler.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("unexpected filtered events status: %d", recorder.Code)
}
if app.lastEventOptions.ShowKnownBots || app.lastEventOptions.ShowAllowed || !app.lastEventOptions.ReviewOnly {
var eventPage model.EventPage
if err := json.Unmarshal(recorder.Body.Bytes(), &eventPage); err != nil {
t.Fatalf("decode event page payload: %v", err)
}
if eventPage.Page != 2 || !eventPage.HasPrev {
t.Fatalf("unexpected event page payload: %+v", eventPage)
}
if app.lastEventOptions.ShowKnownBots || app.lastEventOptions.ShowAllowed || !app.lastEventOptions.ReviewOnly || app.lastEventOptions.Offset != 250 {
t.Fatalf("event filter options were not forwarded correctly: %+v", app.lastEventOptions)
}
@@ -94,11 +101,11 @@ func TestHandlerServesOverviewAndManualActions(t *testing.T) {
if !strings.Contains(recorder.Body.String(), "Local-only review and enforcement console") {
t.Fatalf("overview page did not render expected content")
}
if !strings.Contains(recorder.Body.String(), "Show known bots") {
t.Fatalf("overview page should expose the known bots toggle")
if strings.Contains(recorder.Body.String(), "Show known bots") {
t.Fatalf("overview page should no longer expose the known bots toggle")
}
if !strings.Contains(recorder.Body.String(), "Query Log") {
t.Fatalf("overview page should link to the query log")
if !strings.Contains(recorder.Body.String(), "Requests Log") {
t.Fatalf("overview page should link to the requests log")
}
if !strings.Contains(recorder.Body.String(), "Activity") {
t.Fatalf("overview page should expose the activity chart")
@@ -109,11 +116,11 @@ func TestHandlerServesOverviewAndManualActions(t *testing.T) {
if !strings.Contains(recorder.Body.String(), "Bots") {
t.Fatalf("overview page should expose the bots chart")
}
if !strings.Contains(recorder.Body.String(), "Top IPs by events") {
t.Fatalf("overview page should expose the top IPs by events block")
if !strings.Contains(recorder.Body.String(), "Top bot IPs by events") {
t.Fatalf("overview page should expose the top bot IPs by events block")
}
if !strings.Contains(recorder.Body.String(), "Top IPs by traffic") {
t.Fatalf("overview page should expose the top IPs by traffic block")
if !strings.Contains(recorder.Body.String(), "Top non-bot IPs by traffic") {
t.Fatalf("overview page should expose the split top IP traffic block")
}
if !strings.Contains(recorder.Body.String(), "Top sources by events") {
t.Fatalf("overview page should expose the top sources block")
@@ -124,37 +131,47 @@ func TestHandlerServesOverviewAndManualActions(t *testing.T) {
if !strings.Contains(recorder.Body.String(), "Loading…") {
t.Fatalf("overview page should render stable loading placeholders")
}
if !strings.Contains(recorder.Body.String(), "These filters affect all dashboard charts and top lists") {
t.Fatalf("overview page should explain the scope of the shared filters")
}
if strings.Contains(recorder.Body.String(), "position: sticky") {
t.Fatalf("overview page header should no longer be sticky")
}
if !strings.Contains(recorder.Body.String(), "Show allowed") {
t.Fatalf("overview page should expose the allowed toggle")
}
if strings.Contains(recorder.Body.String(), "Review only") {
t.Fatalf("overview page should not expose the review-only toggle anymore")
}
if !strings.Contains(recorder.Body.String(), "localStorage") {
t.Fatalf("overview page should persist preferences in localStorage")
if strings.Contains(recorder.Body.String(), "Auto refresh") {
t.Fatalf("overview page should not expose requests log controls")
}
recorder = httptest.NewRecorder()
request = httptest.NewRequest(http.MethodGet, "/queries", nil)
handler.ServeHTTP(recorder, request)
if recorder.Code != http.StatusMovedPermanently {
t.Fatalf("unexpected legacy query log redirect status: %d", recorder.Code)
}
if location := recorder.Header().Get("Location"); location != "/requests" {
t.Fatalf("unexpected redirect location: %q", location)
}
recorder = httptest.NewRecorder()
request = httptest.NewRequest(http.MethodGet, "/requests", nil)
handler.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("unexpected query log page status: %d", recorder.Code)
t.Fatalf("unexpected requests log page status: %d", recorder.Code)
}
queryLogBody := recorder.Body.String()
if !strings.Contains(queryLogBody, "Review only") {
t.Fatalf("query log page should expose the review-only toggle")
t.Fatalf("requests log page should expose the review-only toggle")
}
if !strings.Contains(queryLogBody, "These filters affect the full Query Log") {
t.Fatalf("query log page should explain its filters")
if !strings.Contains(queryLogBody, "These filters affect the full Requests Log") {
t.Fatalf("requests log page should explain its filters")
}
if !strings.Contains(queryLogBody, "Request") {
t.Fatalf("query log page should render the request table")
t.Fatalf("requests log page should render the request table")
}
if !strings.Contains(queryLogBody, "Auto refresh") {
t.Fatalf("requests log page should expose the auto refresh toggle")
}
if !strings.Contains(queryLogBody, "Previous") || !strings.Contains(queryLogBody, "Next") {
t.Fatalf("requests log page should expose pagination controls")
}
recorder = httptest.NewRecorder()
@@ -217,6 +234,19 @@ func (s *stubApp) GetOverview(_ context.Context, _ time.Time, _ int, options mod
LastSeenAt: now,
Bot: &model.BotMatch{Name: "Googlebot", ProviderID: "google_official", Verified: true},
}},
TopBotIPsByEvents: []model.TopIPRow{{
IP: "203.0.113.10",
Events: 3,
TrafficBytes: 4096,
LastSeenAt: now,
Bot: &model.BotMatch{Name: "Googlebot", ProviderID: "google_official", Verified: true},
}},
TopNonBotIPsByEvents: []model.TopIPRow{{
IP: "198.51.100.20",
Events: 2,
TrafficBytes: 2048,
LastSeenAt: now,
}},
TopIPsByTraffic: []model.TopIPRow{{
IP: "203.0.113.10",
Events: 3,
@@ -224,6 +254,19 @@ func (s *stubApp) GetOverview(_ context.Context, _ time.Time, _ int, options mod
LastSeenAt: now,
Bot: &model.BotMatch{Name: "Googlebot", ProviderID: "google_official", Verified: true},
}},
TopBotIPsByTraffic: []model.TopIPRow{{
IP: "203.0.113.10",
Events: 3,
TrafficBytes: 4096,
LastSeenAt: now,
Bot: &model.BotMatch{Name: "Googlebot", ProviderID: "google_official", Verified: true},
}},
TopNonBotIPsByTraffic: []model.TopIPRow{{
IP: "198.51.100.20",
Events: 2,
TrafficBytes: 2048,
LastSeenAt: now,
}},
TopSources: []model.TopSourceRow{{
SourceName: "main",
Events: 3,
@@ -265,7 +308,22 @@ func (s *stubApp) GetOverview(_ context.Context, _ time.Time, _ int, options mod
func (s *stubApp) ListEvents(ctx context.Context, _ time.Time, limit int, options model.EventListOptions) ([]model.Event, error) {
s.lastEventOptions = options
overview, _ := s.GetOverview(ctx, time.Time{}, limit, model.OverviewOptions{ShowKnownBots: true, ShowAllowed: true})
return overview.RecentEvents, nil
items := overview.RecentEvents
if limit > 1 {
items = append(items, model.Event{
ID: 2,
SourceName: "main",
ClientIP: "198.51.100.20",
OccurredAt: time.Now().UTC().Add(-time.Minute),
Method: http.MethodPost,
URI: "/xmlrpc.php",
Host: "example.test",
Status: http.StatusNotFound,
CurrentState: model.IPStateReview,
Actions: model.ActionAvailability{CanBlock: true},
})
}
return items, nil
}
func (s *stubApp) ListIPs(ctx context.Context, limit int, state string) ([]model.IPState, error) {