Dashboard: a grid of widgets, what fires, what is up, and the stream
DashboardGrid lays widgets on columns: a tile's strip drags it and its corner resizes it a cell at a time, others move down and rise to fill gaps, a focused tile steps with the arrows, and under the theme's stack_below width the tiles stand in one column. DashboardCard frames a widget. AlertList acknowledges a firing alert from its row and opens one on Enter; IncidentCard shows an incident's gravity, phase and updates. UptimeBar, ServiceStatus and HealthCheck show what is up, a check's detail wrapping under its name when narrow. EventStream keeps kinds and holds the list while new events count up. RefreshIntervalSelector and DashboardFilterBar set the window, the filters and the refresh. StatCard, MetricsChart, ResourceGauge, LogStream, TraceWaterfall, SystemMonitor, ProcessTable and NetworkGraph point to their homes; the maps wait for T41.
This commit is contained in:
1 parent
f3332f935b
commit
9fddf4b6bf
17 files changed
+2661
-18
No files matched your search
@@ -0,0 +1,163 @@
|
||||
use ely_gpui_component::{
|
||||
charts::{LineChart, Series},
|
||||
dashboard::{
|
||||
DashboardCard, DashboardFilter, DashboardFilterBar, DashboardGrid, RefreshIntervalSelector,
|
||||
Tile, TimeWindow,
|
||||
},
|
||||
data_display::{Gauge, Statistic, TrendIndicator},
|
||||
forms::Choice,
|
||||
};
|
||||
use gpui::{App, IntoElement, ParentElement, SharedString, Styled, Window, div, px};
|
||||
use jiff::Timestamp;
|
||||
|
||||
use crate::ui::{change, keep, section};
|
||||
|
||||
/// The board demo's tiles, window, filters and refresh.
|
||||
struct Wall {
|
||||
tiles: Vec<Tile>,
|
||||
window: TimeWindow,
|
||||
environment: Option<SharedString>,
|
||||
service: Option<SharedString>,
|
||||
every: Option<u32>,
|
||||
/// Seconds since the last refresh, at the demo's fixed clock.
|
||||
since: i64,
|
||||
}
|
||||
|
||||
impl Wall {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
tiles: vec![
|
||||
Tile::new("requests", (0, 0), (4, 2)),
|
||||
Tile::new("errors", (4, 0), (4, 2)),
|
||||
Tile::new("cpu", (8, 0), (4, 2)),
|
||||
Tile::new("latency", (0, 2), (8, 3)),
|
||||
Tile::new("saturation", (8, 2), (4, 3)),
|
||||
],
|
||||
window: TimeWindow::Hour,
|
||||
environment: Some("production".into()),
|
||||
service: None,
|
||||
every: Some(30),
|
||||
since: 40,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn grid(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let wall = keep("dashboard-wall", Wall::new, window, cx);
|
||||
let now = wall.read(cx);
|
||||
let (tiles, time, environment, service, every, since) = (
|
||||
now.tiles.clone(),
|
||||
now.window,
|
||||
now.environment.clone(),
|
||||
now.service.clone(),
|
||||
now.every,
|
||||
now.since,
|
||||
);
|
||||
let [moved, windowed, filtered, timed, refreshed] = [(); 5].map(|_| wall.clone());
|
||||
let clock: Timestamp = "2026-09-27T12:00:00Z".parse().expect("a time");
|
||||
let hours: Vec<String> = (0..24).map(|hour| format!("{hour:02}:00")).collect();
|
||||
let latency = Series::new(
|
||||
"p95",
|
||||
(0..24)
|
||||
.map(|ix| 120.0 + 40.0 * ((ix as f64) * 0.5).sin() + if ix > 18 { 60.0 } else { 0.0 }),
|
||||
);
|
||||
let filters = [
|
||||
DashboardFilter {
|
||||
key: "environment".into(),
|
||||
name: "Environment".into(),
|
||||
choices: vec![
|
||||
Choice::new("production", "Production"),
|
||||
Choice::new("staging", "Staging"),
|
||||
],
|
||||
picked: environment,
|
||||
},
|
||||
DashboardFilter {
|
||||
key: "service".into(),
|
||||
name: "Service".into(),
|
||||
choices: vec![
|
||||
Choice::new("web", "Web"),
|
||||
Choice::new("api", "API"),
|
||||
Choice::new("worker", "Worker"),
|
||||
],
|
||||
picked: service,
|
||||
},
|
||||
];
|
||||
let grid = DashboardGrid::new("dashboard-grid", 12, tiles)
|
||||
.card(
|
||||
"requests",
|
||||
DashboardCard::new("Requests").subtitle("per minute").body(
|
||||
Statistic::new("dashboard-requests", "This hour", 12_400.0)
|
||||
.trend(TrendIndicator::new(0.082)),
|
||||
),
|
||||
)
|
||||
.card(
|
||||
"errors",
|
||||
DashboardCard::new("Errors").subtitle("5xx share").body(
|
||||
Statistic::new("dashboard-errors", "This hour", 0.42)
|
||||
.decimals(2)
|
||||
.suffix("%")
|
||||
.trend(TrendIndicator::new(-0.12)),
|
||||
),
|
||||
)
|
||||
.card(
|
||||
"cpu",
|
||||
DashboardCard::new("CPU").body(Gauge::new(
|
||||
"dashboard-cpu",
|
||||
"percent busy",
|
||||
62.0,
|
||||
0.0,
|
||||
100.0,
|
||||
)),
|
||||
)
|
||||
.card(
|
||||
"latency",
|
||||
DashboardCard::new("Latency")
|
||||
.subtitle("p95, milliseconds")
|
||||
.body(LineChart::new("dashboard-latency", hours).series(latency)),
|
||||
)
|
||||
.card(
|
||||
"saturation",
|
||||
DashboardCard::new("Saturation").body(Gauge::new(
|
||||
"dashboard-memory",
|
||||
"memory in use",
|
||||
81.0,
|
||||
0.0,
|
||||
100.0,
|
||||
)),
|
||||
)
|
||||
.on_change(move |next, _, cx| change(&moved, cx, |wall| wall.tiles = next));
|
||||
section(
|
||||
"Dashboard Grid · Widget / DashboardCard · RefreshIntervalSelector · DashboardFilterBar · StatCard → data_display::Statistic · MetricsChart → charts::LineChart · ResourceGauge → data_display::Gauge",
|
||||
"Widgets on a grid of twelve columns: a tile's top strip drags it and its corner resizes it, a cell at a time, while the others make room and rise to fill gaps; a focused tile steps with the arrows. Over it, the window of time and the filters, and how often it refreshes.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_4()
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_wrap()
|
||||
.justify_between()
|
||||
.gap_3()
|
||||
.child(
|
||||
DashboardFilterBar::new("dashboard-filters", time, filters)
|
||||
.on_window(move |next, _, cx| change(&windowed, cx, |wall| wall.window = next))
|
||||
.on_filter(move |key, value, _, cx| {
|
||||
change(&filtered, cx, |wall| match key.as_ref() {
|
||||
"environment" => wall.environment = value,
|
||||
_ => wall.service = value,
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
RefreshIntervalSelector::new("dashboard-refresh", every, clock - jiff::SignedDuration::from_secs(since), clock)
|
||||
.on_change(move |next, _, cx| change(&timed, cx, |wall| wall.every = next))
|
||||
.on_refresh(move |_, cx| change(&refreshed, cx, |wall| wall.since = 0)),
|
||||
),
|
||||
)
|
||||
.child(div().w(px(900.)).child(grid)),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
use gpui::{AnyElement, App, Window, div, prelude::*};
|
||||
|
||||
mod board;
|
||||
mod monitor;
|
||||
|
||||
use super::Page;
|
||||
use crate::script::Step;
|
||||
|
||||
pub const PAGE: Page = Page {
|
||||
number: 34,
|
||||
slug: "dashboard",
|
||||
title: "Dashboard & Monitoring",
|
||||
summary: "Widgets on a grid you arrange, what is firing and what is up, and the stream of what happens.",
|
||||
render,
|
||||
script: SCRIPT,
|
||||
};
|
||||
|
||||
const SCRIPT: &[Step] = &[];
|
||||
|
||||
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
div()
|
||||
.child(board::grid(window, cx))
|
||||
.child(monitor::health(window, cx))
|
||||
.child(monitor::incidents(window, cx))
|
||||
.child(monitor::stream(window, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
use ely_gpui_component::{
|
||||
dashboard::{
|
||||
AlertList, AlertState, Check, EventStream, Health, HealthCheck, Incident, IncidentCard,
|
||||
IncidentPhase, IncidentUpdate, MonitorAlert, Service, ServiceStatus, StreamEvent,
|
||||
UptimeBar,
|
||||
},
|
||||
data_display::Tone,
|
||||
primitives::Severity,
|
||||
};
|
||||
use gpui::{App, IntoElement, ParentElement, Styled, Window, div, px};
|
||||
use jiff::{SignedDuration, Timestamp, civil::date};
|
||||
|
||||
use crate::ui::{change, keep, section};
|
||||
|
||||
fn now() -> Timestamp {
|
||||
"2026-09-27T12:00:00Z".parse().expect("a time")
|
||||
}
|
||||
|
||||
fn ago(minutes: i64) -> Timestamp {
|
||||
now() - SignedDuration::from_mins(minutes)
|
||||
}
|
||||
|
||||
pub fn health(_: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let days = |dip: usize| {
|
||||
(0..60).map(move |ix| {
|
||||
if ix == dip {
|
||||
Some(0.982)
|
||||
} else if ix == dip + 3 {
|
||||
Some(0.9975)
|
||||
} else if ix < 4 {
|
||||
None
|
||||
} else {
|
||||
Some(1.0)
|
||||
}
|
||||
})
|
||||
};
|
||||
section(
|
||||
"UptimeBar · ServiceStatus · HealthCheck Indicator",
|
||||
"Sixty days of a service, a bar a day in the tone of its uptime; services under a line for the whole system; and checks with what each found, when it ran and how long it took.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_wrap()
|
||||
.gap_6()
|
||||
.child(
|
||||
div()
|
||||
.w(px(420.))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_4()
|
||||
.child(UptimeBar::new("dashboard-uptime-api", "API", date(2026, 7, 30), days(41)))
|
||||
.child(UptimeBar::new("dashboard-uptime-web", "Website", date(2026, 7, 30), days(12))),
|
||||
)
|
||||
.child(
|
||||
div().w(px(360.)).child(ServiceStatus::new([
|
||||
Service { name: "API".into(), health: Health::Operational, uptime: Some(0.9998) },
|
||||
Service { name: "Website".into(), health: Health::Operational, uptime: Some(0.9992) },
|
||||
Service { name: "Payments".into(), health: Health::Degraded, uptime: Some(0.9971) },
|
||||
Service { name: "Email".into(), health: Health::Maintenance, uptime: None },
|
||||
])),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w(px(320.))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_2()
|
||||
.child(HealthCheck::new("GET /health", Check::Passing).ran(ago(1), now()).took(42))
|
||||
.child(HealthCheck::new("Database", Check::Passing).ran(ago(1), now()).took(8))
|
||||
.child(HealthCheck::new("Queue depth", Check::Warning).ran(ago(2), now()))
|
||||
.child(HealthCheck::new("Payments provider", Check::Failing).ran(ago(3), now()).took(3_020))
|
||||
.child(HealthCheck::new("Search index", Check::Pending)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn incidents(window: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let alerts = keep(
|
||||
"dashboard-alerts",
|
||||
|| {
|
||||
let alert =
|
||||
|key: &str, title: &str, source: &str, severity, state, minutes| MonitorAlert {
|
||||
key: key.to_string().into(),
|
||||
title: title.to_string().into(),
|
||||
source: source.to_string().into(),
|
||||
severity,
|
||||
state,
|
||||
since: ago(minutes),
|
||||
};
|
||||
vec![
|
||||
alert(
|
||||
"a1",
|
||||
"Payments p95 over 2s",
|
||||
"payments · prometheus",
|
||||
Severity::Danger,
|
||||
AlertState::Firing,
|
||||
12,
|
||||
),
|
||||
alert(
|
||||
"a2",
|
||||
"Queue depth rising",
|
||||
"worker · prometheus",
|
||||
Severity::Warning,
|
||||
AlertState::Firing,
|
||||
25,
|
||||
),
|
||||
alert(
|
||||
"a3",
|
||||
"Disk 85% on db-2",
|
||||
"db-2 · node exporter",
|
||||
Severity::Warning,
|
||||
AlertState::Acknowledged,
|
||||
95,
|
||||
),
|
||||
alert(
|
||||
"a4",
|
||||
"Certificate renewed",
|
||||
"edge · cert-manager",
|
||||
Severity::Info,
|
||||
AlertState::Resolved,
|
||||
300,
|
||||
),
|
||||
]
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let list = alerts.read(cx).clone();
|
||||
let incident = Incident {
|
||||
title: "Card payments fail for some customers".into(),
|
||||
severity: Severity::Danger,
|
||||
services: vec!["Payments".into(), "Checkout".into()],
|
||||
started: ago(48),
|
||||
updates: vec![
|
||||
IncidentUpdate {
|
||||
phase: IncidentPhase::Investigating,
|
||||
text: "Payments time out for about one in ten cards.".into(),
|
||||
at: ago(46),
|
||||
},
|
||||
IncidentUpdate {
|
||||
phase: IncidentPhase::Identified,
|
||||
text: "The provider's EU region is slow; traffic moves to the US region.".into(),
|
||||
at: ago(30),
|
||||
},
|
||||
IncidentUpdate {
|
||||
phase: IncidentPhase::Monitoring,
|
||||
text: "Payments pass again; we watch error rates.".into(),
|
||||
at: ago(9),
|
||||
},
|
||||
],
|
||||
};
|
||||
section(
|
||||
"AlertList · IncidentCard",
|
||||
"Alerts with their severity, what raised them and since when; a firing one is acknowledged from its row. An incident with its severity and phase, what it touches, and its updates newest first.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.flex_wrap()
|
||||
.gap_6()
|
||||
.child(
|
||||
div().w(px(460.)).child(
|
||||
AlertList::new("dashboard-alert-list", list, now()).on_acknowledge(move |key, _, cx| {
|
||||
change(&alerts, cx, |alerts| {
|
||||
alerts.iter_mut().find(|alert| alert.key == *key).expect("a listed alert").state = AlertState::Acknowledged
|
||||
})
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(div().w(px(420.)).child(IncidentCard::new(incident, now()))),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn stream(_: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let event = |key: &str, kind: &str, text: &str, tone, minutes| StreamEvent {
|
||||
key: key.to_string().into(),
|
||||
kind: kind.to_string().into(),
|
||||
text: text.to_string().into(),
|
||||
tone,
|
||||
at: ago(minutes),
|
||||
};
|
||||
let events = vec![
|
||||
event(
|
||||
"e1",
|
||||
"deploy",
|
||||
"web 1.8.1 rolled out to production",
|
||||
Tone::Info,
|
||||
58,
|
||||
),
|
||||
event(
|
||||
"e2",
|
||||
"scale",
|
||||
"worker scaled from 4 to 6 replicas",
|
||||
Tone::Neutral,
|
||||
41,
|
||||
),
|
||||
event("e3", "alert", "Payments p95 over 2s", Tone::Danger, 12),
|
||||
event(
|
||||
"e4",
|
||||
"config",
|
||||
"feature flag new-checkout at 20%",
|
||||
Tone::Accent,
|
||||
7,
|
||||
),
|
||||
event(
|
||||
"e5",
|
||||
"deploy",
|
||||
"web 1.8.2 rolled out to production",
|
||||
Tone::Info,
|
||||
2,
|
||||
),
|
||||
];
|
||||
section(
|
||||
"EventStream · LogStream → terminal::LogViewer · TraceWaterfall → debug::TimelineProfiler · SystemMonitor → devtools::ResourceMonitor · ProcessTable → terminal::ProcessList · NetworkGraph (throughput) → charts::RealtimeChart · MapView / GeoHeatmap → maps (T41) · WorldMap → maps (T41)",
|
||||
"Events as they come, newest first, kept by kind, with Pause to hold the list while new ones count up. Logs, traces, a machine's readings, its processes and live throughput have their homes in the terminal, agent, dev tools and charts chapters; the maps come with the maps chapter.",
|
||||
cx,
|
||||
)
|
||||
.child(div().w(px(620.)).child(EventStream::new("dashboard-events", events, now())))
|
||||
}
|
||||
@@ -5,6 +5,7 @@ mod canvas;
|
||||
mod charts;
|
||||
mod chat;
|
||||
mod collab;
|
||||
mod dashboard;
|
||||
mod data;
|
||||
mod debug;
|
||||
mod devtools;
|
||||
@@ -81,6 +82,7 @@ pub const ALL: &[Page] = &[
|
||||
project::PAGE,
|
||||
canvas::PAGE,
|
||||
devtools::PAGE,
|
||||
dashboard::PAGE,
|
||||
theme::PAGE,
|
||||
];
|
||||
|
||||
|
||||
Reference in new issue
Block a user