mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-05 18:44:03 +08:00
💋 Sync 2026-11-02 01:02:56
This commit is contained in:
1 parent
d97dc35e3d
commit
2f941f0ffb
48 files changed
+4891
-1002
No files matched your search
@@ -738,6 +738,27 @@
|
||||
vertical-align: middle; margin-right: 6px;
|
||||
}
|
||||
|
||||
/* The poll indicator (#106). It must never take part in layout: a fixed-size
|
||||
slot that is always present, faded in rather than shown, because the banner
|
||||
it replaces reflowed the page every couple of seconds — which is what made
|
||||
the rate chips below it hard to hit. */
|
||||
.tc-poll-spinner {
|
||||
display: inline-block; flex: 0 0 12px; width: 12px; height: 12px;
|
||||
border: 2px solid var(--tc-border);
|
||||
border-top-color: var(--tc-faint);
|
||||
border-radius: 50%;
|
||||
animation: tc-spin .7s linear infinite;
|
||||
opacity: 0; transition: opacity .25s ease;
|
||||
}
|
||||
.tc-poll-spinner.is-busy { opacity: 1; }
|
||||
/* A refresh that failed is the one thing the poll has to say. Same slot, so
|
||||
saying it still cannot move anything; the message is in the title. */
|
||||
.tc-poll-spinner.is-error {
|
||||
opacity: 1; animation: none;
|
||||
border-color: var(--tc-err); background: var(--tc-err);
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
/* ── Link style ──────────────────────────────────────────────────── */
|
||||
.tc-link { color: inherit; text-decoration: none; border-bottom: 1px dotted var(--tc-faint); }
|
||||
.tc-link:hover { border-bottom-style: solid; }
|
||||
|
||||
@@ -1397,6 +1397,13 @@ function setStatus(el, type, msg) {
|
||||
el.innerHTML = type === 'loading' ? '<span class="tc-spinner"></span>'+escHtml(msg) : escHtml(msg);
|
||||
}
|
||||
|
||||
// setStatus rewrites className wholesale, which drops the tc-hidden the status
|
||||
// element starts with — so putting it back has to be explicit.
|
||||
function clearStatus(el) {
|
||||
el.className = 'tc-hidden';
|
||||
el.innerHTML = '';
|
||||
}
|
||||
|
||||
function updateUrlParams(opts) {
|
||||
var params = new URLSearchParams();
|
||||
if (opts.lastIp && opts.lastIp !== '__all__') params.set('ip', opts.lastIp);
|
||||
@@ -2091,6 +2098,39 @@ return view.extend({
|
||||
});
|
||||
|
||||
var statusDiv = E('div', { 'class': 'tc-hidden' });
|
||||
|
||||
// #106: a refresh that runs on a timer must not announce itself on the
|
||||
// status line. It gets its own indicator, which cannot disturb the
|
||||
// page: a fixed-size slot that is always in the layout and is only
|
||||
// faded in, never displayed or hidden (tc-hidden is display:none, which
|
||||
// is exactly the reflow being fixed). It waits before showing, so on a
|
||||
// router that answers quickly it never appears at all — a spinner that
|
||||
// blinks for 80ms every two seconds is still a flash.
|
||||
//
|
||||
// A refresh reports its own health HERE and nowhere else. The status
|
||||
// line belongs to what the operator did: every other poller in this
|
||||
// view discards its errors outright, and these two were the only ones
|
||||
// writing to a shared element, which is the whole of #106.
|
||||
var pollSpinner = E('span', { 'class': 'tc-poll-spinner' });
|
||||
var _pollBusy = 0, _pollShowTimer = null;
|
||||
function pollStart() {
|
||||
_pollBusy++;
|
||||
if (_pollShowTimer === null) {
|
||||
_pollShowTimer = setTimeout(function() {
|
||||
_pollShowTimer = null;
|
||||
if (_pollBusy > 0) pollSpinner.classList.add('is-busy');
|
||||
}, 250);
|
||||
}
|
||||
}
|
||||
// err: the message when this refresh failed, nothing when it worked.
|
||||
function pollEnd(err) {
|
||||
if (_pollBusy > 0) _pollBusy--;
|
||||
if (_pollBusy > 0) return;
|
||||
if (_pollShowTimer !== null) { clearTimeout(_pollShowTimer); _pollShowTimer = null; }
|
||||
pollSpinner.classList.remove('is-busy');
|
||||
pollSpinner.classList.toggle('is-error', !!err);
|
||||
pollSpinner.title = err || '';
|
||||
}
|
||||
var statsDiv = E('div', { 'style': 'margin:8px 0', 'class': opts.showStats === false ? 'tc-hidden' : '' });
|
||||
var connsDiv = E('div', { 'class': opts.showConns === false ? 'tc-hidden' : '' });
|
||||
|
||||
@@ -3043,11 +3083,11 @@ return view.extend({
|
||||
self._queryGen++;
|
||||
var gen = self._queryGen;
|
||||
|
||||
setStatus(statusDiv, 'loading', _('Running…'));
|
||||
pollStart();
|
||||
|
||||
callDevice(ip, proto).then(function(data) {
|
||||
if (!data || data.error) {
|
||||
setStatus(statusDiv, 'error', (data && data.error) || _('Unknown error'));
|
||||
pollEnd((data && data.error) || _('Unknown error'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3188,9 +3228,9 @@ return view.extend({
|
||||
}
|
||||
}
|
||||
}
|
||||
setStatus(statusDiv, 'ok', '✓ ' + _('Done'));
|
||||
pollEnd();
|
||||
})
|
||||
.catch(function(e) { setStatus(statusDiv, 'error', '✗ '+e.message); });
|
||||
.catch(function(e) { pollEnd(e.message); });
|
||||
}
|
||||
|
||||
self._tableFilter = null;
|
||||
@@ -3368,7 +3408,7 @@ return view.extend({
|
||||
}
|
||||
|
||||
function runAll() {
|
||||
setStatus(statusDiv, 'loading', _('Scanning all devices…'));
|
||||
pollStart();
|
||||
|
||||
callTrafficctl().then(function(rows) {
|
||||
if (!Array.isArray(rows)) rows = [];
|
||||
@@ -3385,10 +3425,10 @@ return view.extend({
|
||||
netifyMap = m;
|
||||
}).catch(function() {});
|
||||
}
|
||||
setStatus(statusDiv, 'ok', '✓ ' + _('Done'));
|
||||
pollEnd();
|
||||
self._startBytesPoll();
|
||||
})
|
||||
.catch(function(e) { setStatus(statusDiv, 'error', '✗ '+e.message); });
|
||||
.catch(function(e) { pollEnd(e.message); });
|
||||
}
|
||||
|
||||
function updateExtendedStats() {
|
||||
@@ -3405,6 +3445,11 @@ return view.extend({
|
||||
|
||||
function runQuery() {
|
||||
var ip = searchSelect.getValue();
|
||||
// The poll used to wipe the status line on every tick, which also
|
||||
// disposed of a result belonging to the device just left. Now that
|
||||
// it does not, switching devices has to do it — otherwise
|
||||
// "Limiting 192.168.0.50…" lingers over another device's page.
|
||||
if (self._statusIp !== ip) { clearStatus(statusDiv); self._statusIp = ip; }
|
||||
var o = loadOpts(); o.lastIp = ip; saveOpts(o);
|
||||
updateUrlParams(o);
|
||||
updateModeUI();
|
||||
@@ -3464,12 +3509,21 @@ return view.extend({
|
||||
var action = inetBtn._action;
|
||||
var fn = action === 'block' ? callBlock : callUnblock;
|
||||
fn(ip, '').then(function(res) {
|
||||
// A block without a known MAC covers IPv4 only, and the backend
|
||||
// says so in msg. Swallowing it here would put the UI back to
|
||||
// reporting a block that does not block (issue #67).
|
||||
if (res && res.ipv6 === false) {
|
||||
setStatus(statusDiv, 'error', (res && res.msg) || '?');
|
||||
// The result is shown whatever it says, like every other action.
|
||||
// Reporting it only when res.ipv6 === false discarded the rest
|
||||
// of what the backend qualifies: "NOT PERSISTENT" (#105) and the
|
||||
// NSS note (#81) are appended to msg on a SUCCESSFUL block, so
|
||||
// blocking a device — the one path that mattered to the reporter
|
||||
// of #105 — said nothing at all.
|
||||
var state;
|
||||
if (!res || !res.ok || res.ipv6 === false) {
|
||||
// A failure, or a block that does not block over v6 (#67).
|
||||
state = 'error';
|
||||
} else {
|
||||
// The device is now cut off, so not green.
|
||||
state = (action === 'block') ? 'action' : 'ok';
|
||||
}
|
||||
setStatus(statusDiv, state, (res && res.msg) || '?');
|
||||
runQuery();
|
||||
}).catch(function(e) {
|
||||
setStatus(statusDiv, 'error', e.message);
|
||||
@@ -4113,7 +4167,7 @@ return view.extend({
|
||||
offloadBanner,
|
||||
cutPanel,
|
||||
E('div', {'style':'margin-bottom:10px'}, [
|
||||
E('div', {'style':'display:flex;align-items:center;gap:10px;flex-wrap:wrap'}, [searchSelect.el, actionRow]),
|
||||
E('div', {'style':'display:flex;align-items:center;gap:10px;flex-wrap:wrap'}, [searchSelect.el, actionRow, pollSpinner]),
|
||||
quickBar
|
||||
]),
|
||||
statusDiv,
|
||||
|
||||
Reference in new issue
Block a user