A live server-metrics dashboard — Deferred polling engine, dojox/charting live line chart, pub/sub alert system, start/stop/reset controls, threshold-based color coding
Key architectural choice: PollingEngine knows nothing about the chart or the metric cards. It fetches data and publishes to metrics/update. Each display module subscribes independently — swap or add displays without touching PollingEngine.
// monitor/PollingEngine.js
define([
"dojo/_base/declare",
"dojo/_base/lang",
"dojo/Deferred",
"dojo/request/xhr",
"dojo/topic",
"dojo/Stateful",
"monitor/thresholds"
], function(declare, lang, Deferred, xhr, topic, Stateful, THRESHOLDS) {
return declare([Stateful], {
// Observable state — widgets watch these
running: false,
intervalSec: 3, // poll every 3 seconds
pollCount: 0,
lastError: null,
_timer: null,
_history: null, // ring buffer of last 60 data points
HISTORY_MAX: 60,
constructor: function() {
this._history = [];
},
// ── Start polling ─────────────────────────────────────────────
start: function() {
if (this.get("running")) return;
this.set("running", true);
this.set("lastError", null);
this._poll();
},
// ── Stop polling ──────────────────────────────────────────────
stop: function() {
this.set("running", false);
if (this._timer) { clearTimeout(this._timer); this._timer = null; }
},
// ── Reset ─────────────────────────────────────────────────────
reset: function() {
this.stop();
this._history = [];
this.set("pollCount", 0);
this.set("lastError", null);
topic.publish("metrics/reset");
},
// ── Core poll function — uses Deferred chaining ───────────────
_poll: function() {
if (!this.get("running")) return;
var self = this;
var count = this.get("pollCount") + 1;
this.set("pollCount", count);
this._fetch()
.then(function(metrics) {
// Add to history ring buffer
self._history.push(metrics);
if (self._history.length > self.HISTORY_MAX) {
self._history.shift();
}
// Publish live data to all subscribers
topic.publish("metrics/update", {
metrics: metrics,
history: self._history.slice(),
pollCount: count,
timestamp: Date.now()
});
// Check thresholds — publish alerts if breached
self._checkThresholds(metrics);
// Schedule next poll
if (self.get("running")) {
self._timer = setTimeout(function() { self._poll(); },
self.intervalSec * 1000);
}
})
.otherwise(function(err) {
self.set("lastError", err.message);
topic.publish("metrics/error", { error: err, count: count });
// Retry with backoff — 3× normal interval on error
if (self.get("running")) {
self._timer = setTimeout(function() { self._poll(); },
self.intervalSec * 3000);
}
});
},
// ── Fetch metrics from server ─────────────────────────────────
// In production: real XHR call
// In this demo: simulated data with realistic variation
_fetch: function() {
var deferred = new Deferred();
// Simulate async server call
setTimeout(function() {
// Realistic simulation: random walk from previous values
var prev = this._history.length
? this._history[this._history.length - 1]
: { cpu: 35, memory: 62, disk: 45, network: 28 };
function walk(v, min, max, step) {
return Math.max(min, Math.min(max, v + (Math.random() - 0.48) * step));
}
deferred.resolve({
cpu: Math.round(walk(prev.cpu, 0, 100, 8)),
memory: Math.round(walk(prev.memory, 40, 98, 4)),
disk: Math.round(walk(prev.disk, 10, 100, 12)),
network: Math.round(walk(prev.network, 0, 80, 6)),
timestamp: Date.now()
});
}.bind(this), 200 + Math.random() * 300); // 200-500ms simulated latency
return deferred.promise;
// Real XHR version:
// return xhr.get("/api/metrics", { handleAs: "json" });
},
// ── Threshold checking ────────────────────────────────────────
_checkThresholds: function(metrics) {
var alerts = [];
Object.keys(THRESHOLDS).forEach(function(key) {
var t = THRESHOLDS[key];
var val = metrics[key];
if (val === undefined) return;
var level = null;
if (val >= t.critical) level = "critical";
else if (val >= t.warning) level = "warning";
if (level) {
alerts.push({ metric: key, value: val, level: level, threshold: t[level] });
}
});
if (alerts.length) {
topic.publish("metrics/alert", { alerts: alerts, metrics: metrics });
} else {
topic.publish("metrics/allClear");
}
}
});
});
// monitor/thresholds.js
define({
cpu: { warning: 70, critical: 90 },
memory: { warning: 75, critical: 90 },
disk: { warning: 80, critical: 95 },
network: { warning: 60, critical: 80 }
});
// monitor/LiveChart.js
define([
"dojo/_base/declare",
"dojo/_base/lang",
"dijit/_Widget",
"dojox/charting/Chart",
"dojox/charting/axis2d/Default",
"dojox/charting/plot2d/Lines",
"dojox/charting/themes/Julie",
"dojo/topic",
"dojo/dom-construct"
], function(declare, lang, _Widget,
Chart, Default, Lines, theme,
topic, domConstruct) {
return declare([_Widget], {
MAX_POINTS: 60, // keep 60 data points (= MAX_POINTS × intervalSec of history)
_chart: null,
_series: null, // { cpu: [], memory: [], disk: [], network: [] }
buildRendering: function() {
this.domNode = domConstruct.create("div", {
style: "width:100%; height:300px;"
});
},
startup: function() {
if (this._started) return;
this.inherited(arguments);
this._series = { cpu: [], memory: [], disk: [], network: [] };
// ── Create chart ───────────────────────────────────────────
this._chart = new Chart(this.domNode);
this._chart.setTheme(theme);
this._chart.addAxis("x", {
title: "Time (polls)",
natural: true,
majorLabels: false,
minorLabels: false
});
this._chart.addAxis("y", {
title: "% Utilisation",
vertical: true,
min: 0,
max: 100,
majorTick: { stroke: "rgba(255,255,255,.08)", length: 5 }
});
this._chart.addPlot("default", {
type: Lines,
tension: "S", // smooth cubic spline
markers: false,
shadows: { dx:1, dy:1, dw:2 }
});
// Add one series per metric
var colors = { cpu:"#6366f1", memory:"#22d3ee", disk:"#f472b6", network:"#34d399" };
Object.keys(this._series).forEach(lang.hitch(this, function(key) {
this._chart.addSeries(key, [0], {
stroke: { color: colors[key], width: 2 },
fill: "none"
});
}));
this._chart.render();
// ── Subscribe to live data ──────────────────────────────────
this.own(
topic.subscribe("metrics/update", lang.hitch(this, "_onData")),
topic.subscribe("metrics/reset", lang.hitch(this, "_onReset"))
);
},
_onData: function(data) {
var m = data.metrics;
// Append new values, trim to MAX_POINTS
Object.keys(this._series).forEach(lang.hitch(this, function(key) {
this._series[key].push(m[key]);
if (this._series[key].length > this.MAX_POINTS) {
this._series[key].shift();
}
// Update chart series with new data
this._chart.updateSeries(key, this._series[key]);
}));
// Animate chart update
this._chart.render();
},
_onReset: function() {
Object.keys(this._series).forEach(lang.hitch(this, function(key) {
this._series[key] = [];
this._chart.updateSeries(key, [0]);
}));
this._chart.render();
},
destroy: function(preserveDom) {
if (this._chart) { this._chart.destroy(); this._chart = null; }
this.inherited(arguments);
}
});
});
// monitor/AlertPanel.js
define([
"dojo/_base/declare",
"dojo/_base/lang",
"dijit/_Widget",
"dojo/topic",
"dojo/dom-construct",
"dojo/dom-class"
], function(declare, lang, _Widget, topic, domConstruct, domClass) {
return declare([_Widget], {
buildRendering: function() {
this.domNode = domConstruct.create("div", { className: "alert-panel" });
this._list = domConstruct.create("ul", {}, this.domNode);
},
postCreate: function() {
this.inherited(arguments);
this.own(
topic.subscribe("metrics/alert", lang.hitch(this, "_showAlerts")),
topic.subscribe("metrics/allClear", lang.hitch(this, "_clearAlerts")),
topic.subscribe("metrics/reset", lang.hitch(this, "_clearAlerts"))
);
},
_showAlerts: function(data) {
domConstruct.empty(this._list);
domClass.remove(this.domNode, "alert-panel--clear");
data.alerts.forEach(lang.hitch(this, function(alert) {
var isCrit = alert.level === "critical";
var li = domConstruct.create("li", {
className: "alert-item alert-item--" + alert.level,
innerHTML:
"" +
"" + alert.metric.toUpperCase() + " at " +
alert.value + "% " +
"(threshold: " + alert.threshold + "%)"
}, this._list);
// Critical alerts pulse
if (isCrit) domClass.add(li, "alert-pulse");
}));
},
_clearAlerts: function() {
domConstruct.empty(this._list);
domConstruct.create("li", {
className: "alert-item alert-item--ok",
innerHTML: "✅ All metrics within normal range"
}, this._list);
domClass.add(this.domNode, "alert-panel--clear");
}
});
});
// monitor/ControlBar.js
define([
"dojo/_base/declare",
"dojo/_base/lang",
"dijit/_Widget",
"dijit/form/Button",
"dijit/form/NumberSpinner",
"dojo/dom-construct",
"dojo/dom-class"
], function(declare, lang, _Widget, Button, NumberSpinner, domConstruct, domClass) {
return declare([_Widget], {
engine: null, // PollingEngine instance passed in
buildRendering: function() {
this.domNode = domConstruct.create("div", { className:"control-bar" });
},
postCreate: function() {
this.inherited(arguments);
var self = this;
var engine = this.engine;
// ── Start/Stop button (toggle) ────────────────────────────
this._startStopBtn = new Button({
label: "▶ Start",
"class": "btn-primary",
onClick: function() {
if (engine.get("running")) {
engine.stop();
} else {
engine.start();
}
}
}).placeAt(this.domNode);
this._startStopBtn.startup();
// ── Reset button ──────────────────────────────────────────
new Button({
label: "↺ Reset",
onClick: function() { engine.reset(); }
}).placeAt(this.domNode).startup();
// ── Interval selector ─────────────────────────────────────
domConstruct.create("label", {
textContent: "Poll every",
style: "margin-left:16px; margin-right:6px; color:var(--text2)"
}, this.domNode);
var intervalSpinner = new NumberSpinner({
value: engine.get("intervalSec"),
constraints: { min:1, max:30, places:0 },
smallDelta: 1,
style: "width:70px",
onChange: function(v) { engine.set("intervalSec", v); }
}).placeAt(this.domNode);
intervalSpinner.startup();
domConstruct.create("span", {
textContent: "seconds",
style: "margin-left:6px; color:var(--muted)"
}, this.domNode);
// ── Poll counter display ──────────────────────────────────
this._counter = domConstruct.create("span", {
style: "margin-left:20px; color:var(--muted); font-size:.85rem"
}, this.domNode);
// ── Watch engine state → update button label ──────────────
this.own(
engine.watch("running", lang.hitch(this, function(p, old, running) {
this._startStopBtn.set("label", running ? "�?� Pause" : "▶ Start");
domClass.toggle(this._startStopBtn.domNode, "btn-danger", running);
domClass.toggle(this._startStopBtn.domNode, "btn-primary", !running);
})),
engine.watch("pollCount", lang.hitch(this, function(p, o, count) {
this._counter.textContent = "Polls: " + count;
})),
engine.watch("lastError", lang.hitch(this, function(p, o, err) {
if (err) {
this._counter.textContent = "Error: " + err;
this._counter.style.color = "#f87171";
}
}))
);
}
});
});
// monitor/MetricCards.js
define([
"dojo/_base/declare",
"dojo/_base/lang",
"dijit/_Widget",
"dojo/topic",
"dojo/dom-construct",
"dojo/dom-class",
"monitor/thresholds"
], function(declare, lang, _Widget, topic, domConstruct, domClass, THRESHOLDS) {
return declare([_Widget], {
_cards: null,
buildRendering: function() {
this.domNode = domConstruct.create("div", { className:"metric-cards-grid" });
this._cards = {};
},
postCreate: function() {
this.inherited(arguments);
// Build a card for each metric
["cpu", "memory", "disk", "network"].forEach(lang.hitch(this, function(key) {
var card = domConstruct.create("div", {
className: "metric-card",
innerHTML:
"—" +
"" + key.toUpperCase() + "" +
""
}, this.domNode);
this._cards[key] = {
card: card,
value: card.querySelector("#val-" + key),
bar: card.querySelector("#bar-" + key)
};
}));
this.own(
topic.subscribe("metrics/update", lang.hitch(this, "_update")),
topic.subscribe("metrics/reset", lang.hitch(this, "_reset"))
);
},
_update: function(data) {
var metrics = data.metrics;
Object.keys(this._cards).forEach(lang.hitch(this, function(key) {
var val = metrics[key];
var card = this._cards[key];
if (val === undefined) return;
// Update display value
card.value.textContent = val + "%";
// Update progress bar width
card.bar.style.width = val + "%";
// Colour-code based on thresholds
var t = THRESHOLDS[key];
domClass.remove(card.card, "metric-ok metric-warn metric-crit");
if (val >= t.critical) domClass.add(card.card, "metric-crit");
else if (val >= t.warning) domClass.add(card.card, "metric-warn");
else domClass.add(card.card, "metric-ok");
}));
},
_reset: function() {
Object.keys(this._cards).forEach(lang.hitch(this, function(key) {
this._cards[key].value.textContent = "—";
this._cards[key].bar.style.width = "0%";
domClass.remove(this._cards[key].card, "metric-ok metric-warn metric-crit");
}));
}
});
});
// monitor/boot.js
define([
"dojo/_base/declare",
"dijit/layout/BorderContainer",
"dijit/layout/ContentPane",
"monitor/PollingEngine",
"monitor/ControlBar",
"monitor/MetricCards",
"monitor/LiveChart",
"monitor/AlertPanel",
"dojo/domReady!"
], function(
declare,
BorderContainer, ContentPane,
PollingEngine, ControlBar, MetricCards, LiveChart, AlertPanel
) {
// ── Create the single shared polling engine ────────────────────
var engine = new PollingEngine({ intervalSec: 3 });
// ── Full-page layout ───────────────────────────────────────────
var layout = new BorderContainer({
design: "headline",
style: "width:100%;height:100vh;"
});
// Top: control bar (start/stop/reset + interval + poll count)
var topPane = new ContentPane({
region: "top",
style: "height:56px; padding:0 16px;",
content: new ControlBar({ engine: engine })
});
// Left: metric cards + alert panel
var leftPane = new ContentPane({ region:"left", style:"width:280px;", splitter:true });
new MetricCards().placeAt(leftPane.containerNode);
new AlertPanel().placeAt(leftPane.containerNode);
// Center: live chart
var centerPane = new ContentPane({
region: "center",
content: new LiveChart()
});
layout.addChild(topPane);
layout.addChild(leftPane);
layout.addChild(centerPane);
layout.placeAt(document.body);
layout.startup();
// Auto-start polling on load
engine.start();
});
| Pattern | Where Used |
|---|---|
| Deferred for async polling loop | PollingEngine._poll() — chained fetch + schedule next |
| Deferred for simulated latency | PollingEngine._fetch() — setTimeout inside Deferred |
| Stateful for observable engine state | running, pollCount watched by ControlBar |
| Pub/sub one-to-many fanout | metrics/update → 3 subscribers (cards, chart, threshold checker) |
| Deferred error recovery with backoff | .otherwise() in poll loop — retries at 3× interval |
| dojox/charting live data push | chart.updateSeries() then chart.render() on each data point |
| domClass for threshold colour coding | MetricCards toggles ok/warn/crit CSS classes per metric |
Modules: 7
Topics: 4
Key phases: 3, 5, 6
Chart: dojox/charting