|
@@ -1379,7 +1379,6 @@ if (!colors['2500']) colors['2500'] = '#17C3B2';
|
|
|
buildAlarms(snap);
|
|
buildAlarms(snap);
|
|
|
buildOffline(snap);
|
|
buildOffline(snap);
|
|
|
buildTerminals(snap);
|
|
buildTerminals(snap);
|
|
|
- buildHistory();
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function renderFull(snap) {
|
|
function renderFull(snap) {
|
|
@@ -1888,66 +1887,155 @@ if (!colors['2500']) colors['2500'] = '#17C3B2';
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function buildHistory() {
|
|
function buildHistory() {
|
|
|
|
|
+ var picker = el('l2-ts-picker');
|
|
|
|
|
+ if (!picker || picker.getAttribute('data-built') === '1') return;
|
|
|
|
|
+ picker.setAttribute('data-built', '1');
|
|
|
|
|
+
|
|
|
|
|
+ var trigger = el('l2-ts-trigger');
|
|
|
|
|
+ var pop = el('l2-ts-pop');
|
|
|
|
|
+ var daysEl = el('l2-ts-days');
|
|
|
|
|
+ var timesEl = el('l2-ts-times');
|
|
|
|
|
+ var timesTitle = el('l2-ts-times-title');
|
|
|
|
|
+ var label = el('l2-ts-current');
|
|
|
var list = window.L2_HISTORY || [];
|
|
var list = window.L2_HISTORY || [];
|
|
|
- var dateSel = el('l2-ts-date');
|
|
|
|
|
- var timeSel = el('l2-ts-time');
|
|
|
|
|
- if (!dateSel || !timeSel) return;
|
|
|
|
|
- var used = (window.L2_META && !window.L2_META.isLive && window.L2_META.usedTs) ? String(window.L2_META.usedTs) : '';
|
|
|
|
|
- // Two-stage picker: the first select lists only days that really have
|
|
|
|
|
- // snapshots; the second lists only times recorded on that day. A user can
|
|
|
|
|
- // therefore never submit an arbitrary timestamp or get a blank page.
|
|
|
|
|
|
|
+ var meta = window.L2_META || {};
|
|
|
|
|
+ var used = (!meta.isLive && meta.usedTs) ? String(meta.usedTs) : '';
|
|
|
var groups = {}, days = [];
|
|
var groups = {}, days = [];
|
|
|
|
|
+
|
|
|
|
|
+ // Group only timestamps that actually exist in l2_topology_history.
|
|
|
|
|
+ // The popover therefore never offers an arbitrary date/time and cannot
|
|
|
|
|
+ // lead to a blank page.
|
|
|
list.forEach(function (it) {
|
|
list.forEach(function (it) {
|
|
|
var day = String(it.captured_at || fmtTs(it.ts)).slice(0, 10);
|
|
var day = String(it.captured_at || fmtTs(it.ts)).slice(0, 10);
|
|
|
if (!groups[day]) { groups[day] = []; days.push(day); }
|
|
if (!groups[day]) { groups[day] = []; days.push(day); }
|
|
|
groups[day].push(it);
|
|
groups[day].push(it);
|
|
|
});
|
|
});
|
|
|
- var usedDay = '';
|
|
|
|
|
|
|
+
|
|
|
|
|
+ var currentDay = '';
|
|
|
|
|
+ var currentTs = '';
|
|
|
if (used) {
|
|
if (used) {
|
|
|
list.forEach(function (it) {
|
|
list.forEach(function (it) {
|
|
|
- if (String(it.ts) === used) usedDay = String(it.captured_at || fmtTs(it.ts)).slice(0, 10);
|
|
|
|
|
|
|
+ if (String(it.ts) === used) {
|
|
|
|
|
+ currentTs = used;
|
|
|
|
|
+ currentDay = String(it.captured_at || fmtTs(it.ts)).slice(0, 10);
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function timeText(it) {
|
|
|
|
|
+ var ca = String(it.captured_at || '');
|
|
|
|
|
+ return ca.length >= 19 ? ca.slice(11, 19) : fmtTs(it.ts);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function go(ts) {
|
|
|
|
|
+ var u = new URL(window.location.href);
|
|
|
|
|
+ u.searchParams.set('action', 'l2.topology.view');
|
|
|
|
|
+ if (ts) u.searchParams.set('ts', ts);
|
|
|
|
|
+ else u.searchParams.delete('ts');
|
|
|
|
|
+ window.location.href = u.toString();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function positionPop() {
|
|
|
|
|
+ if (!pop) return;
|
|
|
|
|
+ if (window.matchMedia('(max-width: 720px)').matches) {
|
|
|
|
|
+ var rect = trigger.getBoundingClientRect();
|
|
|
|
|
+ var width = Math.min(430, window.innerWidth - 20);
|
|
|
|
|
+ var left = Math.max(10, Math.min(rect.left, window.innerWidth - 10 - width));
|
|
|
|
|
+ pop.style.position = 'fixed';
|
|
|
|
|
+ pop.style.left = left + 'px';
|
|
|
|
|
+ pop.style.right = 'auto';
|
|
|
|
|
+ pop.style.top = Math.max(10, rect.bottom + 6) + 'px';
|
|
|
|
|
+ pop.style.width = width + 'px';
|
|
|
|
|
+ }
|
|
|
|
|
+ else {
|
|
|
|
|
+ pop.style.position = '';
|
|
|
|
|
+ pop.style.left = '';
|
|
|
|
|
+ pop.style.right = '';
|
|
|
|
|
+ pop.style.top = '';
|
|
|
|
|
+ pop.style.width = '';
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function setExpanded(open) {
|
|
|
|
|
+ picker.classList.toggle('open', open);
|
|
|
|
|
+ trigger.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
|
|
|
+ if (open) positionPop();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function updateLabel() {
|
|
|
|
|
+ if (!currentDay) { label.textContent = t('l2.time_live_full', '实时(最新)'); return; }
|
|
|
|
|
+ if (!currentTs) { label.textContent = currentDay + ' · ' + t('l2.time_choose', '选择时间'); return; }
|
|
|
|
|
+ var found = null;
|
|
|
|
|
+ (groups[currentDay] || []).forEach(function (it) { if (String(it.ts) === currentTs) found = it; });
|
|
|
|
|
+ label.textContent = found ? currentDay + ' ' + timeText(found) : currentDay;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function renderDays() {
|
|
|
|
|
+ daysEl.innerHTML = '';
|
|
|
|
|
+ var live = document.createElement('button');
|
|
|
|
|
+ live.type = 'button';
|
|
|
|
|
+ live.className = 'l2-ts-day' + (currentDay ? '' : ' active');
|
|
|
|
|
+ live.textContent = t('l2.time_live_full', '实时(最新)');
|
|
|
|
|
+ live.addEventListener('click', function () { go(''); });
|
|
|
|
|
+ daysEl.appendChild(live);
|
|
|
|
|
+ days.forEach(function (day) {
|
|
|
|
|
+ var b = document.createElement('button');
|
|
|
|
|
+ b.type = 'button';
|
|
|
|
|
+ b.className = 'l2-ts-day' + (day === currentDay ? ' active' : '');
|
|
|
|
|
+ b.textContent = day + ' (' + groups[day].length + ')';
|
|
|
|
|
+ b.addEventListener('click', function (ev) {
|
|
|
|
|
+ ev.stopPropagation();
|
|
|
|
|
+ currentDay = day;
|
|
|
|
|
+ currentTs = '';
|
|
|
|
|
+ renderDays();
|
|
|
|
|
+ renderTimes();
|
|
|
|
|
+ updateLabel();
|
|
|
|
|
+ });
|
|
|
|
|
+ daysEl.appendChild(b);
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
- dateSel.innerHTML = '';
|
|
|
|
|
- var live = document.createElement('option');
|
|
|
|
|
- live.value = '';
|
|
|
|
|
- live.textContent = t('l2.time_live', 'Live');
|
|
|
|
|
- dateSel.appendChild(live);
|
|
|
|
|
- days.forEach(function (day) {
|
|
|
|
|
- var opt = document.createElement('option');
|
|
|
|
|
- opt.value = day;
|
|
|
|
|
- opt.textContent = day + ' (' + groups[day].length + ')';
|
|
|
|
|
- dateSel.appendChild(opt);
|
|
|
|
|
- });
|
|
|
|
|
- dateSel.value = usedDay || '';
|
|
|
|
|
-
|
|
|
|
|
- function fillTimes(preferTs) {
|
|
|
|
|
- var day = dateSel.value;
|
|
|
|
|
- timeSel.innerHTML = '';
|
|
|
|
|
- if (!day) {
|
|
|
|
|
- var dash = document.createElement('option');
|
|
|
|
|
- dash.value = '';
|
|
|
|
|
- dash.textContent = '--:--';
|
|
|
|
|
- timeSel.appendChild(dash);
|
|
|
|
|
- timeSel.disabled = true;
|
|
|
|
|
|
|
+
|
|
|
|
|
+ function renderTimes() {
|
|
|
|
|
+ timesEl.innerHTML = '';
|
|
|
|
|
+ if (!currentDay) {
|
|
|
|
|
+ timesTitle.textContent = t('l2.time_live', '实时');
|
|
|
|
|
+ var empty = document.createElement('div');
|
|
|
|
|
+ empty.className = 'l2-ts-empty';
|
|
|
|
|
+ empty.textContent = t('l2.time_live_hint', '当前显示最新采集数据');
|
|
|
|
|
+ timesEl.appendChild(empty);
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
- var items = groups[day] || [];
|
|
|
|
|
- items.forEach(function (it) {
|
|
|
|
|
- var opt = document.createElement('option');
|
|
|
|
|
- opt.value = String(it.ts || '');
|
|
|
|
|
- var ca = String(it.captured_at || '');
|
|
|
|
|
- opt.textContent = ca.length >= 19 ? ca.slice(11, 19) : fmtTs(it.ts);
|
|
|
|
|
- timeSel.appendChild(opt);
|
|
|
|
|
|
|
+ timesTitle.textContent = currentDay;
|
|
|
|
|
+ (groups[currentDay] || []).forEach(function (it) {
|
|
|
|
|
+ var b = document.createElement('button');
|
|
|
|
|
+ b.type = 'button';
|
|
|
|
|
+ b.className = 'l2-ts-time' + (String(it.ts) === currentTs ? ' active' : '');
|
|
|
|
|
+ b.textContent = timeText(it);
|
|
|
|
|
+ b.addEventListener('click', function () { go(String(it.ts)); });
|
|
|
|
|
+ timesEl.appendChild(b);
|
|
|
});
|
|
});
|
|
|
- timeSel.disabled = false;
|
|
|
|
|
- var chosen = preferTs && items.some(function (it) { return String(it.ts) === String(preferTs); }) ? String(preferTs) : '';
|
|
|
|
|
- if (!chosen && items.length) chosen = String(items[0].ts || '');
|
|
|
|
|
- timeSel.value = chosen;
|
|
|
|
|
}
|
|
}
|
|
|
- fillTimes(used);
|
|
|
|
|
- dateSel.onchange = function () { fillTimes(''); };
|
|
|
|
|
- timeSel.onchange = null;
|
|
|
|
|
|
|
+
|
|
|
|
|
+ trigger.addEventListener('click', function (ev) {
|
|
|
|
|
+ ev.stopPropagation();
|
|
|
|
|
+ setExpanded(!picker.classList.contains('open'));
|
|
|
|
|
+ });
|
|
|
|
|
+ window.addEventListener('resize', function () {
|
|
|
|
|
+ if (picker.classList.contains('open')) positionPop();
|
|
|
|
|
+ });
|
|
|
|
|
+ window.addEventListener('scroll', function () {
|
|
|
|
|
+ if (picker.classList.contains('open')) positionPop();
|
|
|
|
|
+ }, {passive: true});
|
|
|
|
|
+ document.addEventListener('click', function (ev) {
|
|
|
|
|
+ if (!picker.contains(ev.target)) setExpanded(false);
|
|
|
|
|
+ });
|
|
|
|
|
+ document.addEventListener('keydown', function (ev) {
|
|
|
|
|
+ if (ev.key === 'Escape') setExpanded(false);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ renderDays();
|
|
|
|
|
+ renderTimes();
|
|
|
|
|
+ updateLabel();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[c]; }); }
|
|
function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, function (c) { return ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[c]; }); }
|
|
@@ -2375,20 +2463,6 @@ if (!colors['2500']) colors['2500'] = '#17C3B2';
|
|
|
if (!ev.target.closest('.l2-search-wrap')) el('l2-sug').style.display = 'none';
|
|
if (!ev.target.closest('.l2-search-wrap')) el('l2-sug').style.display = 'none';
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- if (el('l2-ts-go')) {
|
|
|
|
|
- var goTs = function () {
|
|
|
|
|
- var dateSel = el('l2-ts-date');
|
|
|
|
|
- var timeSel = el('l2-ts-time');
|
|
|
|
|
- if (!dateSel || !timeSel) return;
|
|
|
|
|
- var u = new URL(window.location.href);
|
|
|
|
|
- u.searchParams.set('action', 'l2.topology.view');
|
|
|
|
|
- if (dateSel.value && timeSel.value) u.searchParams.set('ts', timeSel.value);
|
|
|
|
|
- else u.searchParams.delete('ts');
|
|
|
|
|
- window.location.href = u.toString();
|
|
|
|
|
- };
|
|
|
|
|
- el('l2-ts-go').addEventListener('click', goTs);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
el('l2-pause').addEventListener('click', function () {
|
|
el('l2-pause').addEventListener('click', function () {
|
|
|
paused = !paused;
|
|
paused = !paused;
|
|
|
el('l2-pause').classList.toggle('on', paused);
|
|
el('l2-pause').classList.toggle('on', paused);
|
|
@@ -2422,6 +2496,7 @@ if (!colors['2500']) colors['2500'] = '#17C3B2';
|
|
|
/* ---------------- boot ---------------- */
|
|
/* ---------------- boot ---------------- */
|
|
|
function boot() {
|
|
function boot() {
|
|
|
setupControls();
|
|
setupControls();
|
|
|
|
|
+ buildHistory();
|
|
|
fitViewport();
|
|
fitViewport();
|
|
|
try {
|
|
try {
|
|
|
var qs = new URLSearchParams(window.location.search);
|
|
var qs = new URLSearchParams(window.location.search);
|