Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion ui/admin/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,6 @@ <h1 class="sec-title" id="ev-title">General</h1>
</div></div>
</div>

<script type="module" src="/js/admin.js?v=4c1146ce"></script>
<script type="module" src="/js/admin.js?v=a760c511"></script>
</body>
</html>
2 changes: 1 addition & 1 deletion ui/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,5 @@
</div>
</div>
</dialog>
<script type="module" src="/js/dashboard.js?v=3290dfd1"></script>
<script type="module" src="/js/dashboard.js?v=18f7694e"></script>
</body></html>
2 changes: 1 addition & 1 deletion ui/js/admin-app-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
swapContent,
} from '/js/admin-shared.js?v=38ed2b51';
import { createListbox } from '/js/listbox.js?v=ae7b9a38';
import { MAX_LABELS } from '/js/badge-logic.js?v=e3c32915';
import { MAX_LABELS } from '/js/badge-logic.js?v=5c72bd4c';
import { renderColorControl, BADGE_DEFAULT } from '/js/admin-color-control.js?v=44fdebde';
import { badgeErrorAdvice, TONE } from '/js/admin-error.js?v=b0881957';
import { fluidHoverClear, fluidHoverKb } from '/js/fluid-hover.js?v=cb886e86';
Expand Down
2 changes: 1 addition & 1 deletion ui/js/admin.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { buildAppForm, buildFolderForm, captureActLabels, serializeKvRows } from '/js/admin-app-form.js?v=f7f58a4d';
import { buildAppForm, buildFolderForm, captureActLabels, serializeKvRows } from '/js/admin-app-form.js?v=a896a105';
import { checkAuth, requireLogin, wirePasswordStrength } from '/js/admin-auth.js?v=8aee98e6';
import { recoveryShown } from '/js/config-recovery.js?v=e22b5e5a';
import { focusRow, initList, render, syncFilterUI } from '/js/admin-list.js?v=547999d2';
Expand Down
8 changes: 4 additions & 4 deletions ui/js/badge-logic.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,11 +106,11 @@ export function healthReason(detail, translate) {
if (detail.state === 'unknown') {
parts.push(tr('status.containerNotFound'));
} else if (detail.state && detail.state !== 'running') {
/* Untranslated. The text comes from the daemon. */
const known = CONTAINER_STATES[detail.state];
parts.push(
_clip(detail.status || (known ? tr(known) : tr('status.containerState', { state: detail.state })), REASON_MAX),
);
const state = known ? tr(known) : tr('status.containerState', { state: detail.state });
const daemon = String(detail.status ?? '').trim();
const room = Math.max(8, REASON_MAX - state.length - 3);
parts.push(_clip(daemon ? `${state} · ${_clip(daemon, room)}` : state, REASON_MAX));
} else if (detail.status && /unhealthy/i.test(detail.status)) {
parts.push(_clip(detail.status, REASON_MAX));
}
Expand Down
2 changes: 1 addition & 1 deletion ui/js/dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import {
resetMobileChrome,
mkFolderGlyph,
} from '/js/ui.js?v=fd480686';
import { badgeMinimum, badgeSignature, computeBadgeVisual, readBadgeUpdate } from '/js/badge-logic.js?v=e3c32915';
import { badgeMinimum, badgeSignature, computeBadgeVisual, readBadgeUpdate } from '/js/badge-logic.js?v=5c72bd4c';
import { formatNumber } from '/js/format-number.js?v=52226e9c';
import { closeBadgePopover, wireBadgePopover } from '/js/badge-popover.js?v=9f510a4c';
import { observeGlass } from '/js/glass-rim.js?v=3faec233';
Expand Down
11 changes: 7 additions & 4 deletions ui/test/badge-logic.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,11 @@ test('a user colour is always given its own ink', () => {
The reason is the tile's hover text, and is appended to the accessible label
so it is not sight-only. */

test('a stopped container explains itself using Docker wording', () => {
assert.equal(healthReason({ state: 'exited', status: 'Exited (1) 2 hours ago' }), 'Exited (1) 2 hours ago');
test('a stopped container names its state, then Docker wording', () => {
assert.equal(
healthReason({ state: 'exited', status: 'Exited (1) 2 hours ago' }),
'Container stopped · Exited (1) 2 hours ago',
);
});

test('a container with no status falls back to its state', () => {
Expand Down Expand Up @@ -261,8 +264,8 @@ test('an unhealthy badge carries the reason as hover text', () => {
hasHC: true,
healthDetail: { state: 'exited', status: 'Exited (1)' },
});
assert.equal(v.title, 'Exited (1)');
assert.match(v.aria, /needs attention: Exited \(1\)/, 'and is not sight-only');
assert.equal(v.title, 'Container stopped · Exited (1)');
assert.match(v.aria, /needs attention: Container stopped · Exited \(1\)/, 'and is not sight-only');
});

test('a healthy badge carries no hover text', () => {
Expand Down
18 changes: 13 additions & 5 deletions ui/test/dashboard-localisation.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -136,9 +136,6 @@ test('the reason a tile is red is translated', () => {
assert.match(healthReason({ pingStatus: 503 }, zh), /503/, 'the status code must survive');
});

/* Docker's own text comes from the daemon, so inventing a translation for
"Exited (1) 2 hours ago" would be guessing at a string this code never
produces. */
test('a pending count takes the plural form of its language', () => {
const es = translatorFor('es.json');
assert.equal(computeBadgeVisual({ health: 0, activity: 1, hasHC: true, translate: es }).aria, '1 pendiente');
Expand All @@ -153,9 +150,20 @@ test('a container state without daemon text is named in the reader language', ()
assert.equal(healthReason({ state: 'mystery' }), 'Container mystery');
});

test("Docker's own status text is passed through untranslated", () => {
test("the translated state leads and Docker's own text follows it", () => {
const fa = translatorFor('fa.json');
assert.equal(healthReason({ state: 'exited', status: 'Exited (1) 2 hours ago' }, fa), 'Exited (1) 2 hours ago');
assert.equal(
healthReason({ state: 'exited', status: 'Exited (1) 2 hours ago' }, fa),
`${fa('status.containerExited')} · Exited (1) 2 hours ago`,
);
});

test('a long Docker status is trimmed, never the translated state', () => {
const de = translatorFor('de.json');
const r = healthReason({ state: 'restarting', status: 'Restarting '.repeat(20) }, de);
assert.ok(r.startsWith(`${de('status.containerRestarting')} · Restarting`), r);
assert.ok(r.length <= 90, `too long: ${r.length}`);
assert.match(r, /…$/);
});

/* ── the fallback ─────────────────────────────────────────────────────────── */
Expand Down
53 changes: 53 additions & 0 deletions ui/test/system-summary-link-press.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import { test } from 'node:test';

const src = fs.readFileSync(new URL('../widgets/system-summary/index.html', import.meta.url), 'utf8');

function lift(name, sig) {
const m = src.match(new RegExp(`function ${name}\\(${sig}\\) \\{[\\s\\S]*?\\n\\}`));
assert.ok(m, `${name} is defined in the widget`);
return new Function(`${m[0]}; return ${name};`)();
}
const pressOpensLink = new Function(
`${/function pressOpensLink\(el\) \{.*\}/.exec(src)?.[0]}; return pressOpensLink;`,
)();
const holdToShow = lift('holdToShow', 'target, show');

function node(classes = [], parent = null) {
const el = {
parent,
classes: new Set(classes),
classList: { add: c => el.classes.add(c) },
addEventListener() {},
closest(sel) {
const wanted = sel.split(',').map(s => s.trim().slice(1));
for (let n = el; n; n = n.parent) if (wanted.some(c => n.classes.has(c))) return n;
return null;
},
};
return el;
}

test('a press on a row that shows disk sizes does not open the link', () => {
const body = node();
const row = node(['row'], body);
const label = node(['lbl'], row);
holdToShow(row, () => {});
assert.equal(pressOpensLink(label), false);
assert.equal(pressOpensLink(row), false);
});

test('a press on a chart does not open the link', () => {
assert.equal(pressOpensLink(node([], node(['plot']))), false);
});

test('a press anywhere else opens the link', () => {
const body = node();
assert.equal(pressOpensLink(node(['val'], node(['row'], body))), true);
assert.equal(pressOpensLink(body), true);
});

test('the body click handler opens the link only through that check', () => {
assert.match(src, /if \(target && pressOpensLink\(e\.target\)\) openUrl\(target\);/);
});
7 changes: 5 additions & 2 deletions ui/widgets/system-summary/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -157,9 +157,9 @@
document.body.classList.toggle('linked', linkTo(linkEl, target));
};
setLink(null);
/* The charts take presses for their own detail, so a press there never opens the link. */
/* The charts and disk rows take presses for their own detail, so a press there never opens the link. */
document.body.addEventListener('click', e => {
if (target && !e.target.closest('.plot, .disk')) openUrl(target);
if (target && pressOpensLink(e.target)) openUrl(target);
});

poll({ fetch: fetchStats, interval: tickMs,
Expand Down Expand Up @@ -276,7 +276,10 @@
}
}

function pressOpensLink(el) { return !el.closest('.plot, .hold'); }

function holdToShow(target, show) {
target.classList.add('hold');
target.addEventListener('pointerenter', e => { if (e.pointerType === 'mouse') show(true); });
target.addEventListener('pointerdown', () => show(true));
target.addEventListener('pointerleave', () => show(false));
Expand Down
Loading