RTU/test/web_root/js/pages.js

687 lines
28 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* pages.js — 页面渲染器
*
* 每个页面对象实现:
* render(container) — 渲染 HTML 到容器
* onEnter() — 页面激活时调用
* onLeave() — 页面离开时调用
* onData(signalType, arr) — 接收下行数据更新表格
*
* 术语映射:
* out → 注册 | in → 链接注册 | yk → 遥控 | ao → 参数 | param → 定值
*/
var CTRL_TYPE_MAP = { 0: '只读', 1: '直控', 2: '选控' };
/* ========================================================================
* 共用工具栏 HTML
* ======================================================================== */
function makeToolbar(signalType, showSet) {
var typeLabel = signalType === 'ao' ? '参数' : (signalType === 'param' ? '定值' : signalType);
return ''
+ '<div class="toolbar">'
+ '<div class="field">'
+ '<label>短地址 saddr</label>'
+ '<input type="text" id="tb-saddr" placeholder="例: iec.run_cnt">'
+ '</div>'
+ '<div class="field">'
+ '<label>操作</label>'
+ '<select id="tb-curd">'
+ '<option value="add">add 添加</option>'
+ '<option value="del">del 删除</option>'
+ '</select>'
+ '</div>'
+ '<button class="btn btn-primary" id="tb-exec">执行</button>'
+ '<button class="btn btn-outline" id="tb-clear">清空输入</button>'
+ '</div>';
}
function bindToolbar(signalType) {
var execBtn = document.getElementById('tb-exec');
var clearBtn = document.getElementById('tb-clear');
var saddrInput = document.getElementById('tb-saddr');
var curdSelect = document.getElementById('tb-curd');
if (!execBtn || !clearBtn) return;
execBtn.onclick = function() {
var saddr = saddrInput.value.trim();
var curd = curdSelect.value;
if (!saddr) {
alert('请输入短地址 saddr');
return;
}
if (!WsClient.send({ saddr: saddr, signal_type: signalType, curd: curd, setting_zone: '0', signal_data: '' })) {
alert('WebSocket 未连接');
}
};
clearBtn.onclick = function() {
saddrInput.value = '';
};
}
/* ========================================================================
* 表格渲染工具
* ======================================================================== */
function renderTable(containerId, headers, rowsHtml, emptyText) {
var html = '<div class="table-card"><div class="table-wrapper"><table class="signal-table">';
html += '<thead><tr>';
for (var i = 0; i < headers.length; i++) {
html += '<th>' + headers[i] + '</th>';
}
html += '</tr></thead><tbody id="' + containerId + '">';
if (rowsHtml) {
html += rowsHtml;
} else {
html += '<tr class="empty-row"><td colspan="' + headers.length + '">' + (emptyText || '暂无数据,请先添加信号') + '</td></tr>';
}
html += '</tbody></table></div></div>';
return html;
}
function fmtVal(v) {
if (v === null || v === undefined || v === '') return '-';
return String(v).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
/* ========================================================================
* 1. LoginPage — 登录页
* ======================================================================== */
var LoginPage = {
render: function(container) {
container.innerHTML = ''
+ '<div class="login-card">'
+ '<h1>RTU 远程终端监控系统</h1>'
+ '<p class="login-subtitle">Remote Terminal Unit Web Monitor</p>'
+ '<div class="form-group">'
+ '<label>用户名</label>'
+ '<input type="text" id="login-user" placeholder="暂无需认证" disabled>'
+ '</div>'
+ '<div class="form-group">'
+ '<label>密码</label>'
+ '<input type="password" id="login-pass" placeholder="暂无需认证" disabled>'
+ '</div>'
+ '<button class="login-btn" id="login-btn">登 录</button>'
+ '<p class="login-hint">当前版本无需认证,点击登录即可进入系统</p>'
+ '</div>';
document.getElementById('login-btn').onclick = function() {
localStorage.setItem('rtu_logged_in', '1');
window.location.hash = '#dashboard';
};
},
onEnter: function() {
document.body.classList.add('login-page');
},
onLeave: function() {
document.body.classList.remove('login-page');
}
};
/* ========================================================================
* 2. DashboardPage — 功能入口主页
* ======================================================================== */
var DashboardPage = {
render: function(container) {
var cards = [
{ cls: 'out', icon: '📡', title: '注册', desc: 'out — 状态量信号注册与修改', hash: '#out' },
{ cls: 'in', icon: '📊', title: '链接注册', desc: 'in — 测量量信号链接注册与监视', hash: '#in' },
{ cls: 'yk', icon: '⚡', title: '遥控', desc: 'yk — 遥控分合闸控制', hash: '#yk' },
{ cls: 'ao', icon: '🔧', title: '参数', desc: 'ao — 模拟输出参数设定', hash: '#ao' },
{ cls: 'param', icon: '⚙', title: '定值', desc: 'param — 多定值区参数管理', hash: '#param' },
{ cls: 'monitor', icon: '📋', title: '数据监控', desc: '实时查看 WebSocket 交互数据', hash: '#monitor' }
];
var html = '<div class="page-header"><h2>功能导航</h2><p>选择需要管理的信号类型进入对应功能页</p></div>';
html += '<div class="dashboard-grid">';
for (var i = 0; i < cards.length; i++) {
var c = cards[i];
html += '<a class="dash-card ' + c.cls + '" href="' + c.hash + '">'
+ '<div class="card-icon">' + c.icon + '</div>'
+ '<div class="card-title">' + c.title + '</div>'
+ '<div class="card-desc">' + c.desc + '</div>'
+ '</a>';
}
html += '</div>';
container.innerHTML = html;
},
onEnter: function() {},
onLeave: function() {}
};
/* ========================================================================
* 3. OutPage — 注册管理 (out)
* ======================================================================== */
var OutPage = (function() {
var data = [];
function renderTableHtml() {
if (data.length === 0) return '';
var rows = '';
for (var i = 0; i < data.length; i++) {
var item = data[i];
rows += '<tr>'
+ '<td>' + (i + 1) + '</td>'
+ '<td>' + fmtVal(item.saddr) + '</td>'
+ '<td>' + fmtVal(item.desc) + '</td>'
+ '<td>' + fmtVal(item.type) + '</td>'
+ '<td class="val-cell">' + fmtVal(item.val) + '</td>'
+ '<td><input class="val-input" id="mod-' + i + '" placeholder="修改值"></td>'
+ '<td><button class="btn btn-sm btn-info" data-idx="' + i + '">修改</button></td>'
+ '</tr>';
}
return rows;
}
function bindModifyButtons() {
var buttons = document.querySelectorAll('#out-tbody .btn-info');
for (var k = 0; k < buttons.length; k++) {
buttons[k].onclick = function() {
var idx = parseInt(this.getAttribute('data-idx'));
var input = document.getElementById('mod-' + idx);
var val = input.value.trim();
if (val === '') { alert('请输入修改值'); return; }
var item = data[idx];
WsClient.send({
curd: 'set', signal_type: 'out', saddr: item.saddr,
signal_data: val, setting_zone: '0'
});
};
}
}
return {
render: function(container) {
var headers = ['#', 'saddr', 'desc', '数据类型', 'val', '修改值', '修改'];
container.innerHTML = ''
+ '<div class="page-header"><h2>注册管理</h2><p>out — 状态量信号,注册后支持添加、删除、修改值</p></div>'
+ makeToolbar('out')
+ renderTable('out-tbody', headers, renderTableHtml(), '暂无注册数据,请先添加信号');
bindToolbar('out');
bindModifyButtons();
},
onEnter: function() {},
onLeave: function() { data = []; },
onData: function(arr) {
data = arr || [];
var tbody = document.getElementById('out-tbody');
if (!tbody) return;
if (data.length === 0) {
tbody.innerHTML = '<tr class="empty-row"><td colspan="7">暂无注册数据,请先添加信号</td></tr>';
return;
}
tbody.innerHTML = renderTableHtml();
bindModifyButtons();
}
};
})();
/* ========================================================================
* 4. InPage — 链接注册管理 (in)
* ======================================================================== */
var InPage = (function() {
var data = [];
function renderTableHtml() {
if (data.length === 0) return '';
var rows = '';
for (var i = 0; i < data.length; i++) {
var item = data[i];
rows += '<tr>'
+ '<td>' + (i + 1) + '</td>'
+ '<td>' + fmtVal(item.saddr) + '</td>'
+ '<td>' + fmtVal(item.desc) + '</td>'
+ '<td>' + fmtVal(item.type) + '</td>'
+ '<td class="val-cell">' + fmtVal(item.val) + '</td>'
+ '</tr>';
}
return rows;
}
return {
render: function(container) {
var headers = ['#', 'saddr', 'desc', '数据类型', 'val'];
container.innerHTML = ''
+ '<div class="page-header"><h2>链接注册管理</h2><p>in — 测量量信号,链接注册后仅支持添加和删除,不支持修改</p></div>'
+ makeToolbar('in')
+ renderTable('in-tbody', headers, renderTableHtml(), '暂无链接注册数据,请先添加信号');
bindToolbar('in');
},
onEnter: function() {},
onLeave: function() { data = []; },
onData: function(arr) {
data = arr || [];
var tbody = document.getElementById('in-tbody');
if (!tbody) return;
if (data.length === 0) {
tbody.innerHTML = '<tr class="empty-row"><td colspan="5">暂无链接注册数据,请先添加信号</td></tr>';
return;
}
tbody.innerHTML = renderTableHtml();
}
};
})();
/* ========================================================================
* 5. YkPage — 遥控管理 (yk)
* ======================================================================== */
var YkPage = (function() {
var data = [];
function renderTableHtml() {
if (data.length === 0) return '';
var rows = '';
for (var i = 0; i < data.length; i++) {
var item = data[i];
var ctrlLabel = CTRL_TYPE_MAP[item.ctrl_type] || item.ctrl_type;
rows += '<tr>'
+ '<td>' + (i + 1) + '</td>'
+ '<td>' + fmtVal(item.saddr) + '</td>'
+ '<td>' + fmtVal(item.desc) + '</td>'
+ '<td>' + fmtVal(item.type) + '</td>'
+ '<td class="val-cell">' + fmtVal(item.val) + '</td>'
+ '<td>' + ctrlLabel + '</td>'
+ '<td><button class="btn btn-sm btn-success" data-idx="' + i + '" data-val="1">控合</button></td>'
+ '<td><button class="btn btn-sm btn-danger" data-idx="' + i + '" data-val="0">控分</button></td>'
+ '</tr>';
}
return rows;
}
function bindButtons() {
var buttons = document.querySelectorAll('#yk-tbody .btn-sm');
for (var k = 0; k < buttons.length; k++) {
buttons[k].onclick = function() {
var idx = parseInt(this.getAttribute('data-idx'));
var val = this.getAttribute('data-val');
var item = data[idx];
WsClient.send({
curd: 'set', signal_type: 'yk', saddr: item.saddr,
signal_data: val, setting_zone: '0'
});
};
}
}
return {
render: function(container) {
var headers = ['#', 'saddr', 'desc', '数据类型', 'val', 'ctrl_type', '控合', '控分'];
container.innerHTML = ''
+ '<div class="page-header"><h2>遥控管理</h2><p>yk — 遥控分合闸控制,控合=1(闭合),控分=0(断开)</p></div>'
+ makeToolbar('yk')
+ renderTable('yk-tbody', headers, renderTableHtml(), '暂无遥控数据,请先添加信号');
bindToolbar('yk');
bindButtons();
},
onEnter: function() {},
onLeave: function() { data = []; },
onData: function(arr) {
data = arr || [];
var tbody = document.getElementById('yk-tbody');
if (!tbody) return;
if (data.length === 0) {
tbody.innerHTML = '<tr class="empty-row"><td colspan="8">暂无遥控数据,请先添加信号</td></tr>';
return;
}
tbody.innerHTML = renderTableHtml();
bindButtons();
}
};
})();
/* ========================================================================
* 6. AoPage — 参数管理 (ao, 单值修改)
* ======================================================================== */
var AoPage = (function() {
var data = [];
function renderTableHtml() {
if (data.length === 0) return '';
var rows = '';
for (var i = 0; i < data.length; i++) {
var item = data[i];
var ctrlLabel = CTRL_TYPE_MAP[item.ctrl_type] || item.ctrl_type;
rows += '<tr>'
+ '<td>' + (i + 1) + '</td>'
+ '<td>' + fmtVal(item.saddr) + '</td>'
+ '<td>' + fmtVal(item.desc) + '</td>'
+ '<td>' + fmtVal(item.type) + '</td>'
+ '<td class="val-cell">' + fmtVal(item.val) + '</td>'
+ '<td>' + ctrlLabel + '</td>'
+ '<td>' + fmtVal(item.min) + '</td>'
+ '<td>' + fmtVal(item.max) + '</td>'
+ '<td>' + fmtVal(item.step) + '</td>'
+ '<td>' + fmtVal(item.unit) + '</td>'
+ '<td><input class="val-input" id="ao-mod-' + i + '" placeholder="修改值"></td>'
+ '<td><button class="btn btn-sm btn-info" data-idx="' + i + '">修改</button></td>'
+ '</tr>';
}
return rows;
}
function bindModifyButtons() {
var buttons = document.querySelectorAll('#ao-tbody .btn-info');
for (var k = 0; k < buttons.length; k++) {
buttons[k].onclick = function() {
var idx = parseInt(this.getAttribute('data-idx'));
var input = document.getElementById('ao-mod-' + idx);
var val = input.value.trim();
if (val === '') { alert('请输入修改值'); return; }
var item = data[idx];
WsClient.send({
curd: 'set', signal_type: 'ao', saddr: item.saddr,
signal_data: val, setting_zone: '0'
});
};
}
}
return {
render: function(container) {
var headers = ['#', 'saddr', 'desc', '数据类型', 'val', 'ctrl_type', 'min', 'max', 'step', 'unit', '修改值', '修改'];
container.innerHTML = ''
+ '<div class="page-header"><h2>参数管理</h2><p>ao — 模拟输出参数设定,支持单值修改</p></div>'
+ makeToolbar('ao')
+ renderTable('ao-tbody', headers, renderTableHtml(), '暂无参数数据,请先添加信号');
bindToolbar('ao');
bindModifyButtons();
},
onEnter: function() {},
onLeave: function() { data = []; },
onData: function(arr) {
data = arr || [];
var tbody = document.getElementById('ao-tbody');
if (!tbody) return;
if (data.length === 0) {
tbody.innerHTML = '<tr class="empty-row"><td colspan="12">暂无参数数据,请先添加信号</td></tr>';
return;
}
tbody.innerHTML = renderTableHtml();
bindModifyButtons();
}
};
})();
/* ========================================================================
* 7. ParamPage — 定值管理 (param, 多定值区展开)
* ======================================================================== */
var ParamPage = (function() {
var data = [];
var expandedRows = {}; // 记录展开状态
function renderTableHtml() {
if (data.length === 0) return '';
var rows = '';
for (var i = 0; i < data.length; i++) {
var item = data[i];
var ctrlLabel = CTRL_TYPE_MAP[item.ctrl_type] || item.ctrl_type;
var zones = item.setting_zone_list || [];
var zoneCount = zones.length;
var isOpen = expandedRows[i] === true;
var icon = isOpen ? '▼' : '▶';
// 主行(可点击展开)
rows += '<tr class="expand-row" data-pidx="' + i + '">'
+ '<td>' + (i + 1) + '</td>'
+ '<td>' + fmtVal(item.saddr) + '</td>'
+ '<td>' + fmtVal(item.desc) + '</td>'
+ '<td>' + fmtVal(item.type) + '</td>'
+ '<td>' + ctrlLabel + '</td>'
+ '<td>' + zoneCount + '</td>'
+ '<td><span class="expand-icon' + (isOpen ? ' open' : '') + '">' + icon + '</span> 展开</td>'
+ '</tr>';
// 展开的定值区子表
if (isOpen) {
rows += '<tr class="zone-sub-row" id="zone-sub-' + i + '"><td colspan="7">'
+ '<table class="zone-sub-table">'
+ '<thead><tr>'
+ '<th>定值区 ID</th><th>val</th><th>default_val</th>'
+ '<th>min</th><th>max</th><th>step</th><th>unit</th>'
+ '<th>修改值</th><th>修改</th>'
+ '</tr></thead><tbody>';
for (var j = 0; j < zones.length; j++) {
var zone = zones[j];
rows += '<tr>'
+ '<td>' + fmtVal(zone.id) + '</td>'
+ '<td>' + fmtVal(zone.val) + '</td>'
+ '<td>' + fmtVal(zone.default_val) + '</td>'
+ '<td>' + fmtVal(item.min) + '</td>'
+ '<td>' + fmtVal(item.max) + '</td>'
+ '<td>' + fmtVal(item.step) + '</td>'
+ '<td>' + fmtVal(item.unit) + '</td>'
+ '<td><input class="val-input" id="pm-' + i + '-' + j + '" placeholder="修改值"></td>'
+ '<td><button class="btn btn-sm btn-info" data-pidx="' + i + '" data-zidx="' + j + '">修改</button></td>'
+ '</tr>';
}
rows += '</tbody></table></td></tr>';
}
}
return rows;
}
function bindEvents() {
// 展开/收起
var expandRows = document.querySelectorAll('#param-tbody .expand-row');
for (var k = 0; k < expandRows.length; k++) {
expandRows[k].onclick = function() {
var idx = parseInt(this.getAttribute('data-pidx'));
expandedRows[idx] = !expandedRows[idx];
refreshTable();
};
}
// 修改按钮
var modButtons = document.querySelectorAll('#param-tbody .btn-info');
for (var m = 0; m < modButtons.length; m++) {
modButtons[m].onclick = function(e) {
e.stopPropagation();
var pidx = parseInt(this.getAttribute('data-pidx'));
var zidx = parseInt(this.getAttribute('data-zidx'));
var input = document.getElementById('pm-' + pidx + '-' + zidx);
var val = input.value.trim();
if (val === '') { alert('请输入修改值'); return; }
var item = data[pidx];
var zoneId = item.setting_zone_list[zidx].id;
WsClient.send({
curd: 'set', signal_type: 'param', saddr: item.saddr,
signal_data: val, setting_zone: zoneId
});
};
}
}
function refreshTable() {
var tbody = document.getElementById('param-tbody');
if (!tbody) return;
tbody.innerHTML = renderTableHtml();
bindEvents();
}
return {
render: function(container) {
expandedRows = {};
var headers = ['#', 'saddr', 'desc', '数据类型', 'ctrl_type', '定值区数', '展开'];
container.innerHTML = ''
+ '<div class="page-header"><h2>定值管理</h2><p>param — 多定值区参数管理,点击行展开查看各定值区详情并可修改</p></div>'
+ makeToolbar('param')
+ renderTable('param-tbody', headers, renderTableHtml(), '暂无定值数据,请先添加信号');
bindToolbar('param');
bindEvents();
},
onEnter: function() {},
onLeave: function() { data = []; expandedRows = {}; },
onData: function(arr) {
data = arr || [];
expandedRows = {}; // 新数据到达时重置展开状态
var tbody = document.getElementById('param-tbody');
if (!tbody) return;
if (data.length === 0) {
tbody.innerHTML = '<tr class="empty-row"><td colspan="7">暂无定值数据,请先添加信号</td></tr>';
return;
}
tbody.innerHTML = renderTableHtml();
bindEvents();
}
};
})();
/* ========================================================================
* 8. MonitorPage — 数据交互监控可展开列表15 分钟留存)
* ======================================================================== */
var MonitorPage = (function() {
var refreshTimer = null;
var expandedSet = {}; // 记录展开状态 {index: true}
function formatTime(ts) {
var d = new Date(ts);
var h = String(d.getHours()).padStart(2, '0');
var m = String(d.getMinutes()).padStart(2, '0');
var s = String(d.getSeconds()).padStart(2, '0');
var ms = String(d.getMilliseconds()).padStart(3, '0');
return h + ':' + m + ':' + s + '.' + ms;
}
function buildSummary(dir, raw) {
try {
var obj = JSON.parse(raw);
if (dir === '↑') {
// 上行:提取 curd, signal_type, saddr
var parts = [];
if (obj.curd) parts.push(obj.curd);
if (obj.signal_type) parts.push(obj.signal_type);
if (obj.saddr) parts.push(obj.saddr);
return parts.length > 0 ? parts.join(' ') : raw.substring(0, 60);
} else {
// 下行:统计各信号类型数量
var summaryParts = [];
var types = ['out', 'in', 'yk', 'ao', 'param'];
for (var t = 0; t < types.length; t++) {
var arr = obj[types[t]];
if (Array.isArray(arr) && arr.length > 0) {
summaryParts.push(types[t] + '×' + arr.length);
}
}
return summaryParts.length > 0 ? summaryParts.join(', ') : raw.substring(0, 60);
}
} catch (e) {
return raw.substring(0, 60);
}
}
function escapeHtml(str) {
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function renderLog() {
var el = document.getElementById('monitor-log');
if (!el) return;
var records = MonitorRing.getAll();
if (records.length === 0) {
el.innerHTML = '<div class="monitor-empty">暂无数据交互,请先操作信号页面...</div>';
document.getElementById('monitor-count').textContent = '0';
return;
}
// 保存滚动状态:用户是否在底部
var prevScrollTop = el.scrollTop;
var prevClientHeight = el.clientHeight;
var prevScrollHeight = el.scrollHeight;
var wasAtBottom = (prevScrollTop + prevClientHeight >= prevScrollHeight - 10);
var html = '';
for (var i = 0; i < records.length; i++) {
var r = records[i];
var isOpen = expandedSet[i] === true;
var summary = buildSummary(r.dir, r.data);
var dirClass = r.dir === '↑' ? 'mon-up' : 'mon-down';
var dirLabel = r.dir === '↑' ? '↑ 发送' : '↓ 接收';
var source = r.dir === '↑' ? '客户端' : 'RTU Server';
var target = r.dir === '↑' ? 'RTU Server' : '客户端';
var arrow = r.dir === '↑' ? '→' : '←';
html += '<div class="mon-item' + (isOpen ? ' open' : '') + '" data-midx="' + i + '">';
html += '<div class="mon-item-head">';
html += '<span class="mon-time">[' + formatTime(r.time) + ']</span>';
html += '<span class="mon-dir ' + dirClass + '">' + dirLabel + '</span>';
html += '<span class="mon-addr">' + source + ' ' + arrow + ' ' + target + '</span>';
html += '<span class="mon-summary">' + escapeHtml(summary) + '</span>';
html += '<span class="mon-expand-icon">' + (isOpen ? '▼' : '▶') + '</span>';
html += '</div>';
if (isOpen) {
html += '<div class="mon-item-body"><pre>' + escapeHtml(r.data) + '</pre></div>';
}
html += '</div>';
}
el.innerHTML = html;
// 绑定点击展开/收起
var heads = el.querySelectorAll('.mon-item-head');
for (var k = 0; k < heads.length; k++) {
heads[k].onclick = function() {
var item = this.parentElement;
var idx = parseInt(item.getAttribute('data-midx'));
expandedSet[idx] = !expandedSet[idx];
renderLog();
};
}
// 恢复滚动位置:在底部才跟随,否则保持原位
if(wasAtBottom)
{
el.scrollTop = el.scrollHeight;
}
else if(prevScrollHeight > 0)
{
var ratio = prevScrollTop / prevScrollHeight;
el.scrollTop = Math.round(ratio * el.scrollHeight);
}
document.getElementById('monitor-count').textContent = records.length;
}
return {
render: function(container) {
expandedSet = {};
container.innerHTML = ''
+ '<div class="page-header"><h2>数据监控</h2><p>实时查看 WebSocket 上下行 JSON 数据15 分钟自动清理历史,点击条目展开详情</p></div>'
+ '<div class="monitor-stats">'
+ '<span class="stat-label">状态:</span>'
+ '<span class="stat-value" id="monitor-ws-status">' + WsClient.getStatus() + '</span>'
+ '<span class="stat-label" style="margin-left:16px">已记录:</span>'
+ '<span class="stat-value" id="monitor-count">0</span>'
+ '<span class="stat-label">条</span>'
+ '<button class="btn btn-sm btn-outline" id="monitor-clear" style="margin-left:auto">清空记录</button>'
+ '</div>'
+ '<div class="monitor-log" id="monitor-log"></div>';
document.getElementById('monitor-clear').onclick = function() {
MonitorRing.clear();
expandedSet = {};
renderLog();
};
renderLog();
},
onEnter: function() {
refreshTimer = setInterval(renderLog, 500);
},
onLeave: function() {
if (refreshTimer) {
clearInterval(refreshTimer);
refreshTimer = null;
}
expandedSet = {};
}
};
})();