From e86c5fe1f2c56fbfb00f706c3bf7d6afd6b14401 Mon Sep 17 00:00:00 2001 From: william Date: Wed, 17 Jul 2024 15:02:59 +0800 Subject: [PATCH] beautify interface and log view --- .../resources/view/tailscale/interface.js | 31 ++++- .../resources/view/tailscale/log.js | 128 ++++++++++-------- 2 files changed, 97 insertions(+), 62 deletions(-) diff --git a/htdocs/luci-static/resources/view/tailscale/interface.js b/htdocs/luci-static/resources/view/tailscale/interface.js index 3d79c1a..8eb9b31 100644 --- a/htdocs/luci-static/resources/view/tailscale/interface.js +++ b/htdocs/luci-static/resources/view/tailscale/interface.js @@ -5,7 +5,9 @@ */ 'use strict'; +'require dom'; 'require fs'; +'require poll'; 'require ui'; 'require view'; @@ -68,12 +70,17 @@ return view.extend({ }); }, - render: function(data) { - var title = E('h2', {class: 'content'}, _('Tailscale')); - var desc = E('div', {class: 'cbi-map-descr'}, _('Tailscale is a cross-platform and easy to use virtual LAN.')); + pollData: function (container) { + poll.add(L.bind(function () { + return this.load().then(L.bind(function (data) { + dom.content(container, this.renderContent(data)); + }, this)); + }, this)); + }, + renderContent: function (data) { if (!Array.isArray(data)) { - return E('div', {}, [title, desc, E('div', {}, _('No interface online.'))]); + return E('div', {}, _('No interface online.')); } var rows = data.flatMap(function(interfaceData) { return [ @@ -105,7 +112,21 @@ return view.extend({ ]; }); - return E('div', {}, [title, desc, E('table', { 'class': 'table' }, rows)]); + return E('table', { 'class': 'table' }, rows); + }, + + render: function(data) { + var content = E([], [ + E('h2', {class: 'content'}, _('Tailscale')), + E('div', {class: 'cbi-map-descr'}, _('Tailscale is a cross-platform and easy to use virtual LAN.')), + E('div') + ]); + var container = content.lastElementChild; + + dom.content(container, this.renderContent(data)); + this.pollData(container); + + return content; }, handleSaveApply: null, diff --git a/htdocs/luci-static/resources/view/tailscale/log.js b/htdocs/luci-static/resources/view/tailscale/log.js index 71c99bd..b953b2f 100644 --- a/htdocs/luci-static/resources/view/tailscale/log.js +++ b/htdocs/luci-static/resources/view/tailscale/log.js @@ -1,7 +1,3 @@ -/* SPDX-License-Identifier: GPL-3.0-only - * - * Copyright (C) 2024 asvow - */ 'use strict'; 'require fs'; 'require poll'; @@ -9,68 +5,86 @@ 'require view'; return view.extend({ - logs: [], - reverseLogs: false, + retrieveLog: async function() { + return Promise.all([ + L.resolveDefault(fs.stat('/sbin/logread'), null), + L.resolveDefault(fs.stat('/usr/sbin/logread'), null) + ]).then(function(stat) { + var logger = stat[0] ? stat[0].path : stat[1] ? stat[1].path : null; - load: function () { - var self = this; - /* Thanks to @animegasan */ - poll.add(function() { - return fs.exec('/sbin/logread', ['-e', 'tailscale']) - .then(function (res) { - if (res.code === 0) { - var statusMappings = { - 'daemon.err': { status: 'StdErr', startIndex: 9 }, - 'daemon.notice': { status: 'Info', startIndex: 10 } - }; - if (res.stdout) { - self.logs = res.stdout.split('\n').map(function(log) { - var logParts = log.split(' ').filter(Boolean); - if (logParts.length >= 6) { - var formattedTime = logParts[1] + ' ' + logParts[2] + ' - ' + logParts[3]; - var status = logParts[5]; - var mapping = statusMappings[status] || { status: status, startIndex: 9 }; - status = mapping.status; - var startIndex = mapping.startIndex; - var message = logParts.slice(startIndex).join(' '); - return formattedTime + ' [ ' + status + ' ] - ' + message; - } else { - return ''; - } - }).filter(Boolean); - } - self.updateLogView(); + return fs.exec_direct(logger, [ '-e', 'tailscale' ]).then(logdata => { + var statusMappings = { + 'daemon.err': { status: 'StdErr', startIndex: 9 }, + 'daemon.notice': { status: 'Info', startIndex: 10 } + }; + const loglines = logdata.trim().split(/\n/).map(function(log) { + var logParts = log.split(' ').filter(Boolean); + if (logParts.length >= 6) { + var formattedTime = logParts[1] + ' ' + logParts[2] + ' - ' + logParts[3]; + var status = logParts[5]; + var mapping = statusMappings[status] || { status: status, startIndex: 9 }; + status = mapping.status; + var startIndex = mapping.startIndex; + var message = logParts.slice(startIndex).join(' '); + return formattedTime + ' [ ' + status + ' ] - ' + message; } else { - throw new Error(res.stdout + ' ' + res.stderr); + return 'Log is empty.'; } - }) + }).filter(Boolean); + return { value: loglines.join('\n'), rows: loglines.length + 1 }; + }).catch(function(err) { + ui.addNotification(null, E('p', {}, _('Unable to load log data: ' + err.message))); + return ''; + }); }); }, - updateLogView: function() { - var view = document.getElementById('syslog'); - var logs = this.logs; - if (logs.length <= 100) { - view.textContent = _('No logs available'); - return; + pollLog: async function() { + const element = document.getElementById('syslog'); + if (element) { + const log = await this.retrieveLog(); + element.value = log.value; + element.rows = log.rows; } - if (this.reverseLogs) { - logs = logs.slice().reverse(); - } - view.textContent = logs.join('\n'); }, - render: function () { - var self = this; - var button = E('button', { - 'class': 'cbi-button cbi-button-neutral', - click: function() { - self.reverseLogs = !self.reverseLogs; - self.updateLogView(); - } - }, _('Toggle Log Order')); - var logArea = E('div', { 'id': 'syslog', 'style': 'white-space: pre;' }); - return E('div', {}, [ button, logArea ]); + load: async function() { + poll.add(this.pollLog.bind(this)); + return await this.retrieveLog(); + }, + + render: function(loglines) { + var scrollDownButton = E('button', { + 'id': 'scrollDownButton', + 'class': 'cbi-button cbi-button-neutral' + }, _('Scroll to tail', 'scroll to bottom (the tail) of the log file') + ); + scrollDownButton.addEventListener('click', function() { + scrollUpButton.scrollIntoView(); + }); + + var scrollUpButton = E('button', { + 'id' : 'scrollUpButton', + 'class': 'cbi-button cbi-button-neutral' + }, _('Scroll to head', 'scroll to top (the head) of the log file') + ); + scrollUpButton.addEventListener('click', function() { + scrollDownButton.scrollIntoView(); + }); + + return E([], [ + E('div', { 'id': 'content_syslog' }, [ + E('div', {'style': 'padding-bottom: 20px'}, [scrollDownButton]), + E('textarea', { + 'id': 'syslog', + 'style': 'font-size:12px', + 'readonly': 'readonly', + 'wrap': 'off', + 'rows': loglines.rows, + }, [ loglines.value ]), + E('div', {'style': 'padding-bottom: 20px'}, [scrollUpButton]) + ]) + ]); }, handleSaveApply: null,