mirror of
https://github.com/lukaszraczylo/graphql-monitoring-proxy.git
synced 2026-10-12 05:32:36 +00:00
feat(admin): redesign dashboard and make cluster view a real control
Replace the AI-slop visual (indigo->purple gradient, glassmorphism, uniform rounded cards, hover-lift) with a deliberate warm-paper 'instrument panel': monospace tabular telemetry, GraphQL brand magenta accent, flat hairline cards, real type hierarchy, WCAG-AA contrast, focus-visible, prefers-reduced-motion and aria-live status. No new font/CDN deps. Make the cluster-view toggle functional: the WebSocket stats stream now honours a ?view=local|cluster query param, so toggling streams this-node-only vs aggregated metrics instead of being cosmetic (the stream previously always sent cluster-aware data when an aggregator existed). The toggle is always visible -- disabled with a 'single node' hint when Redis cluster mode is off -- so it is discoverable. Fix cluster-mode rendering bugs: derive backend health from healthy/total instances (health card no longer stuck on 'Loading'), show 'n/a' for Redis memory instead of a misleading 0 MB, render aggregated CB/retry/coalescing/connections on the polling path too, and surface the proxy version and backend-health detail. Tests assert the served dashboard against stable markers instead of the cosmetic page title.
This commit is contained in:
+949
-1339
File diff suppressed because it is too large
Load Diff
+14
-4
@@ -670,6 +670,16 @@ func (ad *AdminDashboard) handleStatsWebSocket(c *websocket.Conn) {
|
|||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Determine the requested view. "local" forces this-instance stats; any
|
||||||
|
// other value (default) is cluster-aware and returns aggregated metrics
|
||||||
|
// when a metrics aggregator is available. This makes the dashboard's
|
||||||
|
// "Cluster view" toggle a real control rather than cosmetic, since the
|
||||||
|
// WebSocket stream is the primary data path.
|
||||||
|
gather := ad.gatherAllStatsClusterAware
|
||||||
|
if c.Query("view") == "local" {
|
||||||
|
gather = ad.gatherAllStats
|
||||||
|
}
|
||||||
|
|
||||||
// Channel to signal when to stop
|
// Channel to signal when to stop
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
|
|
||||||
@@ -694,8 +704,8 @@ func (ad *AdminDashboard) handleStatsWebSocket(c *websocket.Conn) {
|
|||||||
enc := json.NewEncoder(&buf)
|
enc := json.NewEncoder(&buf)
|
||||||
enc.SetEscapeHTML(false)
|
enc.SetEscapeHTML(false)
|
||||||
|
|
||||||
// Send initial stats immediately (cluster-aware for dashboard)
|
// Send initial stats immediately (honours the requested view)
|
||||||
if stats := ad.gatherAllStatsClusterAware(); stats != nil {
|
if stats := gather(); stats != nil {
|
||||||
buf.Reset()
|
buf.Reset()
|
||||||
if err := enc.Encode(stats); err == nil {
|
if err := enc.Encode(stats); err == nil {
|
||||||
// json.Encoder.Encode appends a trailing newline; strip it
|
// json.Encoder.Encode appends a trailing newline; strip it
|
||||||
@@ -708,8 +718,8 @@ func (ad *AdminDashboard) handleStatsWebSocket(c *websocket.Conn) {
|
|||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ticker.C:
|
case <-ticker.C:
|
||||||
// Gather all stats (cluster-aware for dashboard)
|
// Gather all stats (honours the requested view)
|
||||||
stats := ad.gatherAllStatsClusterAware()
|
stats := gather()
|
||||||
|
|
||||||
// Encode into reused buffer (no per-tick allocation churn)
|
// Encode into reused buffer (no per-tick allocation churn)
|
||||||
buf.Reset()
|
buf.Reset()
|
||||||
|
|||||||
@@ -76,10 +76,13 @@ func TestAdminDashboard_ServeDashboard(t *testing.T) {
|
|||||||
contentType := resp.Header.Get("Content-Type")
|
contentType := resp.Header.Get("Content-Type")
|
||||||
assert.Contains(t, contentType, "text/html")
|
assert.Contains(t, contentType, "text/html")
|
||||||
|
|
||||||
// Verify HTML content is returned
|
// Verify the dashboard HTML is returned (assert on stable markers, not
|
||||||
|
// the cosmetic page title which is part of the UI and may change).
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
assert.Contains(t, string(body), "GraphQL Proxy Admin Dashboard")
|
assert.Contains(t, string(body), "<!DOCTYPE html>")
|
||||||
|
assert.Contains(t, string(body), "graphql-proxy")
|
||||||
|
assert.Contains(t, string(body), "/admin/ws/stats")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAdminDashboard_GetStats(t *testing.T) {
|
func TestAdminDashboard_GetStats(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user