Color the dot beside each host name by its latest latency (closes #207)
check / check (push) Waiting to run
check / check (push) Waiting to run
updateHostRow gives the dot the color of the row's latest figure from CONFIG.latencyColors, gray when the check failed or no result has come yet. Pausing still sets it gray. The dot carries a status-dot class and data-host, as the figure and status line do, so both functions find it the same way. Model: opus-5-5
This commit is contained in:
1 parent
93bbd80d57
commit
2c7ef9ab7d
3 files changed
+55
-20
No files matched your search
@@ -113,19 +113,19 @@ half second. As soon as one answers, a new round starts.
|
|||||||
### Rows, colors and controls
|
### Rows, colors and controls
|
||||||
|
|
||||||
A figure and its line in the graph take the color of the legend in the footer,
|
A figure and its line in the graph take the color of the legend in the footer,
|
||||||
from green under 50 ms to red from 500 ms up; gray is no answer. The graph's
|
from green under 50 ms to red from 500 ms up; gray is no answer. The dot beside
|
||||||
axis ends at 1000 ms; a higher latency is drawn at the top, and the figure still
|
the host's name takes the same color. The graph's axis ends at 1000 ms; a higher
|
||||||
shows its real value.
|
latency is drawn at the top, and the figure still shows its real value.
|
||||||
|
|
||||||
From time to time, the page sorts the WAN rows by their latest latency: fastest
|
From time to time, the page sorts the WAN rows by their latest latency: fastest
|
||||||
first, failed last. The pin button at the left of a row keeps the row at the
|
first, failed last. The pin button at the left of a row keeps the row at the
|
||||||
top; pinned rows are sorted by name. `datavi.be` is pinned at start.
|
top; pinned rows are sorted by name. `datavi.be` is pinned at start.
|
||||||
|
|
||||||
`Pause` stops the checks: the graphs keep scrolling with a gap for each paused
|
`Pause` stops the checks: the graphs keep scrolling with a gap for each paused
|
||||||
round, and the figures turn gray. The `Interval` menu sets the time between
|
round, and the figures and dots turn gray. The `Interval` menu sets the time
|
||||||
rounds: 1, 2, 3, 5, 10, 15, 30 or 60 seconds. Changing it clears the history and
|
between rounds: 1, 2, 3, 5, 10, 15, 30 or 60 seconds. Changing it clears the
|
||||||
starts a new round at once. The `Debug log` checkbox in the footer opens a panel
|
history and starts a new round at once. The `Debug log` checkbox in the footer
|
||||||
with the page's last 1000 log lines.
|
opens a panel with the page's last 1000 log lines.
|
||||||
|
|
||||||
## Reporting
|
## Reporting
|
||||||
|
|
||||||
|
|||||||
+40
-3
@@ -28,8 +28,8 @@ import {
|
|||||||
// a host's row that tick or greyOutUI draws into is a plain object, made
|
// a host's row that tick or greyOutUI draws into is a plain object, made
|
||||||
// the first time a test looks it up and kept in elements under its
|
// the first time a test looks it up and kept in elements under its
|
||||||
// selector until the next test starts. As on a page, writing its text
|
// selector until the next test starts. As on a page, writing its text
|
||||||
// replaces its markup; the status dot greyOutUI looks for in it is not
|
// replaces its markup; setting a style keeps it in the element's style
|
||||||
// there. Drawing a sparkline does nothing; it looks for the pixel ratio on
|
// object. Drawing a sparkline does nothing; it looks for the pixel ratio on
|
||||||
// window and finds none. The commit the page's footer names, which
|
// window and finds none. The commit the page's footer names, which
|
||||||
// vite.config.js fills in when it builds the page, is blank. In each test,
|
// vite.config.js fills in when it builds the page, is blank. In each test,
|
||||||
// console.error and console.info print nothing and keep what they are
|
// console.error and console.info print nothing and keep what they are
|
||||||
@@ -63,7 +63,7 @@ globalThis.document = {
|
|||||||
querySelector: (selector) =>
|
querySelector: (selector) =>
|
||||||
(elements[selector] ??= {
|
(elements[selector] ??= {
|
||||||
getContext: () => canvasContext,
|
getContext: () => canvasContext,
|
||||||
querySelector: () => null,
|
style: {},
|
||||||
set textContent(text) {
|
set textContent(text) {
|
||||||
this.innerHTML = text;
|
this.innerHTML = text;
|
||||||
},
|
},
|
||||||
@@ -83,6 +83,12 @@ function statusText(state, host) {
|
|||||||
return elements[`.status-text[data-host="${index}"]`]?.innerHTML;
|
return elements[`.status-text[data-host="${index}"]`]?.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The color last given to the dot beside host's name.
|
||||||
|
function statusDot(state, host) {
|
||||||
|
const index = state.allHosts.indexOf(host);
|
||||||
|
return elements[`.status-dot[data-host="${index}"]`]?.style.backgroundColor;
|
||||||
|
}
|
||||||
|
|
||||||
// What this test wrote to the browser console with console[method].
|
// What this test wrote to the browser console with console[method].
|
||||||
function consoleLines(method) {
|
function consoleLines(method) {
|
||||||
return console[method].mock.calls.map((call) => call.arguments[0]);
|
return console[method].mock.calls.map((call) => call.arguments[0]);
|
||||||
@@ -683,6 +689,37 @@ test("a check that timed out or found its target unreachable has a grey figure a
|
|||||||
assert.equal(latencyHex(null), "#6b7280");
|
assert.equal(latencyHex(null), "#6b7280");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("a check that answers after 488ms turns the dot beside its target's name orange", async (t) => {
|
||||||
|
const { state, target } = stateWithOneTarget();
|
||||||
|
mockTargets(t, () => 488);
|
||||||
|
const round = tick(state, new AbortController().signal);
|
||||||
|
t.mock.timers.tick(488);
|
||||||
|
assert.notEqual(await settled(round), "still waiting");
|
||||||
|
assert.equal(statusDot(state, target), "#f97316");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a check that fails after one that answered turns the dot beside its target's name gray", async (t) => {
|
||||||
|
const { state, target } = stateWithOneTarget();
|
||||||
|
target.pushSample(Date.now(), { latency: 8, error: null });
|
||||||
|
mockTargets(t, () => 23, target.url);
|
||||||
|
const round = tick(state, new AbortController().signal);
|
||||||
|
t.mock.timers.tick(23);
|
||||||
|
assert.notEqual(await settled(round), "still waiting");
|
||||||
|
assert.equal(statusDot(state, target), "#6b7280");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("pausing turns the dot beside a target's name gray", async (t) => {
|
||||||
|
const { state, target } = stateWithOneTarget();
|
||||||
|
mockTargets(t, () => 8);
|
||||||
|
const round = tick(state, new AbortController().signal);
|
||||||
|
t.mock.timers.tick(8);
|
||||||
|
assert.notEqual(await settled(round), "still waiting");
|
||||||
|
assert.equal(statusDot(state, target), "#22c55e");
|
||||||
|
state.paused = true;
|
||||||
|
greyOutUI(state);
|
||||||
|
assert.equal(statusDot(state, target), "#6b7280");
|
||||||
|
});
|
||||||
|
|
||||||
// buildUI draws the page into #app, here a plain object. Each entry in the
|
// buildUI draws the page into #app, here a plain object. Each entry in the
|
||||||
// footer's color legend is a colored dot and the latencies it stands for,
|
// footer's color legend is a colored dot and the latencies it stands for,
|
||||||
// with < and > written as < and >. A latency takes the color of the
|
// with < and > written as < and >. A latency takes the color of the
|
||||||
|
|||||||
+8
-10
@@ -30,8 +30,8 @@ export const CONFIG = {
|
|||||||
return [0, 1, 2, 3, 4, 5].map((i) => Math.round((d * i) / 5));
|
return [0, 1, 2, 3, 4, 5].map((i) => Math.round((d * i) / 5));
|
||||||
},
|
},
|
||||||
canvasHeight: 96,
|
canvasHeight: 96,
|
||||||
// A latency figure and its sparkline take the color of the first entry
|
// A latency figure, its row's dot and its sparkline take the color of
|
||||||
// whose limit, in ms, the latency is below.
|
// the first entry whose limit, in ms, the latency is below.
|
||||||
latencyColors: [
|
latencyColors: [
|
||||||
{ below: 50, hex: "#22c55e", className: "text-green-500" },
|
{ below: 50, hex: "#22c55e", className: "text-green-500" },
|
||||||
{ below: 100, hex: "#84cc16", className: "text-lime-500" },
|
{ below: 100, hex: "#84cc16", className: "text-lime-500" },
|
||||||
@@ -819,7 +819,7 @@ export function hostRowHTML(host, index, showPin = true) {
|
|||||||
${pinBtn}
|
${pinBtn}
|
||||||
<div class="w-[420px] flex-shrink-0 grid grid-cols-[minmax(0,1fr)_auto] items-center">
|
<div class="w-[420px] flex-shrink-0 grid grid-cols-[minmax(0,1fr)_auto] items-center">
|
||||||
<div class="flex items-center gap-2 min-w-[200px]">
|
<div class="flex items-center gap-2 min-w-[200px]">
|
||||||
<div class="w-3 h-3 rounded-full flex-shrink-0 bg-[#6b7280]"></div>
|
<div class="status-dot w-3 h-3 rounded-full flex-shrink-0 bg-[#6b7280]" data-host="${index}"></div>
|
||||||
<span class="font-medium text-white truncate">${escapeHTML(host.name)}</span>
|
<span class="font-medium text-white truncate">${escapeHTML(host.name)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="latency-value text-4xl font-bold tabular-nums text-right mt-3" data-host="${index}">
|
<div class="latency-value text-4xl font-bold tabular-nums text-right mt-3" data-host="${index}">
|
||||||
@@ -982,8 +982,10 @@ function updateHostRow(host, index) {
|
|||||||
const canvas = document.querySelector(
|
const canvas = document.querySelector(
|
||||||
`.sparkline-canvas[data-host="${index}"]`,
|
`.sparkline-canvas[data-host="${index}"]`,
|
||||||
);
|
);
|
||||||
if (!latencyEl || !statusEl || !canvas) return;
|
const dot = document.querySelector(`.status-dot[data-host="${index}"]`);
|
||||||
|
if (!latencyEl || !statusEl || !canvas || !dot) return;
|
||||||
|
|
||||||
|
dot.style.backgroundColor = latencyHex(host.lastLatency);
|
||||||
if (host.lastLatency !== null) {
|
if (host.lastLatency !== null) {
|
||||||
const cls = latencyClass(host.lastLatency, host.status);
|
const cls = latencyClass(host.lastLatency, host.status);
|
||||||
latencyEl.innerHTML = `<span class="${cls}">${host.lastLatency}<span class="text-lg">ms</span></span>`;
|
latencyEl.innerHTML = `<span class="${cls}">${host.lastLatency}<span class="text-lg">ms</span></span>`;
|
||||||
@@ -1420,12 +1422,8 @@ export function greyOutUI(state) {
|
|||||||
statusEl.textContent = "paused";
|
statusEl.textContent = "paused";
|
||||||
statusEl.className = `${STATUS_TEXT_CLASS} text-gray-500`;
|
statusEl.className = `${STATUS_TEXT_CLASS} text-gray-500`;
|
||||||
}
|
}
|
||||||
// Grey out the status dot
|
const dot = document.querySelector(`.status-dot[data-host="${i}"]`);
|
||||||
const row = document.querySelector(`.host-row[data-index="${i}"]`);
|
if (dot) dot.style.backgroundColor = latencyHex(null);
|
||||||
if (row) {
|
|
||||||
const dot = row.querySelector(".rounded-full");
|
|
||||||
if (dot) dot.style.backgroundColor = "#6b7280";
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Grey out summary current stats
|
// Grey out summary current stats
|
||||||
|
|||||||
Reference in new issue
Block a user