Five chart types on ECharts (modular imports, not the full bundle):
TimeSeriesChart (multi-series, legend toggle), BarChart (incl.
stacked), SingleStat (big number + sparkline + trend), Heatmap, TopN.
Shared interactions: tooltips, dataZoom feeding the global time-range
picker, click-to-drill-into-query (drilldown.ts strips a panel's query
to its pre-stats filter and appends the clicked series/x-value as a
new filter term -- no backend change needed).
pivot.ts reshapes the query language's existing {columns, rows} tabular
output into per-series chart data client-side -- `stats count by
service, timestamp` already returns "long" rows, so multi-series
support needed zero query-language changes. theme.ts reads real
computed CSS custom properties so charts render in the active theme's
actual colors, with an SSR_FALLBACK for adapter-static's prerender pass
where `document` doesn't exist.
heatmap is the one narrow, justified backend change: a new VizType
needed to feed a new visualization, not a new query capability. Three
places had to change together, not two -- api/dashboards/types.go's
validator, web/src/lib/api.ts's union (previous commit), and the
dashboard_panels table's viz_type CHECK constraint
(migrations/0035_add_heatmap_viz_type.sql), which mirrors the Go
validator and doesn't update itself.
/dev/charts (unlisted, dev-only) is a synthetic fixture/perf-test route:
confirmed 50ms first-two-frames render time on a production build
against a 30,006-row/6-series stress case, and a 211,975-byte gzipped
chart chunk -- both real measurements behind the ECharts-over-
Observable-Plot-or-D3 choice, not estimates.
91 lines
3.3 KiB
Go
91 lines
3.3 KiB
Go
// Package dashboards implements CRUD for saved, multi-panel dashboards
|
|
// -- see /docs/phase-3-dashboard-design.md. Deliberately pure CRUD: panel
|
|
// *query execution* happens client-side (the web UI calls the existing
|
|
// POST /query per panel), so this package never touches querylang.
|
|
package dashboards
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// VizType is one of the panel visualization kinds. "top_n" renders
|
|
// through the same path as "table" -- the query itself already did the
|
|
// sort/limit -- so there's no execution-side difference, only UI framing.
|
|
type VizType string
|
|
|
|
const (
|
|
VizTable VizType = "table"
|
|
VizLine VizType = "line"
|
|
VizBar VizType = "bar"
|
|
VizSingleStat VizType = "single_stat"
|
|
VizTopN VizType = "top_n"
|
|
// VizHeatmap is Phase 5's addition (log-volume-over-time patterns) --
|
|
// same "query already produced the right rows, only UI framing
|
|
// differs" shape as VizTopN, no new execution path.
|
|
VizHeatmap VizType = "heatmap"
|
|
)
|
|
|
|
func validVizType(v VizType) bool {
|
|
switch v {
|
|
case VizTable, VizLine, VizBar, VizSingleStat, VizTopN, VizHeatmap:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
type Dashboard struct {
|
|
ID string `json:"id"`
|
|
TenantID string `json:"tenant_id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
DefaultEarliest string `json:"default_earliest"`
|
|
DefaultLatest string `json:"default_latest"`
|
|
CreatedBy string `json:"created_by"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
Panels []Panel `json:"panels,omitempty"`
|
|
}
|
|
|
|
type Panel struct {
|
|
ID string `json:"id"`
|
|
DashboardID string `json:"dashboard_id"`
|
|
Title string `json:"title"`
|
|
Query string `json:"query"`
|
|
QueryLanguage string `json:"query_language"`
|
|
VizType VizType `json:"viz_type"`
|
|
VizConfig json.RawMessage `json:"viz_config,omitempty"`
|
|
PositionX int `json:"position_x"`
|
|
PositionY int `json:"position_y"`
|
|
Width int `json:"width"`
|
|
Height int `json:"height"`
|
|
EarliestOverride *string `json:"earliest_override,omitempty"`
|
|
LatestOverride *string `json:"latest_override,omitempty"`
|
|
SortOrder int `json:"sort_order"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// validatePanel enforces the two rules /docs/phase-3-dashboard-design.md
|
|
// states as disclosed non-goals rather than silent gaps: raw-SQL panels
|
|
// aren't supported (time-range injection has no reliable splice point
|
|
// into arbitrary SQL), and viz_type must be one this API knows how to
|
|
// store/render.
|
|
func validatePanel(p *Panel) error {
|
|
if p.Query == "" {
|
|
return fmt.Errorf("query must not be empty")
|
|
}
|
|
if p.QueryLanguage == "sql" {
|
|
return fmt.Errorf("raw-SQL panels are not supported -- dashboards only support pipe-syntax queries, since the dashboard time-range picker is injected as leading query terms")
|
|
}
|
|
if !validVizType(p.VizType) {
|
|
return fmt.Errorf("viz_type must be one of table, line, bar, single_stat, top_n, heatmap, got %q", p.VizType)
|
|
}
|
|
if len(p.VizConfig) == 0 {
|
|
p.VizConfig = json.RawMessage(`{}`)
|
|
}
|
|
return nil
|
|
}
|