diff options
29 files changed, 2550 insertions, 2206 deletions
diff --git a/.gitignore b/.gitignore index 07494e98ef..2e4ba9b81a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ dist/ *.po~ /docs modules/luci-base/src/po2lmo +modules/luci-base/src/jsmin diff --git a/applications/luci-app-opkg/htdocs/luci-static/resources/view/opkg.js b/applications/luci-app-opkg/htdocs/luci-static/resources/view/opkg.js new file mode 100644 index 0000000000..274a982929 --- /dev/null +++ b/applications/luci-app-opkg/htdocs/luci-static/resources/view/opkg.js @@ -0,0 +1,812 @@ +var packages = { + available: { providers: {}, pkgs: {} }, + installed: { providers: {}, pkgs: {} } +}; + +var currentDisplayMode = 'available', currentDisplayRows = []; + +function parseList(s, dest) +{ + var re = /([^\n]*)\n/g, + pkg = null, key = null, val = null, m; + + while ((m = re.exec(s)) !== null) { + if (m[1].match(/^\s(.*)$/)) { + if (pkg !== null && key !== null && val !== null) + val += '\n' + RegExp.$1.trim(); + + continue; + } + + if (key !== null && val !== null) { + switch (key) { + case 'package': + pkg = { name: val }; + break; + + case 'depends': + case 'provides': + var list = val.split(/\s*,\s*/); + if (list.length !== 1 || list[0].length > 0) + pkg[key] = list; + break; + + case 'installed-time': + pkg.installtime = new Date(+val * 1000); + break; + + case 'installed-size': + pkg.installsize = +val; + break; + + case 'status': + var stat = val.split(/\s+/), + mode = stat[1], + installed = stat[2]; + + switch (mode) { + case 'user': + case 'hold': + pkg[mode] = true; + break; + } + + switch (installed) { + case 'installed': + pkg.installed = true; + break; + } + break; + + case 'essential': + if (val === 'yes') + pkg.essential = true; + break; + + case 'size': + pkg.size = +val; + break; + + case 'architecture': + case 'auto-installed': + case 'filename': + case 'sha256sum': + case 'section': + break; + + default: + pkg[key] = val; + break; + } + + key = val = null; + } + + if (m[1].trim().match(/^([\w-]+)\s*:(.+)$/)) { + key = RegExp.$1.toLowerCase(); + val = RegExp.$2.trim(); + } + else { + dest.pkgs[pkg.name] = pkg; + + var provides = dest.providers[pkg.name] ? [] : [ pkg.name ]; + + if (pkg.provides) + provides.push.apply(provides, pkg.provides); + + provides.forEach(function(p) { + dest.providers[p] = dest.providers[p] || []; + dest.providers[p].push(pkg); + }); + } + } +} + +function display(pattern) +{ + var src = packages[currentDisplayMode === 'updates' ? 'installed' : currentDisplayMode], + table = document.querySelector('#packages'), + pager = document.querySelector('#pager'); + + currentDisplayRows.length = 0; + + if (typeof(pattern) === 'string' && pattern.length > 0) + pattern = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'ig'); + + for (var name in src.pkgs) { + var pkg = src.pkgs[name], + desc = pkg.description || '', + altsize = null; + + if (!pkg.size && packages.available.pkgs[name]) + altsize = packages.available.pkgs[name].size; + + if (!desc && packages.available.pkgs[name]) + desc = packages.available.pkgs[name].description || ''; + + desc = desc.split(/\n/); + desc = desc[0].trim() + (desc.length > 1 ? '…' : ''); + + if ((pattern instanceof RegExp) && + !name.match(pattern) && !desc.match(pattern)) + continue; + + var btn, ver; + + if (currentDisplayMode === 'updates') { + var avail = packages.available.pkgs[name]; + if (!avail || avail.version === pkg.version) + continue; + + ver = '%s » %s'.format( + truncateVersion(pkg.version || '-'), + truncateVersion(avail.version || '-')); + + btn = E('div', { + 'class': 'btn cbi-button-positive', + 'data-package': name, + 'click': handleInstall + }, _('Upgrade…')); + } + else if (currentDisplayMode === 'installed') { + ver = truncateVersion(pkg.version || '-'); + btn = E('div', { + 'class': 'btn cbi-button-negative', + 'data-package': name, + 'click': handleRemove + }, _('Remove')); + } + else { + ver = truncateVersion(pkg.version || '-'); + + if (!packages.installed.pkgs[name]) + btn = E('div', { + 'class': 'btn cbi-button-action', + 'data-package': name, + 'click': handleInstall + }, _('Install…')); + else if (packages.installed.pkgs[name].version != pkg.version) + btn = E('div', { + 'class': 'btn cbi-button-positive', + 'data-package': name, + 'click': handleInstall + }, _('Upgrade…')); + else + btn = E('div', { + 'class': 'btn cbi-button-neutral', + 'disabled': 'disabled' + }, _('Installed')); + } + + name = '%h'.format(name); + desc = '%h'.format(desc || '-'); + + if (pattern) { + name = name.replace(pattern, '<ins>$&</ins>'); + desc = desc.replace(pattern, '<ins>$&</ins>'); + } + + currentDisplayRows.push([ + name, + ver, + pkg.size ? '%.1024mB'.format(pkg.size) + : (altsize ? '~%.1024mB'.format(altsize) : '-'), + desc, + btn + ]); + } + + currentDisplayRows.sort(function(a, b) { + if (a[0] < b[0]) + return -1; + else if (a[0] > b[0]) + return 1; + else + return 0; + }); + + pager.parentNode.style.display = ''; + pager.setAttribute('data-offset', 100); + handlePage({ target: pager.querySelector('.prev') }); +} + +function handlePage(ev) +{ + var filter = document.querySelector('input[name="filter"]'), + pager = ev.target.parentNode, + offset = +pager.getAttribute('data-offset'), + next = ev.target.classList.contains('next'); + + if ((next && (offset + 100) >= currentDisplayRows.length) || + (!next && (offset < 100))) + return; + + offset += next ? 100 : -100; + pager.setAttribute('data-offset', offset); + pager.querySelector('.text').firstChild.data = currentDisplayRows.length + ? _('Displaying %d-%d of %d').format(1 + offset, Math.min(offset + 100, currentDisplayRows.length), currentDisplayRows.length) + : _('No packages'); + + if (offset < 100) + pager.querySelector('.prev').setAttribute('disabled', 'disabled'); + else + pager.querySelector('.prev').removeAttribute('disabled'); + + if ((offset + 100) >= currentDisplayRows.length) + pager.querySelector('.next').setAttribute('disabled', 'disabled'); + else + pager.querySelector('.next').removeAttribute('disabled'); + + var placeholder = _('No information available'); + + if (filter.value) + placeholder = [ + E('span', {}, _('No packages matching "<strong>%h</strong>".').format(filter.value)), ' (', + E('a', { href: '#', onclick: 'handleReset(event)' }, _('Reset')), ')' + ]; + + cbi_update_table('#packages', currentDisplayRows.slice(offset, offset + 100), + placeholder); +} + +function handleMode(ev) +{ + var tab = findParent(ev.target, 'li'); + if (tab.getAttribute('data-mode') === currentDisplayMode) + return; + + tab.parentNode.querySelectorAll('li').forEach(function(li) { + li.classList.remove('cbi-tab'); + li.classList.add('cbi-tab-disabled'); + }); + + tab.classList.remove('cbi-tab-disabled'); + tab.classList.add('cbi-tab'); + + currentDisplayMode = tab.getAttribute('data-mode'); + + display(document.querySelector('input[name="filter"]').value); + + ev.target.blur(); + ev.preventDefault(); +} + +function orderOf(c) +{ + if (c === '~') + return -1; + else if (c === '' || c >= '0' && c <= '9') + return 0; + else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) + return c.charCodeAt(0); + else + return c.charCodeAt(0) + 256; +} + +function compareVersion(val, ref) +{ + var vi = 0, ri = 0, + isdigit = { 0:1, 1:1, 2:1, 3:1, 4:1, 5:1, 6:1, 7:1, 8:1, 9:1 }; + + val = val || ''; + ref = ref || ''; + + while (vi < val.length || ri < ref.length) { + var first_diff = 0; + + while ((vi < val.length && !isdigit[val.charAt(vi)]) || + (ri < ref.length && !isdigit[ref.charAt(ri)])) { + var vc = orderOf(val.charAt(vi)), rc = orderOf(ref.charAt(ri)); + if (vc !== rc) + return vc - rc; + + vi++; ri++; + } + + while (val.charAt(vi) === '0') + vi++; + + while (ref.charAt(ri) === '0') + ri++; + + while (isdigit[val.charAt(vi)] && isdigit[ref.charAt(ri)]) { + first_diff = first_diff || (val.charCodeAt(vi) - ref.charCodeAt(ri)); + vi++; ri++; + } + + if (isdigit[val.charAt(vi)]) + return 1; + else if (isdigit[ref.charAt(ri)]) + return -1; + else if (first_diff) + return first_diff; + } + + return 0; +} + +function versionSatisfied(ver, ref, vop) +{ + var r = compareVersion(ver, ref); + + switch (vop) { + case '<': + case '<=': + return r <= 0; + + case '>': + case '>=': + return r >= 0; + + case '<<': + return r < 0; + + case '>>': + return r > 0; + + case '=': + return r == 0; + } + + return false; +} + +function pkgStatus(pkg, vop, ver, info) +{ + info.errors = info.errors || []; + info.install = info.install || []; + + if (pkg.installed) { + if (vop && !versionSatisfied(pkg.version, ver, vop)) { + var repl = null; + + (packages.available.providers[pkg.name] || []).forEach(function(p) { + if (!repl && versionSatisfied(p.version, ver, vop)) + repl = p; + }); + + if (repl) { + info.install.push(repl); + return E('span', { + 'class': 'label', + 'data-tooltip': _('Requires update to %h %h') + .format(repl.name, repl.version) + }, _('Needs upgrade')); + } + + info.errors.push(_('The installed version of package <em>%h</em> is not compatible, require %s while %s is installed.').format(pkg.name, truncateVersion(ver, vop), truncateVersion(pkg.version))); + + return E('span', { + 'class': 'label warning', + 'data-tooltip': _('Require version %h %h,\ninstalled %h') + .format(vop, ver, pkg.version) + }, _('Version incompatible')); + } + + return E('span', { 'class': 'label notice' }, _('Installed')); + } + else if (!pkg.missing) { + if (!vop || versionSatisfied(pkg.version, ver, vop)) { + info.install.push(pkg); + return E('span', { 'class': 'label' }, _('Not installed')); + } + + info.errors.push(_('The repository version of package <em>%h</em> is not compatible, require %s but only %s is available.') + .format(pkg.name, truncateVersion(ver, vop), truncateVersion(pkg.version))); + + return E('span', { + 'class': 'label warning', + 'data-tooltip': _('Require version %h %h,\ninstalled %h') + .format(vop, ver, pkg.version) + }, _('Version incompatible')); + } + else { + info.errors.push(_('Required dependency package <em>%h</em> is not available in any repository.').format(pkg.name)); + + return E('span', { 'class': 'label warning' }, _('Not available')); + } +} + +function renderDependencyItem(dep, info) +{ + var li = E('li'), + vop = dep.version ? dep.version[0] : null, + ver = dep.version ? dep.version[1] : null, + depends = []; + + for (var i = 0; dep.pkgs && i < dep.pkgs.length; i++) { + var pkg = packages.installed.pkgs[dep.pkgs[i]] || + packages.available.pkgs[dep.pkgs[i]] || + { name: dep.name }; + + if (i > 0) + li.appendChild(document.createTextNode(' | ')); + + var text = pkg.name; + + if (pkg.installsize) + text += ' (%.1024mB)'.format(pkg.installsize); + else if (pkg.size) + text += ' (~%.1024mB)'.format(pkg.size); + + li.appendChild(E('span', { 'data-tooltip': pkg.description }, + [ text, ' ', pkgStatus(pkg, vop, ver, info) ])); + + (pkg.depends || []).forEach(function(d) { + if (depends.indexOf(d) === -1) + depends.push(d); + }); + } + + if (!li.firstChild) + li.appendChild(E('span', {}, + [ dep.name, ' ', + pkgStatus({ name: dep.name, missing: true }, vop, ver, info) ])); + + var subdeps = renderDependencies(depends, info); + if (subdeps) + li.appendChild(subdeps); + + return li; +} + +function renderDependencies(depends, info) +{ + var deps = depends || [], + items = []; + + info.seen = info.seen || []; + + for (var i = 0; i < deps.length; i++) { + if (deps[i] === 'libc') + continue; + + if (deps[i].match(/^(.+)\s+\((<=|<|>|>=|=|<<|>>)(.+)\)$/)) { + dep = RegExp.$1.trim(); + vop = RegExp.$2.trim(); + ver = RegExp.$3.trim(); + } + else { + dep = deps[i].trim(); + vop = ver = null; + } + + if (info.seen[dep]) + continue; + + var pkgs = []; + + (packages.installed.providers[dep] || []).forEach(function(p) { + if (pkgs.indexOf(p.name) === -1) pkgs.push(p.name); + }); + + (packages.available.providers[dep] || []).forEach(function(p) { + if (pkgs.indexOf(p.name) === -1) pkgs.push(p.name); + }); + + info.seen[dep] = { + name: dep, + pkgs: pkgs, + version: [vop, ver] + }; + + items.push(renderDependencyItem(info.seen[dep], info)); + } + + if (items.length) + return E('ul', { 'class': 'deps' }, items); + + return null; +} + +function truncateVersion(v, op) +{ + v = v.replace(/\b(([a-f0-9]{8})[a-f0-9]{24,32})\b/, + '<span data-tooltip="$1">$2…</span>'); + + if (!op || op === '=') + return v; + + return '%h %h'.format(op, v); +} + +function handleReset(ev) +{ + var filter = document.querySelector('input[name="filter"]'); + + filter.value = ''; + display(); +} + +function handleInstall(ev) +{ + var name = ev.target.getAttribute('data-package'), + pkg = packages.available.pkgs[name], + depcache = {}, + size; + + if (pkg.installsize) + size = _('~%.1024mB installed').format(pkg.installsize); + else if (pkg.size) + size = _('~%.1024mB compressed').format(pkg.size); + else + size = _('unknown'); + + var deps = renderDependencies(pkg.depends, depcache), + tree = null, errs = null, inst = null, desc = null; + + if (depcache.errors && depcache.errors.length) { + errs = E('ul', { 'class': 'errors' }); + depcache.errors.forEach(function(err) { + errs.appendChild(E('li', {}, err)); + }); + } + + var totalsize = pkg.installsize || pkg.size || 0, + totalpkgs = 1; + + if (depcache.install && depcache.install.length) + depcache.install.forEach(function(ipkg) { + totalsize += ipkg.installsize || ipkg.size || 0; + totalpkgs++; + }); + + inst = E('p', {}, + _('Require approx. %.1024mB size for %d package(s) to install.') + .format(totalsize, totalpkgs)); + + if (deps) { + tree = E('li', '<strong>%s:</strong>'.format(_('Dependencies'))); + tree.appendChild(deps); + } + + if (pkg.description) { + desc = E('div', {}, [ + E('h5', {}, _('Description')), + E('p', {}, pkg.description) + ]); + } + + L.showModal(_('Details for package <em>%h</em>').format(pkg.name), [ + E('ul', {}, [ + E('li', '<strong>%s:</strong> %h'.format(_('Version'), pkg.version)), + E('li', '<strong>%s:</strong> %h'.format(_('Size'), size)), + tree || '', + ]), + desc || '', + errs || inst || '', + E('div', { 'class': 'right' }, [ + E('div', { + 'class': 'btn', + 'click': L.hideModal + }, _('Cancel')), + ' ', + E('div', { + 'data-command': 'install', + 'data-package': name, + 'class': 'btn cbi-button-action', + 'click': handleOpkg + }, _('Install')) + ]) + ]); +} + +function handleManualInstall(ev) +{ + var name_or_url = document.querySelector('input[name="install"]').value, + install = E('div', { + 'class': 'btn cbi-button-action', + 'data-command': 'install', + 'data-package': name_or_url, + 'click': function(ev) { + document.querySelector('input[name="install"]').value = ''; + handleOpkg(ev); + } + }, _('Install')), warning; + + if (!name_or_url.length) { + return; + } + else if (name_or_url.indexOf('/') !== -1) { + warning = E('p', {}, _('Installing packages from untrusted sources is a potential security risk! Really attempt to install <em>%h</em>?').format(name_or_url)); + } + else if (!packages.available.providers[name_or_url]) { + warning = E('p', {}, _('The package <em>%h</em> is not available in any configured repository.').format(name_or_url)); + install = ''; + } + else { + warning = E('p', {}, _('Really attempt to install <em>%h</em>?').format(name_or_url)); + } + + L.showModal(_('Manually install package'), [ + warning, + E('div', { 'class': 'right' }, [ + E('div', { + 'click': L.hideModal, + 'class': 'btn cbi-button-neutral' + }, _('Cancel')), + ' ', install + ]) + ]); +} + +function handleConfig(ev) +{ + L.showModal(_('OPKG Configuration'), [ + E('p', { 'class': 'spinning' }, _('Loading configuration data…')) + ]); + + L.get('admin/system/opkg/config', null, function(xhr, conf) { + var body = [ + E('p', {}, _('Below is a listing of the various configuration files used by <em>opkg</em>. Use <em>opkg.conf</em> for global settings and <em>customfeeds.conf</em> for custom repository entries. The configuration in the other files may be changed but is usually not preserved by <em>sysupgrade</em>.')) + ]; + + Object.keys(conf).sort().forEach(function(file) { + body.push(E('h5', {}, '%h'.format(file))); + body.push(E('textarea', { + 'name': file, + 'rows': Math.max(Math.min(conf[file].match(/\n/g).length, 10), 3) + }, '%h'.format(conf[file]))); + }); + + body.push(E('div', { 'class': 'right' }, [ + E('div', { + 'class': 'btn cbi-button-neutral', + 'click': L.hideModal + }, _('Cancel')), + ' ', + E('div', { + 'class': 'btn cbi-button-positive', + 'click': function(ev) { + var data = {}; + findParent(ev.target, '.modal').querySelectorAll('textarea[name]') + .forEach(function(textarea) { + data[textarea.getAttribute('name')] = textarea.value + }); + + L.showModal(_('OPKG Configuration'), [ + E('p', { 'class': 'spinning' }, _('Saving configuration data…')) + ]); + + L.post('admin/system/opkg/config', { data: JSON.stringify(data) }, L.hideModal); + } + }, _('Save')), + ])); + + L.showModal(_('OPKG Configuration'), body); + }); +} + +function handleRemove(ev) +{ + var name = ev.target.getAttribute('data-package'), + pkg = packages.installed.pkgs[name], + avail = packages.available.pkgs[name] || {}, + size, desc; + + if (avail.installsize) + size = _('~%.1024mB installed').format(avail.installsize); + else if (avail.size) + size = _('~%.1024mB compressed').format(avail.size); + else + size = _('unknown'); + + if (avail.description) { + desc = E('div', {}, [ + E('h5', {}, _('Description')), + E('p', {}, avail.description) + ]); + } + + L.showModal(_('Remove package <em>%h</em>').format(pkg.name), [ + E('ul', {}, [ + E('li', '<strong>%s:</strong> %h'.format(_('Version'), pkg.version)), + E('li', '<strong>%s:</strong> %h'.format(_('Size'), size)) + ]), + desc || '', + E('div', { 'style': 'display:flex; justify-content:space-between; flex-wrap:wrap' }, [ + E('label', {}, [ + E('input', { type: 'checkbox', checked: 'checked', name: 'autoremove' }), + _('Automatically remove unused dependencies') + ]), + E('div', { 'style': 'flex-grow:1', 'class': 'right' }, [ + E('div', { + 'class': 'btn', + 'click': L.hideModal + }, _('Cancel')), + ' ', + E('div', { + 'data-command': 'remove', + 'data-package': name, + 'class': 'btn cbi-button-negative', + 'click': handleOpkg + }, _('Remove')) + ]) + ]) + ]); +} + +function handleOpkg(ev) +{ + var cmd = ev.target.getAttribute('data-command'), + pkg = ev.target.getAttribute('data-package'), + rem = document.querySelector('input[name="autoremove"]'), + url = 'admin/system/opkg/exec/' + encodeURIComponent(cmd); + + var dlg = L.showModal(_('Executing package manager'), [ + E('p', { 'class': 'spinning' }, + _('Waiting for the <em>opkg %h</em> command to complete…').format(cmd)) + ]); + + L.post(url, { package: pkg, autoremove: rem ? rem.checked : false }, function(xhr, res) { + dlg.removeChild(dlg.lastChild); + + if (res.stdout) + dlg.appendChild(E('pre', [ res.stdout ])); + + if (res.stderr) { + dlg.appendChild(E('h5', _('Errors'))); + dlg.appendChild(E('pre', { 'class': 'errors' }, [ res.stderr ])); + } + + if (res.code !== 0) + dlg.appendChild(E('p', _('The <em>opkg %h</em> command failed with code <code>%d</code>.').format(cmd, (res.code & 0xff) || -1))); + + dlg.appendChild(E('div', { 'class': 'right' }, + E('div', { + 'class': 'btn', + 'click': function() { + L.hideModal(); + updateLists(); + } + }, _('Dismiss')))); + }); +} + +function updateLists() +{ + cbi_update_table('#packages', [], + E('div', { 'class': 'spinning' }, _('Loading package information…'))); + + packages.available = { providers: {}, pkgs: {} }; + packages.installed = { providers: {}, pkgs: {} }; + + L.get('admin/system/opkg/statvfs', null, function(xhr, stat) { + var pg = document.querySelector('.cbi-progressbar'), + total = stat.blocks || 0, + free = stat.bfree || 0; + + pg.firstElementChild.style.width = Math.floor(total ? ((100 / total) * free) : 100) + '%'; + pg.setAttribute('title', '%s (%.1024mB)'.format(pg.firstElementChild.style.width, free * (stat.frsize || 0))); + + L.get('admin/system/opkg/list/available', null, function(xhr) { + parseList(xhr.responseText, packages.available); + L.get('admin/system/opkg/list/installed', null, function(xhr) { + parseList(xhr.responseText, packages.installed); + display(document.querySelector('input[name="filter"]').value); + }); + }); + }); +} + +window.requestAnimationFrame(function() { + var filter = document.querySelector('input[name="filter"]'), + keyTimeout = null; + + filter.value = filter.getAttribute('value'); + filter.addEventListener('keyup', + function(ev) { + if (keyTimeout !== null) + window.clearTimeout(keyTimeout); + + keyTimeout = window.setTimeout(function() { + display(ev.target.value); + }, 250); + }); + + document.querySelector('#pager > .prev').addEventListener('click', handlePage); + document.querySelector('#pager > .next').addEventListener('click', handlePage); + document.querySelector('.cbi-tabmenu.mode').addEventListener('click', handleMode); + + updateLists(); +}); diff --git a/applications/luci-app-opkg/luasrc/view/opkg.htm b/applications/luci-app-opkg/luasrc/view/opkg.htm index e610ebad34..0d2a4e2920 100644 --- a/applications/luci-app-opkg/luasrc/view/opkg.htm +++ b/applications/luci-app-opkg/luasrc/view/opkg.htm @@ -81,826 +81,6 @@ } </style> -<script type="text/javascript">//<![CDATA[ - var packages = { - available: { providers: {}, pkgs: {} }, - installed: { providers: {}, pkgs: {} } - }; - - var currentDisplayMode = 'available', currentDisplayRows = []; - - function parseList(s, dest) - { - var re = /([^\n]*)\n/g, - pkg = null, key = null, val = null, m; - - while ((m = re.exec(s)) !== null) { - if (m[1].match(/^\s(.*)$/)) { - if (pkg !== null && key !== null && val !== null) - val += '\n' + RegExp.$1.trim(); - - continue; - } - - if (key !== null && val !== null) { - switch (key) { - case 'package': - pkg = { name: val }; - break; - - case 'depends': - case 'provides': - var list = val.split(/\s*,\s*/); - if (list.length !== 1 || list[0].length > 0) - pkg[key] = list; - break; - - case 'installed-time': - pkg.installtime = new Date(+val * 1000); - break; - - case 'installed-size': - pkg.installsize = +val; - break; - - case 'status': - var stat = val.split(/\s+/), - mode = stat[1], - installed = stat[2]; - - switch (mode) { - case 'user': - case 'hold': - pkg[mode] = true; - break; - } - - switch (installed) { - case 'installed': - pkg.installed = true; - break; - } - break; - - case 'essential': - if (val === 'yes') - pkg.essential = true; - break; - - case 'size': - pkg.size = +val; - break; - - case 'architecture': - case 'auto-installed': - case 'filename': - case 'sha256sum': - case 'section': - break; - - default: - pkg[key] = val; - break; - } - - key = val = null; - } - - if (m[1].trim().match(/^([\w-]+)\s*:(.+)$/)) { - key = RegExp.$1.toLowerCase(); - val = RegExp.$2.trim(); - } - else { - dest.pkgs[pkg.name] = pkg; - - var provides = dest.providers[pkg.name] ? [] : [ pkg.name ]; - - if (pkg.provides) - provides.push.apply(provides, pkg.provides); - - provides.forEach(function(p) { - dest.providers[p] = dest.providers[p] || []; - dest.providers[p].push(pkg); - }); - } - } - } - - function display(pattern) - { - var src = packages[currentDisplayMode === 'updates' ? 'installed' : currentDisplayMode], - table = document.querySelector('#packages'), - pager = document.querySelector('#pager'); - - currentDisplayRows.length = 0; - - if (typeof(pattern) === 'string' && pattern.length > 0) - pattern = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'ig'); - - for (var name in src.pkgs) { - var pkg = src.pkgs[name], - desc = pkg.description || '', - altsize = null; - - if (!pkg.size && packages.available.pkgs[name]) - altsize = packages.available.pkgs[name].size; - - if (!desc && packages.available.pkgs[name]) - desc = packages.available.pkgs[name].description || ''; - - desc = desc.split(/\n/); - desc = desc[0].trim() + (desc.length > 1 ? '…' : ''); - - if ((pattern instanceof RegExp) && - !name.match(pattern) && !desc.match(pattern)) - continue; - - var btn, ver; - - if (currentDisplayMode === 'updates') { - var avail = packages.available.pkgs[name]; - if (!avail || avail.version === pkg.version) - continue; - - ver = '%s » %s'.format( - truncateVersion(pkg.version || '-'), - truncateVersion(avail.version || '-')); - - btn = E('button', { - 'class': 'btn cbi-button-positive', - 'data-package': name, - 'click': handleInstall - }, _('Upgrade…')); - } - else if (currentDisplayMode === 'installed') { - ver = truncateVersion(pkg.version || '-'); - btn = E('button', { - 'class': 'btn cbi-button-negative', - 'data-package': name, - 'click': handleRemove - }, _('Remove')); - } - else { - ver = truncateVersion(pkg.version || '-'); - - if (!packages.installed.pkgs[name]) - btn = E('button', { - 'class': 'btn cbi-button-action', - 'data-package': name, - 'click': handleInstall - }, _('Install…')); - else if (packages.installed.pkgs[name].version != pkg.version) - btn = E('button', { - 'class': 'btn cbi-button-positive', - 'data-package': name, - 'click': handleInstall - }, _('Upgrade…')); - else - btn = E('button', { - 'class': 'btn cbi-button-neutral', - 'disabled': 'disabled' - }, _('Installed')); - } - - name = '%h'.format(name); - desc = '%h'.format(desc || '-'); - - if (pattern) { - name = name.replace(pattern, '<ins>$&</ins>'); - desc = desc.replace(pattern, '<ins>$&</ins>'); - } - - currentDisplayRows.push([ - name, - ver, - pkg.size ? '%.1024mB'.format(pkg.size) - : (altsize ? '~%.1024mB'.format(altsize) : '-'), - desc, - btn - ]); - } - - currentDisplayRows.sort(function(a, b) { - if (a[0] < b[0]) - return -1; - else if (a[0] > b[0]) - return 1; - else - return 0; - }); - - pager.parentNode.style.display = ''; - pager.setAttribute('data-offset', 100); - handlePage({ target: pager.querySelector('.prev') }); - } - - function handlePage(ev) - { - var filter = document.querySelector('input[name="filter"]'), - pager = ev.target.parentNode, - offset = +pager.getAttribute('data-offset'), - next = ev.target.classList.contains('next'); - - if ((next && (offset + 100) >= currentDisplayRows.length) || - (!next && (offset < 100))) - return; - - offset += next ? 100 : -100; - pager.setAttribute('data-offset', offset); - pager.querySelector('.text').firstChild.data = currentDisplayRows.length - ? _('Displaying %d-%d of %d').format(1 + offset, Math.min(offset + 100, currentDisplayRows.length), currentDisplayRows.length) - : _('No packages'); - - if (offset < 100) - pager.querySelector('.prev').setAttribute('disabled', 'disabled'); - else - pager.querySelector('.prev').removeAttribute('disabled'); - - if ((offset + 100) >= currentDisplayRows.length) - pager.querySelector('.next').setAttribute('disabled', 'disabled'); - else - pager.querySelector('.next').removeAttribute('disabled'); - - var placeholder = _('No information available'); - - if (filter.value) - placeholder = [ - E('span', {}, _('No packages matching "<strong>%h</strong>".').format(filter.value)), ' (', - E('a', { href: '#', onclick: 'handleReset(event)' }, _('Reset')), ')' - ]; - - cbi_update_table('#packages', currentDisplayRows.slice(offset, offset + 100), - placeholder); - } - - function handleMode(ev) - { - var tab = findParent(ev.target, 'li'); - if (tab.getAttribute('data-mode') === currentDisplayMode) - return; - - tab.parentNode.querySelectorAll('li').forEach(function(li) { - li.classList.remove('cbi-tab'); - li.classList.add('cbi-tab-disabled'); - }); - - tab.classList.remove('cbi-tab-disabled'); - tab.classList.add('cbi-tab'); - - currentDisplayMode = tab.getAttribute('data-mode'); - - display(document.querySelector('input[name="filter"]').value); - - ev.target.blur(); - ev.preventDefault(); - } - - function orderOf(c) - { - if (c === '~') - return -1; - else if (c === '' || c >= '0' && c <= '9') - return 0; - else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) - return c.charCodeAt(0); - else - return c.charCodeAt(0) + 256; - } - - function compareVersion(val, ref) - { - var vi = 0, ri = 0, - isdigit = { 0:1, 1:1, 2:1, 3:1, 4:1, 5:1, 6:1, 7:1, 8:1, 9:1 }; - - val = val || ''; - ref = ref || ''; - - while (vi < val.length || ri < ref.length) { - var first_diff = 0; - - while ((vi < val.length && !isdigit[val.charAt(vi)]) || - (ri < ref.length && !isdigit[ref.charAt(ri)])) { - var vc = orderOf(val.charAt(vi)), rc = orderOf(ref.charAt(ri)); - if (vc !== rc) - return vc - rc; - - vi++; ri++; - } - - while (val.charAt(vi) === '0') - vi++; - - while (ref.charAt(ri) === '0') - ri++; - - while (isdigit[val.charAt(vi)] && isdigit[ref.charAt(ri)]) { - first_diff = first_diff || (val.charCodeAt(vi) - ref.charCodeAt(ri)); - vi++; ri++; - } - - if (isdigit[val.charAt(vi)]) - return 1; - else if (isdigit[ref.charAt(ri)]) - return -1; - else if (first_diff) - return first_diff; - } - - return 0; - } - - function versionSatisfied(ver, ref, vop) - { - var r = compareVersion(ver, ref); - - switch (vop) { - case '<': - case '<=': - return r <= 0; - - case '>': - case '>=': - return r >= 0; - - case '<<': - return r < 0; - - case '>>': - return r > 0; - - case '=': - return r == 0; - } - - return false; - } - - function pkgStatus(pkg, vop, ver, info) - { - info.errors = info.errors || []; - info.install = info.install || []; - - if (pkg.installed) { - if (vop && !versionSatisfied(pkg.version, ver, vop)) { - var repl = null; - - (packages.available.providers[pkg.name] || []).forEach(function(p) { - if (!repl && versionSatisfied(p.version, ver, vop)) - repl = p; - }); - - if (repl) { - info.install.push(repl); - return E('span', { - 'class': 'label', - 'data-tooltip': _('Requires update to %h %h') - .format(repl.name, repl.version) - }, _('Needs upgrade')); - } - - info.errors.push(_('The installed version of package <em>%h</em> is not compatible, require %s while %s is installed.').format(pkg.name, truncateVersion(ver, vop), truncateVersion(pkg.version))); - - return E('span', { - 'class': 'label warning', - 'data-tooltip': _('Require version %h %h,\ninstalled %h') - .format(vop, ver, pkg.version) - }, _('Version incompatible')); - } - - return E('span', { 'class': 'label notice' }, _('Installed')); - } - else if (!pkg.missing) { - if (!vop || versionSatisfied(pkg.version, ver, vop)) { - info.install.push(pkg); - return E('span', { 'class': 'label' }, _('Not installed')); - } - - info.errors.push(_('The repository version of package <em>%h</em> is not compatible, require %s but only %s is available.') - .format(pkg.name, truncateVersion(ver, vop), truncateVersion(pkg.version))); - - return E('span', { - 'class': 'label warning', - 'data-tooltip': _('Require version %h %h,\ninstalled %h') - .format(vop, ver, pkg.version) - }, _('Version incompatible')); - } - else { - info.errors.push(_('Required dependency package <em>%h</em> is not available in any repository.').format(pkg.name)); - - return E('span', { 'class': 'label warning' }, _('Not available')); - } - } - - function renderDependencyItem(dep, info) - { - var li = E('li'), - vop = dep.version ? dep.version[0] : null, - ver = dep.version ? dep.version[1] : null, - depends = []; - - for (var i = 0; dep.pkgs && i < dep.pkgs.length; i++) { - var pkg = packages.installed.pkgs[dep.pkgs[i]] || - packages.available.pkgs[dep.pkgs[i]] || - { name: dep.name }; - - if (i > 0) - li.appendChild(document.createTextNode(' | ')); - - var text = pkg.name; - - if (pkg.installsize) - text += ' (%.1024mB)'.format(pkg.installsize); - else if (pkg.size) - text += ' (~%.1024mB)'.format(pkg.size); - - li.appendChild(E('span', { 'data-tooltip': pkg.description }, - [ text, ' ', pkgStatus(pkg, vop, ver, info) ])); - - (pkg.depends || []).forEach(function(d) { - if (depends.indexOf(d) === -1) - depends.push(d); - }); - } - - if (!li.firstChild) - li.appendChild(E('span', {}, - [ dep.name, ' ', - pkgStatus({ name: dep.name, missing: true }, vop, ver, info) ])); - - var subdeps = renderDependencies(depends, info); - if (subdeps) - li.appendChild(subdeps); - - return li; - } - - function renderDependencies(depends, info) - { - var deps = depends || [], - items = []; - - info.seen = info.seen || []; - - for (var i = 0; i < deps.length; i++) { - if (deps[i] === 'libc') - continue; - - if (deps[i].match(/^(.+)\s+\((<=|<|>|>=|=|<<|>>)(.+)\)$/)) { - dep = RegExp.$1.trim(); - vop = RegExp.$2.trim(); - ver = RegExp.$3.trim(); - } - else { - dep = deps[i].trim(); - vop = ver = null; - } - - if (info.seen[dep]) - continue; - - var pkgs = []; - - (packages.installed.providers[dep] || []).forEach(function(p) { - if (pkgs.indexOf(p.name) === -1) pkgs.push(p.name); - }); - - (packages.available.providers[dep] || []).forEach(function(p) { - if (pkgs.indexOf(p.name) === -1) pkgs.push(p.name); - }); - - info.seen[dep] = { - name: dep, - pkgs: pkgs, - version: [vop, ver] - }; - - items.push(renderDependencyItem(info.seen[dep], info)); - } - - if (items.length) - return E('ul', { 'class': 'deps' }, items); - - return null; - } - - function truncateVersion(v, op) - { - v = v.replace(/\b(([a-f0-9]{8})[a-f0-9]{24,32})\b/, - '<span data-tooltip="$1">$2…</span>'); - - if (!op || op === '=') - return v; - - return '%h %h'.format(op, v); - } - - function handleReset(ev) - { - var filter = document.querySelector('input[name="filter"]'); - - filter.value = ''; - display(); - } - - function handleInstall(ev) - { - var name = ev.target.getAttribute('data-package'), - pkg = packages.available.pkgs[name], - depcache = {}, - size; - - if (pkg.installsize) - size = _('~%.1024mB installed').format(pkg.installsize); - else if (pkg.size) - size = _('~%.1024mB compressed').format(pkg.size); - else - size = _('unknown'); - - var deps = renderDependencies(pkg.depends, depcache), - tree = null, errs = null, inst = null, desc = null; - - if (depcache.errors && depcache.errors.length) { - errs = E('ul', { 'class': 'errors' }); - depcache.errors.forEach(function(err) { - errs.appendChild(E('li', {}, err)); - }); - } - - var totalsize = pkg.installsize || pkg.size || 0, - totalpkgs = 1; - - if (depcache.install && depcache.install.length) - depcache.install.forEach(function(ipkg) { - totalsize += ipkg.installsize || ipkg.size || 0; - totalpkgs++; - }); - - inst = E('p', {}, - _('Require approx. %.1024mB size for %d package(s) to install.') - .format(totalsize, totalpkgs)); - - if (deps) { - tree = E('li', '<strong>%s:</strong>'.format(_('Dependencies'))); - tree.appendChild(deps); - } - - if (pkg.description) { - desc = E('div', {}, [ - E('h5', {}, _('Description')), - E('p', {}, pkg.description) - ]); - } - - showModal(_('Details for package <em>%h</em>').format(pkg.name), [ - E('ul', {}, [ - E('li', '<strong>%s:</strong> %h'.format(_('Version'), pkg.version)), - E('li', '<strong>%s:</strong> %h'.format(_('Size'), size)), - tree || '', - ]), - desc || '', - errs || inst || '', - E('div', { 'class': 'right' }, [ - E('button', { - 'class': 'btn', - 'click': hideModal - }, _('Cancel')), - ' ', - E('button', { - 'data-command': 'install', - 'data-package': name, - 'class': 'btn cbi-button-action', - 'click': handleOpkg - }, _('Install')) - ]) - ]); - } - - function handleManualInstall(ev) - { - var name_or_url = document.querySelector('input[name="install"]').value, - install = E('button', { - 'class': 'btn cbi-button-action', - 'data-command': 'install', - 'data-package': name_or_url, - 'click': function(ev) { - document.querySelector('input[name="install"]').value = ''; - handleOpkg(ev); - } - }, _('Install')), warning; - - if (!name_or_url.length) { - return; - } - else if (name_or_url.indexOf('/') !== -1) { - warning = E('p', {}, _('Installing packages from untrusted sources is a potential security risk! Really attempt to install <em>%h</em>?').format(name_or_url)); - } - else if (!packages.available.providers[name_or_url]) { - warning = E('p', {}, _('The package <em>%h</em> is not available in any configured repository.').format(name_or_url)); - install = ''; - } - else { - warning = E('p', {}, _('Really attempt to install <em>%h</em>?').format(name_or_url)); - } - - showModal(_('Manually install package'), [ - warning, - E('div', { 'class': 'right' }, [ - E('button', { - 'click': hideModal, - 'class': 'btn cbi-button-neutral' - }, _('Cancel')), - ' ', install - ]) - ]); - } - - function handleConfig(ev) - { - showModal(_('OPKG Configuration'), [ - E('p', { 'class': 'spinning' }, _('Loading configuration data…')) - ]); - - XHR.get('<%=url("admin/system/opkg/config")%>', null, function(xhr, conf) { - var body = [ - E('p', {}, _('Below is a listing of the various configuration files used by <em>opkg</em>. Use <em>opkg.conf</em> for global settings and <em>customfeeds.conf</em> for custom repository entries. The configuration in the other files may be changed but is usually not preserved by <em>sysupgrade</em>.')) - ]; - - Object.keys(conf).sort().forEach(function(file) { - body.push(E('h5', {}, '%h'.format(file))); - body.push(E('textarea', { - 'name': file, - 'rows': Math.max(Math.min(conf[file].match(/\n/g).length, 10), 3) - }, '%h'.format(conf[file]))); - }); - - body.push(E('div', { 'class': 'right' }, [ - E('button', { - 'class': 'btn cbi-button-neutral', - 'click': hideModal - }, _('Cancel')), - ' ', - E('button', { - 'class': 'btn cbi-button-positive', - 'click': function(ev) { - var data = {}; - findParent(ev.target, '.modal').querySelectorAll('textarea[name]') - .forEach(function(textarea) { - data[textarea.getAttribute('name')] = textarea.value - }); - - showModal(_('OPKG Configuration'), [ - E('p', { 'class': 'spinning' }, _('Saving configuration data…')) - ]); - - (new XHR()).post('<%=url("admin/system/opkg/config")%>', - { token: '<%=token%>', data: JSON.stringify(data) }, hideModal); - } - }, _('Save')), - ])); - - showModal(_('OPKG Configuration'), body); - }); - } - - function handleRemove(ev) - { - var name = ev.target.getAttribute('data-package'), - pkg = packages.installed.pkgs[name], - avail = packages.available.pkgs[name] || {}, - size, desc; - - if (avail.installsize) - size = _('~%.1024mB installed').format(avail.installsize); - else if (avail.size) - size = _('~%.1024mB compressed').format(avail.size); - else - size = _('unknown'); - - if (avail.description) { - desc = E('div', {}, [ - E('h5', {}, _('Description')), - E('p', {}, avail.description) - ]); - } - - showModal(_('Remove package <em>%h</em>').format(pkg.name), [ - E('ul', {}, [ - E('li', '<strong>%s:</strong> %h'.format(_('Version'), pkg.version)), - E('li', '<strong>%s:</strong> %h'.format(_('Size'), size)) - ]), - desc || '', - E('div', { 'style': 'display:flex; justify-content:space-between; flex-wrap:wrap' }, [ - E('label', {}, [ - E('input', { type: 'checkbox', checked: 'checked', name: 'autoremove' }), - _('Automatically remove unused dependencies') - ]), - E('div', { 'style': 'flex-grow:1', 'class': 'right' }, [ - E('button', { - 'class': 'btn', - 'click': hideModal - }, _('Cancel')), - ' ', - E('button', { - 'data-command': 'remove', - 'data-package': name, - 'class': 'btn cbi-button-negative', - 'click': handleOpkg - }, _('Remove')) - ]) - ]) - ]); - } - - function handleOpkg(ev) - { - var cmd = ev.target.getAttribute('data-command'), - pkg = ev.target.getAttribute('data-package'), - rem = document.querySelector('input[name="autoremove"]'), - url = '<%=url("admin/system/opkg/exec")%>/' + encodeURIComponent(cmd); - - var dlg = showModal(_('Executing package manager'), [ - E('p', { 'class': 'spinning' }, - _('Waiting for the <em>opkg %h</em> command to complete…').format(cmd)) - ]); - - (new XHR()).post(url, { - token: '<%=token%>', - package: pkg, - autoremove: rem ? rem.checked : false - }, function(xhr, res) { - dlg.removeChild(dlg.lastChild); - - if (res.stdout) - dlg.appendChild(E('pre', [ res.stdout ])); - - if (res.stderr) { - dlg.appendChild(E('h5', _('Errors'))); - dlg.appendChild(E('pre', { 'class': 'errors' }, [ res.stderr ])); - } - - if (res.code !== 0) - dlg.appendChild(E('p', _('The <em>opkg %h</em> command failed with code <code>%d</code>.').format(cmd, (res.code & 0xff) || -1))); - - dlg.appendChild(E('div', { 'class': 'right' }, - E('button', { - 'class': 'btn', - 'click': function() { - hideModal(); - updateLists(); - } - }, _('Dismiss')))); - }); - } - - function updateLists() - { - cbi_update_table('#packages', [], - E('div', { 'class': 'spinning' }, _('Loading package information…'))); - - packages.available = { providers: {}, pkgs: {} }; - packages.installed = { providers: {}, pkgs: {} }; - - XHR.get('<%=url("admin/system/opkg/statvfs")%>', null, function(xhr, stat) { - var pg = document.querySelector('.cbi-progressbar'), - total = stat.blocks || 0, - free = stat.bfree || 0; - - pg.firstElementChild.style.width = Math.floor(total ? ((100 / total) * free) : 100) + '%'; - pg.setAttribute('title', '%s (%.1024mB)'.format(pg.firstElementChild.style.width, free * (stat.frsize || 0))); - - XHR.get('<%=url("admin/system/opkg/list/available")%>', null, function(xhr) { - parseList(xhr.responseText, packages.available); - XHR.get('<%=url("admin/system/opkg/list/installed")%>', null, function(xhr) { - parseList(xhr.responseText, packages.installed); - display(document.querySelector('input[name="filter"]').value); - }); - }); - }); - } - - window.requestAnimationFrame(function() { - var filter = document.querySelector('input[name="filter"]'), - keyTimeout = null; - - filter.value = ''; - filter.addEventListener('keyup', - function(ev) { - if (keyTimeout !== null) - window.clearTimeout(keyTimeout); - - keyTimeout = window.setTimeout(function() { - display(ev.target.value); - }, 250); - }); - - document.querySelector('#pager > .prev').addEventListener('click', handlePage); - document.querySelector('#pager > .next').addEventListener('click', handlePage); - document.querySelector('.cbi-tabmenu.mode').addEventListener('click', handleMode); - - updateLists(); - }); -//]]></script> - <h2><%:Software%></h2> <div class="controls"> @@ -913,7 +93,7 @@ <div> <label><%:Filter%>:</label> - <input type="text" name="filter" placeholder="<%:Type to filter…%>" /><!-- + <input type="text" name="filter" placeholder="<%:Type to filter…%>"<%=attr("value", luci.http.formvalue("query") or "")%> /><!-- --><button class="btn cbi-button" onclick="handleReset(event)"><%:Clear%></button> </div> @@ -955,4 +135,6 @@ </div> </div> +<script type="text/javascript" src="<%=resource%>/view/opkg.js"></script> + <%+footer%> @@ -84,7 +84,7 @@ PKG_GITBRANCH?=$(if $(DUMP),x,$(strip $(shell \ PKG_RELEASE?=1 PKG_INSTALL:=$(if $(realpath src/Makefile),1) PKG_BUILD_DEPENDS += lua/host luci-base/host $(LUCI_BUILD_DEPENDS) -PKG_CONFIG_DEPENDS += CONFIG_LUCI_SRCDIET +PKG_CONFIG_DEPENDS += CONFIG_LUCI_SRCDIET CONFIG_LUCI_JSMIN PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME) @@ -113,6 +113,10 @@ ifeq ($(PKG_NAME),luci-base) bool "Minify Lua sources" default n + config LUCI_JSMIN + bool "Minify JavaScript sources" + default y + menu "Translations"$(foreach lang,$(LUCI_LANGUAGES), config LUCI_LANG_$(lang) @@ -158,6 +162,13 @@ define SrcDiet done endef +define JsMin + $(FIND) $(1) -type f -name '*.js' | while read src; do \ + if jsmin < "$$$$src" > "$$$$src.o"; \ + then mv "$$$$src.o" "$$$$src"; fi; \ + done +endef + define SubstituteVersion $(FIND) $(1) -type f -name '*.htm' | while read src; do \ $(SED) 's/<%# *\([^ ]*\)PKG_VERSION *%>/\1$(PKG_VERSION)/g' \ @@ -177,6 +188,7 @@ define Package/$(PKG_NAME)/install if [ -d $(PKG_BUILD_DIR)/htdocs ]; then \ $(INSTALL_DIR) $(1)$(HTDOCS); \ cp -pR $(PKG_BUILD_DIR)/htdocs/* $(1)$(HTDOCS)/; \ + $(if $(CONFIG_LUCI_JSMIN),$(call JsMin,$(1)$(HTDOCS)/),true); \ else true; fi if [ -d $(PKG_BUILD_DIR)/root ]; then \ $(INSTALL_DIR) $(1)/; \ diff --git a/modules/luci-base/Makefile b/modules/luci-base/Makefile index 06ee7985eb..9bc8ec17a1 100644 --- a/modules/luci-base/Makefile +++ b/modules/luci-base/Makefile @@ -36,13 +36,14 @@ define Host/Configure endef define Host/Compile - $(MAKE) -C src/ clean po2lmo + $(MAKE) -C src/ clean po2lmo jsmin endef define Host/Install $(INSTALL_DIR) $(1)/bin $(INSTALL_DIR) $(1)/lib/lua/5.1 $(INSTALL_BIN) src/po2lmo $(1)/bin/po2lmo + $(INSTALL_BIN) src/jsmin $(1)/bin/jsmin $(INSTALL_BIN) $(HOST_BUILD_DIR)/bin/luasrcdiet $(1)/bin/luasrcdiet $(CP) $(HOST_BUILD_DIR)/luasrcdiet $(1)/lib/lua/5.1/ endef diff --git a/modules/luci-base/htdocs/luci-static/resources/cbi.js b/modules/luci-base/htdocs/luci-static/resources/cbi.js index 1607b9af65..edf634ee74 100644 --- a/modules/luci-base/htdocs/luci-static/resources/cbi.js +++ b/modules/luci-base/htdocs/luci-static/resources/cbi.js @@ -2,7 +2,7 @@ LuCI - Lua Configuration Interface Copyright 2008 Steven Barth <steven@midlink.org> - Copyright 2008-2012 Jo-Philipp Wich <jow@openwrt.org> + Copyright 2008-2018 Jo-Philipp Wich <jo@mein.io> Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -1478,107 +1478,11 @@ if (!window.requestAnimationFrame) { } -var dummyElem, domParser; - -function isElem(e) -{ - return (typeof(e) === 'object' && e !== null && 'nodeType' in e); -} - -function toElem(s) -{ - var elem; - - try { - domParser = domParser || new DOMParser(); - elem = domParser.parseFromString(s, 'text/html').body.firstChild; - } - catch(e) {} - - if (!elem) { - try { - dummyElem = dummyElem || document.createElement('div'); - dummyElem.innerHTML = s; - elem = dummyElem.firstChild; - } - catch (e) {} - } - - return elem || null; -} - -function matchesElem(node, selector) -{ - return ((node.matches && node.matches(selector)) || - (node.msMatchesSelector && node.msMatchesSelector(selector))); -} - -function findParent(node, selector) -{ - if (node.closest) - return node.closest(selector); - - while (node) - if (matchesElem(node, selector)) - return node; - else - node = node.parentNode; - - return null; -} - -function E() -{ - var html = arguments[0], - attr = (arguments[1] instanceof Object && !Array.isArray(arguments[1])) ? arguments[1] : null, - data = attr ? arguments[2] : arguments[1], - elem; - - if (isElem(html)) - elem = html; - else if (html.charCodeAt(0) === 60) - elem = toElem(html); - else - elem = document.createElement(html); - - if (!elem) - return null; - - if (attr) - for (var key in attr) - if (attr.hasOwnProperty(key) && attr[key] !== null && attr[key] !== undefined) - switch (typeof(attr[key])) { - case 'function': - elem.addEventListener(key, attr[key]); - break; - - case 'object': - elem.setAttribute(key, JSON.stringify(attr[key])); - break; - - default: - elem.setAttribute(key, attr[key]); - } - - if (typeof(data) === 'function') - data = data(elem); - - if (isElem(data)) { - elem.appendChild(data); - } - else if (Array.isArray(data)) { - for (var i = 0; i < data.length; i++) - if (isElem(data[i])) - elem.appendChild(data[i]); - else - elem.appendChild(document.createTextNode('' + data[i])); - } - else if (data !== null && data !== undefined) { - elem.innerHTML = '' + data; - } - - return elem; -} +function isElem(e) { return L.dom.elem(e) } +function toElem(s) { return L.dom.parse(s) } +function matchesElem(node, selector) { return L.dom.matches(node, selector) } +function findParent(node, selector) { return L.dom.parent(node, selector) } +function E() { return L.dom.create.apply(L.dom, arguments) } if (typeof(window.CustomEvent) !== 'function') { function CustomEvent(event, params) { @@ -2278,96 +2182,18 @@ function cbi_update_table(table, data, placeholder) { }); } -var tooltipDiv = null, tooltipTimeout = null; - -function showTooltip(ev) { - var target = findParent(ev.target, '[data-tooltip]'); - - if (!target) - return; - - if (tooltipTimeout !== null) { - window.clearTimeout(tooltipTimeout); - tooltipTimeout = null; - } - - var rect = target.getBoundingClientRect(), - x = rect.left + window.pageXOffset, - y = rect.top + rect.height + window.pageYOffset; - - tooltipDiv.className = 'cbi-tooltip'; - tooltipDiv.innerHTML = '▲ '; - tooltipDiv.firstChild.data += target.getAttribute('data-tooltip'); - - if (target.hasAttribute('data-tooltip-style')) - tooltipDiv.classList.add(target.getAttribute('data-tooltip-style')); - - if ((y + tooltipDiv.offsetHeight) > (window.innerHeight + window.pageYOffset)) { - y -= (tooltipDiv.offsetHeight + target.offsetHeight); - tooltipDiv.firstChild.data = '▼ ' + tooltipDiv.firstChild.data.substr(2); - } - - tooltipDiv.style.top = y + 'px'; - tooltipDiv.style.left = x + 'px'; - tooltipDiv.style.opacity = 1; -} - -function hideTooltip(ev) { - if (ev.target === tooltipDiv || ev.relatedTarget === tooltipDiv) - return; - - if (tooltipTimeout !== null) { - window.clearTimeout(tooltipTimeout); - tooltipTimeout = null; - } - - tooltipDiv.style.opacity = 0; - tooltipTimeout = window.setTimeout(function() { tooltipDiv.removeAttribute('style'); }, 250); -} - - -var modalDiv = null; - function showModal(title, children) { - var dlg = modalDiv.firstElementChild; - - while (dlg.firstChild) - dlg.removeChild(dlg.firstChild); - - dlg.setAttribute('class', 'modal'); - dlg.appendChild(E('h4', {}, title)); - - if (!Array.isArray(children)) - children = [ children ]; - - for (var i = 0; i < children.length; i++) - if (isElem(children[i])) - dlg.appendChild(children[i]); - else - dlg.appendChild(document.createTextNode('' + children[i])); - - document.body.classList.add('modal-overlay-active'); - - return dlg; + return L.showModal(title, children); } function hideModal() { - document.body.classList.remove('modal-overlay-active'); + return L.hideModal(); } document.addEventListener('DOMContentLoaded', function() { - tooltipDiv = document.body.appendChild(E('div', { 'class': 'cbi-tooltip' })); - modalDiv = document.body.appendChild(E('div', { 'id': 'modal_overlay' }, - E('div', { 'class': 'modal' }))); - - document.addEventListener('mouseover', showTooltip, true); - document.addEventListener('mouseout', hideTooltip, true); - document.addEventListener('focus', showTooltip, true); - document.addEventListener('blur', hideTooltip, true); - document.addEventListener('validation-failure', function(ev) { if (ev.target === document.activeElement) showTooltip(ev); diff --git a/modules/luci-base/htdocs/luci-static/resources/luci.js b/modules/luci-base/htdocs/luci-static/resources/luci.js new file mode 100644 index 0000000000..dcda941f7b --- /dev/null +++ b/modules/luci-base/htdocs/luci-static/resources/luci.js @@ -0,0 +1,323 @@ +(function(window, document, undefined) { + var modalDiv = null, + tooltipDiv = null, + tooltipTimeout = null, + dummyElem = null, + domParser = null; + + LuCI.prototype = { + /* URL construction helpers */ + path: function(prefix, parts) { + var url = [ prefix || '' ]; + + for (var i = 0; i < parts.length; i++) + if (/^(?:[a-zA-Z0-9_.%,;-]+\/)*[a-zA-Z0-9_.%,;-]+$/.test(parts[i])) + url.push('/', parts[i]); + + if (url.length === 1) + url.push('/'); + + return url.join(''); + }, + + url: function() { + return this.path(this.env.scriptname, arguments); + }, + + resource: function() { + return this.path(this.env.resource, arguments); + }, + + location: function() { + return this.path(this.env.scriptname, this.env.requestpath); + }, + + + /* HTTP resource fetching */ + get: function(url, args, cb) { + return this.poll(0, url, args, cb, false); + }, + + post: function(url, args, cb) { + return this.poll(0, url, args, cb, true); + }, + + poll: function(interval, url, args, cb, post) { + var data = post ? { token: this.env.token } : null; + + if (!/^(?:\/|\S+:\/\/)/.test(url)) + url = this.url(url); + + if (typeof(args) === 'object' && args !== null) { + data = data || {}; + + for (var key in args) + if (args.hasOwnProperty(key)) + switch (typeof(args[key])) { + case 'string': + case 'number': + case 'boolean': + data[key] = args[key]; + break; + + case 'object': + data[key] = JSON.stringify(args[key]); + break; + } + } + + if (interval > 0) + return XHR.poll(interval, url, data, cb, post); + else if (post) + return XHR.post(url, data, cb); + else + return XHR.get(url, data, cb); + }, + + halt: function() { XHR.halt() }, + run: function() { XHR.run() }, + + + /* Modal dialog */ + showModal: function(title, children) { + var dlg = modalDiv.firstElementChild; + + dlg.setAttribute('class', 'modal'); + + this.dom.content(dlg, this.dom.create('h4', {}, title)); + this.dom.append(dlg, children); + + document.body.classList.add('modal-overlay-active'); + + return dlg; + }, + + hideModal: function() { + document.body.classList.remove('modal-overlay-active'); + }, + + + /* Tooltip */ + showTooltip: function(ev) { + var target = findParent(ev.target, '[data-tooltip]'); + + if (!target) + return; + + if (tooltipTimeout !== null) { + window.clearTimeout(tooltipTimeout); + tooltipTimeout = null; + } + + var rect = target.getBoundingClientRect(), + x = rect.left + window.pageXOffset, + y = rect.top + rect.height + window.pageYOffset; + + tooltipDiv.className = 'cbi-tooltip'; + tooltipDiv.innerHTML = '▲ '; + tooltipDiv.firstChild.data += target.getAttribute('data-tooltip'); + + if (target.hasAttribute('data-tooltip-style')) + tooltipDiv.classList.add(target.getAttribute('data-tooltip-style')); + + if ((y + tooltipDiv.offsetHeight) > (window.innerHeight + window.pageYOffset)) { + y -= (tooltipDiv.offsetHeight + target.offsetHeight); + tooltipDiv.firstChild.data = '▼ ' + tooltipDiv.firstChild.data.substr(2); + } + + tooltipDiv.style.top = y + 'px'; + tooltipDiv.style.left = x + 'px'; + tooltipDiv.style.opacity = 1; + }, + + hideTooltip: function(ev) { + if (ev.target === tooltipDiv || ev.relatedTarget === tooltipDiv) + return; + + if (tooltipTimeout !== null) { + window.clearTimeout(tooltipTimeout); + tooltipTimeout = null; + } + + tooltipDiv.style.opacity = 0; + tooltipTimeout = window.setTimeout(function() { tooltipDiv.removeAttribute('style'); }, 250); + }, + + + /* Widget helper */ + itemlist: function(node, items, separators) { + var children = []; + + if (!Array.isArray(separators)) + separators = [ separators || E('br') ]; + + for (var i = 0; i < items.length; i += 2) { + if (items[i+1] !== null && items[i+1] !== undefined) { + var sep = separators[(i/2) % separators.length], + cld = []; + + children.push(E('span', { class: 'nowrap' }, [ + items[i] ? E('strong', items[i] + ': ') : '', + items[i+1] + ])); + + if ((i+2) < items.length) + children.push(this.dom.elem(sep) ? sep.cloneNode(true) : sep); + } + } + + this.dom.content(node, children); + + return node; + } + }; + + /* DOM manipulation */ + LuCI.prototype.dom = { + elem: function(e) { + return (typeof(e) === 'object' && e !== null && 'nodeType' in e); + }, + + parse: function(s) { + var elem; + + try { + domParser = domParser || new DOMParser(); + elem = domParser.parseFromString(s, 'text/html').body.firstChild; + } + catch(e) {} + + if (!elem) { + try { + dummyElem = dummyElem || document.createElement('div'); + dummyElem.innerHTML = s; + elem = dummyElem.firstChild; + } + catch (e) {} + } + + return elem || null; + }, + + matches: function(node, selector) { + var m = this.elem(node) ? node.matches || node.msMatchesSelector : null; + return m ? m.call(node, selector) : false; + }, + + parent: function(node, selector) { + if (this.elem(node) && node.closest) + return node.closest(selector); + + while (this.elem(node)) + if (this.matches(node, selector)) + return node; + else + node = node.parentNode; + + return null; + }, + + append: function(node, children) { + if (!this.elem(node)) + return null; + + if (Array.isArray(children)) { + for (var i = 0; i < children.length; i++) + if (this.elem(children[i])) + node.appendChild(children[i]); + else if (children !== null && children !== undefined) + node.appendChild(document.createTextNode('' + children[i])); + + return node.lastChild; + } + else if (typeof(children) === 'function') { + return this.append(node, children(node)); + } + else if (this.elem(children)) { + return node.appendChild(children); + } + else if (children !== null && children !== undefined) { + node.innerHTML = '' + children; + return node.lastChild; + } + + return null; + }, + + content: function(node, children) { + if (!this.elem(node)) + return null; + + while (node.firstChild) + node.removeChild(node.firstChild); + + return this.append(node, children); + }, + + attr: function(node, key, val) { + if (!this.elem(node)) + return null; + + var attr = null; + + if (typeof(key) === 'object' && key !== null) + attr = key; + else if (typeof(key) === 'string') + attr = {}, attr[key] = val; + + for (key in attr) { + if (!attr.hasOwnProperty(key) || attr[key] === null || attr[key] === undefined) + continue; + + switch (typeof(attr[key])) { + case 'function': + node.addEventListener(key, attr[key]); + break; + + case 'object': + node.setAttribute(key, JSON.stringify(attr[key])); + break; + + default: + node.setAttribute(key, attr[key]); + } + } + }, + + create: function() { + var html = arguments[0], + attr = (arguments[1] instanceof Object && !Array.isArray(arguments[1])) ? arguments[1] : null, + data = attr ? arguments[2] : arguments[1], + elem; + + if (this.elem(html)) + elem = html; + else if (html.charCodeAt(0) === 60) + elem = this.parse(html); + else + elem = document.createElement(html); + + if (!elem) + return null; + + this.attr(elem, attr); + this.append(elem, data); + + return elem; + } + }; + + function LuCI(env) { + this.env = env; + + modalDiv = document.body.appendChild(this.dom.create('div', { id: 'modal_overlay' }, this.dom.create('div', { class: 'modal' }))); + tooltipDiv = document.body.appendChild(this.dom.create('div', { class: 'cbi-tooltip' })); + + document.addEventListener('mouseover', this.showTooltip.bind(this), true); + document.addEventListener('mouseout', this.hideTooltip.bind(this), true); + document.addEventListener('focus', this.showTooltip.bind(this), true); + document.addEventListener('blur', this.hideTooltip.bind(this), true); + } + + window.LuCI = LuCI; +})(window, document); diff --git a/modules/luci-base/luasrc/view/cbi/value.htm b/modules/luci-base/luasrc/view/cbi/value.htm index 79a358b305..27f3cb2bd9 100644 --- a/modules/luci-base/luasrc/view/cbi/value.htm +++ b/modules/luci-base/luasrc/view/cbi/value.htm @@ -21,6 +21,6 @@ ifattr(#self.keylist > 0, "data-choices", { self.keylist, self.vallist }) %> /> <%- if self.password then -%> - <button class="cbi-button cbi-button-neutral" title="<%:Reveal/hide password%>" aria-label="<%:Reveal/hide password%>" onclick="var e = this.previousElementSibling; e.type = (e.type === 'password') ? 'text' : 'password'">∗</button> + <button class="cbi-button cbi-button-neutral" title="<%:Reveal/hide password%>" aria-label="<%:Reveal/hide password%>" onclick="var e = this.previousElementSibling; e.type = (e.type === 'password') ? 'text' : 'password'; event.preventDefault()">∗</button> <% end %> <%+cbi/valuefooter%> diff --git a/modules/luci-base/luasrc/view/header.htm b/modules/luci-base/luasrc/view/header.htm index f6e20c9a40..2813c4d943 100644 --- a/modules/luci-base/luasrc/view/header.htm +++ b/modules/luci-base/luasrc/view/header.htm @@ -10,3 +10,14 @@ luci.dispatcher.context.template_header_sent = true end %> + +<script type="text/javascript" src="<%=resource%>/luci.js"></script> +<script type="text/javascript"> + L = new LuCI(<%= luci.http.write_json({ + token = token, + resource = resource, + scriptname = luci.http.getenv("SCRIPT_NAME"), + pathinfo = luci.http.getenv("PATH_INFO"), + requestpath = luci.dispatcher.context.requestpath + }) %>); +</script> diff --git a/modules/luci-base/src/Makefile b/modules/luci-base/src/Makefile index 03e887e1d5..3e6ead1085 100644 --- a/modules/luci-base/src/Makefile +++ b/modules/luci-base/src/Makefile @@ -4,6 +4,9 @@ clean: rm -f po2lmo parser.so version.lua *.o +jsmin: jsmin.o + $(CC) $(LDFLAGS) -o $@ $^ + po2lmo: po2lmo.o template_lmo.o $(CC) $(LDFLAGS) -o $@ $^ diff --git a/modules/luci-base/src/jsmin.c b/modules/luci-base/src/jsmin.c new file mode 100644 index 0000000000..d23718df39 --- /dev/null +++ b/modules/luci-base/src/jsmin.c @@ -0,0 +1,292 @@ +/* jsmin.c + 2011-09-30 + +Copyright (c) 2002 Douglas Crockford (www.crockford.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +The Software shall be used for Good, not Evil. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include <stdlib.h> +#include <stdio.h> + +static int theA; +static int theB; +static int theLookahead = EOF; + + +/* isAlphanum -- return true if the character is a letter, digit, underscore, + dollar sign, or non-ASCII character. +*/ + +static int +isAlphanum(int c) +{ + return ((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || + (c >= 'A' && c <= 'Z') || c == '_' || c == '$' || c == '\\' || + c > 126); +} + + +/* get -- return the next character from stdin. Watch out for lookahead. If + the character is a control character, translate it to a space or + linefeed. +*/ + +static int +get() +{ + int c = theLookahead; + theLookahead = EOF; + if (c == EOF) { + c = getc(stdin); + } + if (c >= ' ' || c == '\n' || c == EOF) { + return c; + } + if (c == '\r') { + return '\n'; + } + return ' '; +} + + +/* peek -- get the next character without getting it. +*/ + +static int +peek() +{ + theLookahead = get(); + return theLookahead; +} + + +/* next -- get the next character, excluding comments. peek() is used to see + if a '/' is followed by a '/' or '*'. +*/ + +static int +next() +{ + int c = get(); + if (c == '/') { + switch (peek()) { + case '/': + for (;;) { + c = get(); + if (c <= '\n') { + return c; + } + } + case '*': + get(); + for (;;) { + switch (get()) { + case '*': + if (peek() == '/') { + get(); + return ' '; + } + break; + case EOF: + fprintf(stderr, "Error: JSMIN Unterminated comment.\n"); + exit(1); + } + } + default: + return c; + } + } + return c; +} + + +/* action -- do something! What you do is determined by the argument: + 1 Output A. Copy B to A. Get the next B. + 2 Copy B to A. Get the next B. (Delete A). + 3 Get the next B. (Delete B). + action treats a string as a single character. Wow! + action recognizes a regular expression if it is preceded by ( or , or =. +*/ + +static void +action(int d) +{ + switch (d) { + case 1: + putc(theA, stdout); + case 2: + theA = theB; + if (theA == '\'' || theA == '"' || theA == '`') { + for (;;) { + putc(theA, stdout); + theA = get(); + if (theA == theB) { + break; + } + if (theA == '\\') { + putc(theA, stdout); + theA = get(); + } + if (theA == EOF) { + fprintf(stderr, "Error: JSMIN unterminated string literal."); + exit(1); + } + } + } + case 3: + theB = next(); + if (theB == '/' && (theA == '(' || theA == ',' || theA == '=' || + theA == ':' || theA == '[' || theA == '!' || + theA == '&' || theA == '|' || theA == '?' || + theA == '{' || theA == '}' || theA == ';' || + theA == '\n')) { + putc(theA, stdout); + putc(theB, stdout); + for (;;) { + theA = get(); + if (theA == '[') { + for (;;) { + putc(theA, stdout); + theA = get(); + if (theA == ']') { + break; + } + if (theA == '\\') { + putc(theA, stdout); + theA = get(); + } + if (theA == EOF) { + fprintf(stderr, + "Error: JSMIN unterminated set in Regular Expression literal.\n"); + exit(1); + } + } + } else if (theA == '/') { + break; + } else if (theA =='\\') { + putc(theA, stdout); + theA = get(); + } + if (theA == EOF) { + fprintf(stderr, + "Error: JSMIN unterminated Regular Expression literal.\n"); + exit(1); + } + putc(theA, stdout); + } + theB = next(); + } + } +} + + +/* jsmin -- Copy the input to the output, deleting the characters which are + insignificant to JavaScript. Comments will be removed. Tabs will be + replaced with spaces. Carriage returns will be replaced with linefeeds. + Most spaces and linefeeds will be removed. +*/ + +static void +jsmin() +{ + theA = '\n'; + action(3); + while (theA != EOF) { + switch (theA) { + case ' ': + if (isAlphanum(theB)) { + action(1); + } else { + action(2); + } + break; + case '\n': + switch (theB) { + case '{': + case '[': + case '(': + case '+': + case '-': + action(1); + break; + case ' ': + action(3); + break; + default: + if (isAlphanum(theB)) { + action(1); + } else { + action(2); + } + } + break; + default: + switch (theB) { + case ' ': + if (isAlphanum(theA)) { + action(1); + break; + } + action(3); + break; + case '\n': + switch (theA) { + case '}': + case ']': + case ')': + case '+': + case '-': + case '"': + case '\'': + case '`': + action(1); + break; + default: + if (isAlphanum(theA)) { + action(1); + } else { + action(3); + } + } + break; + default: + action(1); + break; + } + } + } +} + + +/* main -- Output any command line arguments as comments + and then minify the input. +*/ +extern int +main(int argc, char* argv[]) +{ + int i; + for (i = 1; i < argc; i += 1) { + fprintf(stdout, "// %s\n", argv[i]); + } + jsmin(); + return 0; +} diff --git a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/network.js b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/network.js new file mode 100644 index 0000000000..acca7cf8a5 --- /dev/null +++ b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/network.js @@ -0,0 +1,135 @@ +function iface_reconnect(id) { + L.halt(); + L.dom.content(document.getElementById(id + '-ifc-description'), E('em', _('Interface is reconnecting...'))); + L.post(L.url('admin/network/iface_reconnect', id), L.run); +} + +function iface_delete(ev) { + if (!confirm(_('Really delete this interface? The deletion cannot be undone! You might lose access to this device if you are connected via this interface'))) { + ev.preventDefault(); + return false; + } + + ev.target.previousElementSibling.value = '1'; + return true; +} + +var networks = []; + +document.querySelectorAll('[data-network]').forEach(function(n) { + networks.push(n.getAttribute('data-network')); +}); + +function render_iface(ifc) { + return E('span', { class: 'cbi-tooltip-container' }, [ + E('img', { 'class' : 'middle', 'src': L.resource('icons/%s%s.png').format( + ifc.is_alias ? 'alias' : ifc.type, + ifc.is_up ? '' : '_disabled') }), + E('span', { 'class': 'cbi-tooltip ifacebadge large' }, [ + E('img', { 'src': L.resource('icons/%s%s.png').format( + ifc.type, ifc.is_up ? '' : '_disabled') }), + L.itemlist(E('span', { 'class': 'left' }), [ + _('Type'), ifc.typename, + _('Device'), ifc.ifname, + _('Connected'), ifc.is_up ? _('yes') : _('no'), + _('MAC'), ifc.macaddr, + _('RX'), '%.2mB (%d %s)'.format(ifc.rx_bytes, ifc.rx_packets, _('Pkts.')), + _('TX'), '%.2mB (%d %s)'.format(ifc.tx_bytes, ifc.tx_packets, _('Pkts.')) + ]) + ]) + ]); +} + +L.poll(5, L.url('admin/network/iface_status', networks.join(',')), null, + function(x, ifcs) { + if (ifcs) { + for (var idx = 0; idx < ifcs.length; idx++) { + var ifc = ifcs[idx]; + + var s = document.getElementById(ifc.id + '-ifc-devices'); + if (s) { + var c = [ render_iface(ifc) ]; + + if (ifc.subdevices && ifc.subdevices.length) + { + var sifs = [ ' (' ]; + + for (var j = 0; j < ifc.subdevices.length; j++) + sifs.push(render_iface(ifc.subdevices[j])); + + sifs.push(')'); + + c.push(E('span', {}, sifs)); + } + + c.push(E('br')); + c.push(E('small', {}, ifc.is_alias ? _('Alias of "%s"').format(ifc.is_alias) : ifc.name)); + + L.dom.content(s, c); + } + + var d = document.getElementById(ifc.id + '-ifc-description'); + if (d && ifc.proto && ifc.ifname) { + var desc = null, c = []; + + if (ifc.is_dynamic) + desc = _('Virtual dynamic interface'); + else if (ifc.is_alias) + desc = _('Alias Interface'); + + if (ifc.desc) + desc = desc ? '%s (%s)'.format(desc, ifc.desc) : ifc.desc; + + L.itemlist(d, [ + _('Protocol'), '%h'.format(desc || '?'), + _('Uptime'), ifc.is_up ? '%t'.format(ifc.uptime) : null, + _('MAC'), (!ifc.is_dynamic && !ifc.is_alias && ifc.macaddr) ? ifc.macaddr : null, + _('RX'), (!ifc.is_dynamic && !ifc.is_alias) ? '%.2mB (%d %s)'.format(ifc.rx_bytes, ifc.rx_packets, _('Pkts.')) : null, + _('TX'), (!ifc.is_dynamic && !ifc.is_alias) ? '%.2mB (%d %s)'.format(ifc.tx_bytes, ifc.tx_packets, _('Pkts.')) : null, + _('IPv4'), ifc.ipaddrs ? ifc.ipaddrs[0] : null, + _('IPv4'), ifc.ipaddrs ? ifc.ipaddrs[1] : null, + _('IPv4'), ifc.ipaddrs ? ifc.ipaddrs[2] : null, + _('IPv4'), ifc.ipaddrs ? ifc.ipaddrs[3] : null, + _('IPv4'), ifc.ipaddrs ? ifc.ipaddrs[4] : null, + _('IPv6'), ifc.ip6addrs ? ifc.ip6addrs[0] : null, + _('IPv6'), ifc.ip6addrs ? ifc.ip6addrs[1] : null, + _('IPv6'), ifc.ip6addrs ? ifc.ip6addrs[2] : null, + _('IPv6'), ifc.ip6addrs ? ifc.ip6addrs[3] : null, + _('IPv6'), ifc.ip6addrs ? ifc.ip6addrs[4] : null, + _('IPv6'), ifc.ip6addrs ? ifc.ip6addrs[5] : null, + _('IPv6'), ifc.ip6addrs ? ifc.ip6addrs[6] : null, + _('IPv6'), ifc.ip6addrs ? ifc.ip6addrs[7] : null, + _('IPv6'), ifc.ip6addrs ? ifc.ip6addrs[8] : null, + _('IPv6'), ifc.ip6addrs ? ifc.ip6addrs[9] : null, + _('IPv6-PD'), ifc.ip6prefix, + _('Error'), ifc.errors ? ifc.errors[0] : null, + _('Error'), ifc.errors ? ifc.errors[1] : null, + _('Error'), ifc.errors ? ifc.errors[2] : null, + _('Error'), ifc.errors ? ifc.errors[3] : null, + _('Error'), ifc.errors ? ifc.errors[4] : null, + ]); + } + else if (d && !ifc.proto) { + var e = document.getElementById(ifc.id + '-ifc-edit'); + if (e) e.disabled = true; + + var link = L.url('admin/system/packages') + '?query=luci-proto&display=available'; + L.dom.content(d, [ + E('em', _('Unsupported protocol type.')), E('br'), + E('a', { href: link }, _('Install protocol extensions...')) + ]); + } + else if (d && !ifc.ifname) { + var link = L.url('admin/network/network', ifc.name) + '?tab.network.%s=physical'.format(ifc.name); + L.dom.content(d, [ + E('em', _('Network without interfaces.')), E('br'), + E('a', { href: link }, _('Assign interfaces...')) + ]); + } + else if (d) { + L.dom.content(d, E('em' ,_('Interface not present or not connected yet.'))); + } + } + } + } +); diff --git a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js new file mode 100644 index 0000000000..bdeb23d235 --- /dev/null +++ b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js @@ -0,0 +1,93 @@ +function wifi_delete(ev) { + if (!confirm(_('Really delete this wireless network? The deletion cannot be undone! You might lose access to this device if you are connected via this network.'))) { + ev.preventDefault(); + return false; + } + + ev.target.previousElementSibling.value = '1'; + return true; +} + +function wifi_restart(ev) { + L.halt(); + + findParent(ev.target, '.table').querySelectorAll('[data-disabled="false"]').forEach(function(s) { + L.dom.content(s, E('em', _('Wireless is restarting...'))); + }); + + L.post(L.url('admin/network/wireless_reconnect', ev.target.getAttribute('data-radio')), L.run); +} + +var networks = [ ]; + +document.querySelectorAll('[data-network]').forEach(function(n) { + networks.push(n.getAttribute('data-network')); +}); + +L.poll(5, L.url('admin/network/wireless_status', networks.join(',')), null, + function(x, st) { + if (st) { + var rowstyle = 1; + var radiostate = { }; + + st.forEach(function(s) { + var r = radiostate[s.device.device] || (radiostate[s.device.device] = {}); + + s.is_assoc = (s.bssid && s.bssid != '00:00:00:00:00:00' && s.channel && s.mode != 'Unknown' && !s.disabled); + + r.up = r.up || s.is_assoc; + r.channel = r.channel || s.channel; + r.bitrate = r.bitrate || s.bitrate; + r.frequency = r.frequency || s.frequency; + }); + + for (var i = 0; i < st.length; i++) { + var iw = st[i], + sig = document.getElementById(iw.id + '-iw-signal'), + info = document.getElementById(iw.id + '-iw-status'), + disabled = (info && info.getAttribute('data-disabled') === 'true'); + + var p = iw.quality; + var q = disabled ? -1 : p; + + var icon; + if (q < 0) + icon = L.resource('icons/signal-none.png'); + else if (q == 0) + icon = L.resource('icons/signal-0.png'); + else if (q < 25) + icon = L.resource('icons/signal-0-25.png'); + else if (q < 50) + icon = L.resource('icons/signal-25-50.png'); + else if (q < 75) + icon = L.resource('icons/signal-50-75.png'); + else + icon = L.resource('icons/signal-75-100.png'); + + L.dom.content(sig, E('span', { + class: 'ifacebadge', + title: '%s %d %s / %s: %d %s'.format(_('Signal'), iw.signal, _('dBm'), _('Noise'), iw.noise, _('dBm')) + }, [ E('img', { src: icon }), ' %d%%'.format(p) ])); + + L.itemlist(info, [ + _('SSID'), '%h'.format(iw.ssid || '?'), + _('Mode'), iw.mode, + _('BSSID'), iw.is_assoc ? iw.bssid : null, + _('Encryption'), iw.is_assoc ? iw.encryption || _('None') : null, + null, iw.is_assoc ? null : E('em', disabled ? _('Wireless is disabled') : _('Wireless is not associated')) + ], [ ' | ', E('br') ]); + } + + for (var dev in radiostate) { + var img = document.getElementById(dev + '-iw-upstate'); + if (img) img.src = L.resource('icons/wifi' + (radiostate[dev].up ? '' : '_disabled') + '.png'); + + var stat = document.getElementById(dev + '-iw-devinfo'); + L.itemlist(stat, [ + _('Channel'), '%s (%s %s)'.format(radiostate[dev].channel || '?', radiostate[dev].frequency || '?', _('GHz')), + _('Bitrate'), '%s %s'.format(radiostate[dev].bitrate || '?', _('Mbit/s')) + ], ' | '); + } + } + } +); diff --git a/modules/luci-mod-network/luasrc/model/cbi/admin_network/network.lua b/modules/luci-mod-network/luasrc/model/cbi/admin_network/network.lua index 0c0ca5263d..b98086dea6 100644 --- a/modules/luci-mod-network/luasrc/model/cbi/admin_network/network.lua +++ b/modules/luci-mod-network/luasrc/model/cbi/admin_network/network.lua @@ -15,59 +15,6 @@ m:chain("dhcp") m.pageaction = false -local tpl_networks = tpl.Template(nil, [[ - <div class="cbi-section-node"> - <div class="table"> - <% - for i, net in ipairs(netlist) do - local z = net[3] - local c = z and z:get_color() or "#EEEEEE" - local t = z and translate("Part of zone %q" % z:name()) or translate("No zone assigned") - local disabled = (net[4]:get("auto") == "0") - local dynamic = net[4]:is_dynamic() - %> - <div class="tr cbi-rowstyle-<%=i % 2 + 1%>"> - <div class="td col-3 center middle"> - <div class="ifacebox"> - <div class="ifacebox-head" style="background-color:<%=c%>" title="<%=pcdata(t)%>"> - <strong><%=net[1]:upper()%></strong> - </div> - <div class="ifacebox-body" id="<%=net[1]%>-ifc-devices" data-network="<%=net[1]%>"> - <img src="<%=resource%>/icons/ethernet_disabled.png" style="width:16px; height:16px" /><br /> - <small>?</small> - </div> - </div> - </div> - <div class="td col-5 left middle" id="<%=net[1]%>-ifc-description"> - <em><%:Collecting data...%></em> - </div> - <div class="td cbi-section-actions"> - <div> - <input type="button" class="cbi-button cbi-button-neutral" onclick="iface_reconnect('<%=net[1]%>')" title="<%:Reconnect this interface%>" value="<%:Restart%>"<%=ifattr(disabled or dynamic, "disabled", "disabled")%> /> - - <% if disabled then %> - <input type="hidden" name="cbid.network.<%=net[1]%>.__disable__" value="1" /> - <input type="submit" name="cbi.apply" class="cbi-button cbi-button-neutral" onclick="this.previousElementSibling.value='0'" title="<%:Reconnect this interface%>" value="<%:Connect%>"<%=ifattr(dynamic, "disabled", "disabled")%> /> - <% else %> - <input type="hidden" name="cbid.network.<%=net[1]%>.__disable__" value="0" /> - <input type="submit" name="cbi.apply" class="cbi-button cbi-button-neutral" onclick="this.previousElementSibling.value='1'" title="<%:Shutdown this interface%>" value="<%:Stop%>"<%=ifattr(dynamic, "disabled", "disabled")%> /> - <% end %> - - <input type="button" class="cbi-button cbi-button-action important" onclick="location.href='<%=url("admin/network/network", net[1])%>'" title="<%:Edit this interface%>" value="<%:Edit%>" id="<%=net[1]%>-ifc-edit"<%=ifattr(dynamic, "disabled", "disabled")%> /> - - <input type="hidden" name="cbid.network.<%=net[1]%>.__delete__" value="" /> - <input type="submit" name="cbi.apply" class="cbi-button cbi-button-negative" onclick="iface_delete(event)" value="<%:Delete%>"<%=ifattr(dynamic, "disabled", "disabled")%> /> - </div> - </div> - </div> - <% end %> - </div> - </div> - <div class="cbi-section-create"> - <input type="button" class="cbi-button cbi-button-add" value="<%:Add new interface...%>" onclick="location.href='<%=url("admin/network/iface_add")%>'" /> - </div> -]]) - local _, net local ifaces, netlist = { }, { } @@ -102,6 +49,8 @@ table.sort(netlist, end) s = m:section(TypedSection, "interface", translate("Interface Overview")) +s.template = "admin_network/iface_overview" +s.netlist = netlist function s.cfgsections(self) local _, net, sl = nil, nil, { } @@ -113,12 +62,6 @@ function s.cfgsections(self) return sl end -function s.render(self) - tpl_networks:render({ - netlist = netlist - }) -end - o = s:option(Value, "__disable__") function o.write(self, sid, value) @@ -138,8 +81,6 @@ function o.write(self, sid, value) end -m:section(SimpleSection).template = "admin_network/iface_overview_status" - if fs.access("/etc/init.d/dsl_control") then local ok, boarddata = pcall(json.parse, fs.readfile("/etc/board.json")) local modemtype = (ok == true) diff --git a/modules/luci-mod-network/luasrc/model/cbi/admin_network/wifi_overview.lua b/modules/luci-mod-network/luasrc/model/cbi/admin_network/wifi_overview.lua index 3bffb3502c..54720d6889 100644 --- a/modules/luci-mod-network/luasrc/model/cbi/admin_network/wifi_overview.lua +++ b/modules/luci-mod-network/luasrc/model/cbi/admin_network/wifi_overview.lua @@ -64,68 +64,6 @@ function guess_wifi_hw(dev) end end -local tpl_radio = tpl.Template(nil, [[ - <div class="cbi-section-node"> - <div class="table"> - <!-- physical device --> - <div class="tr cbi-rowstyle-2"> - <div class="td col-2 center middle"> - <span class="ifacebadge"><img src="<%=resource%>/icons/wifi_disabled.png" id="<%=dev:name()%>-iw-upstate" /> <%=dev:name()%></span> - </div> - <div class="td col-7 left middle"> - <big><strong><%=hw%></strong></big><br /> - <span id="<%=dev:name()%>-iw-devinfo"></span> - </div> - <div class="td middle cbi-section-actions"> - <div> - <input type="button" class="cbi-button cbi-button-neutral" title="<%:Restart radio interface%>" value="<%:Restart%>" data-radio="<%=dev:name()%>" onclick="wifi_restart(event)" /> - <input type="button" class="cbi-button cbi-button-action important" title="<%:Find and join network%>" value="<%:Scan%>" onclick="cbi_submit(this, 'device', '<%=dev:name()%>', '<%=url('admin/network/wireless_join')%>')" /> - <input type="button" class="cbi-button cbi-button-add" title="<%:Provide new network%>" value="<%:Add%>" onclick="cbi_submit(this, 'device', '<%=dev:name()%>', '<%=url('admin/network/wireless_add')%>')" /> - </div> - </div> - </div> - <!-- /physical device --> - - <!-- network list --> - <% if #wnets > 0 then %> - <% for i, net in ipairs(wnets) do local disabled = (dev:get("disabled") == "1" or net:get("disabled") == "1") %> - <div class="tr cbi-rowstyle-<%=1 + ((i-1) % 2)%>"> - <div class="td col-2 center middle" id="<%=net:id()%>-iw-signal"> - <span class="ifacebadge" title="<%:Not associated%>"><img src="<%=resource%>/icons/signal-<%= disabled and "none" or "0" %>.png" /> 0%</span> - </div> - <div class="td col-7 left middle" id="<%=net:id()%>-iw-status" data-network="<%=net:id()%>" data-disabled="<%= disabled and "true" or "false" %>"> - <em><%= disabled and translate("Wireless is disabled") or translate("Collecting data...") %></em> - </div> - <div class="td middle cbi-section-actions"> - <div> - <% if disabled then %> - <input name="cbid.wireless.<%=net:name()%>.__disable__" type="hidden" value="1" /> - <input name="cbi.apply" type="submit" class="cbi-button cbi-button-neutral" title="<%:Enable this network%>" value="<%:Enable%>" onclick="this.previousElementSibling.value='0'" /> - <% else %> - <input name="cbid.wireless.<%=net:name()%>.__disable__" type="hidden" value="0" /> - <input name="cbi.apply" type="submit" class="cbi-button cbi-button-neutral" title="<%:Disable this network%>" value="<%:Disable%>" onclick="this.previousElementSibling.value='1'" /> - <% end %> - - <input type="button" class="cbi-button cbi-button-action important" onclick="location.href='<%=net:adminlink()%>'" title="<%:Edit this network%>" value="<%:Edit%>" /> - - <input name="cbid.wireless.<%=net:name()%>.__delete__" type="hidden" value="" /> - <input name="cbi.apply" type="submit" class="cbi-button cbi-button-negative" title="<%:Delete this network%>" value="<%:Remove%>" onclick="wifi_delete(event)" /> - </div> - </div> - </div> - <% end %> - <% else %> - <div class="tr placeholder"> - <div class="td"> - <em><%:No network configured on this device%></em> - </div> - </div> - <% end %> - <!-- /network list --> - </div> - </div> -]]) - m = Map("wireless", translate("Wireless Overview")) m:chain("network") @@ -147,15 +85,10 @@ end local _, dev, net for _, dev in ipairs(ntm:get_wifidevs()) do s = m:section(TypedSection) + s.template = "admin_network/wifi_overview" s.wnets = dev:get_wifinets() - - function s.render(self, sid) - tpl_radio:render({ - hw = guess_wifi_hw(dev), - dev = dev, - wnets = self.wnets - }) - end + s.dev = dev + s.hw = guess_wifi_hw(dev) function s.cfgsections(self) local _, net, sl = nil, nil, { } @@ -208,9 +141,6 @@ for _, dev in ipairs(ntm:get_wifidevs()) do end end -s = m:section(NamedSection, "__script__") -s.template = "admin_network/wifi_overview_status" - s = m:section(NamedSection, "__assoclist__") function s.render(self, sid) diff --git a/modules/luci-mod-network/luasrc/view/admin_network/iface_overview.htm b/modules/luci-mod-network/luasrc/view/admin_network/iface_overview.htm new file mode 100644 index 0000000000..4fd46e2bff --- /dev/null +++ b/modules/luci-mod-network/luasrc/view/admin_network/iface_overview.htm @@ -0,0 +1,53 @@ +<div class="cbi-section-node"> + <div class="table"> + <% + for i, net in ipairs(self.netlist) do + local z = net[3] + local c = z and z:get_color() or "#EEEEEE" + local t = z and translate("Part of zone %q" % z:name()) or translate("No zone assigned") + local disabled = (net[4]:get("auto") == "0") + local dynamic = net[4]:is_dynamic() + %> + <div class="tr cbi-rowstyle-<%=i % 2 + 1%>"> + <div class="td col-3 center middle"> + <div class="ifacebox"> + <div class="ifacebox-head" style="background-color:<%=c%>" title="<%=pcdata(t)%>"> + <strong><%=net[1]:upper()%></strong> + </div> + <div class="ifacebox-body" id="<%=net[1]%>-ifc-devices" data-network="<%=net[1]%>"> + <img src="<%=resource%>/icons/ethernet_disabled.png" style="width:16px; height:16px" /><br /> + <small>?</small> + </div> + </div> + </div> + <div class="td col-5 left middle" id="<%=net[1]%>-ifc-description"> + <em><%:Collecting data...%></em> + </div> + <div class="td cbi-section-actions"> + <div> + <input type="button" class="cbi-button cbi-button-neutral" onclick="iface_reconnect('<%=net[1]%>')" title="<%:Reconnect this interface%>" value="<%:Restart%>"<%=ifattr(disabled or dynamic, "disabled", "disabled")%> /> + + <% if disabled then %> + <input type="hidden" name="cbid.network.<%=net[1]%>.__disable__" value="1" /> + <input type="submit" name="cbi.apply" class="cbi-button cbi-button-neutral" onclick="this.previousElementSibling.value='0'" title="<%:Reconnect this interface%>" value="<%:Connect%>"<%=ifattr(dynamic, "disabled", "disabled")%> /> + <% else %> + <input type="hidden" name="cbid.network.<%=net[1]%>.__disable__" value="0" /> + <input type="submit" name="cbi.apply" class="cbi-button cbi-button-neutral" onclick="this.previousElementSibling.value='1'" title="<%:Shutdown this interface%>" value="<%:Stop%>"<%=ifattr(dynamic, "disabled", "disabled")%> /> + <% end %> + + <input type="button" class="cbi-button cbi-button-action important" onclick="location.href='<%=url("admin/network/network", net[1])%>'" title="<%:Edit this interface%>" value="<%:Edit%>" id="<%=net[1]%>-ifc-edit"<%=ifattr(dynamic, "disabled", "disabled")%> /> + + <input type="hidden" name="cbid.network.<%=net[1]%>.__delete__" value="" /> + <input type="submit" name="cbi.apply" class="cbi-button cbi-button-negative" onclick="iface_delete(event)" value="<%:Delete%>"<%=ifattr(dynamic, "disabled", "disabled")%> /> + </div> + </div> + </div> + <% end %> + </div> +</div> + +<div class="cbi-section-create"> + <input type="button" class="cbi-button cbi-button-add" value="<%:Add new interface...%>" onclick="location.href='<%=url("admin/network/iface_add")%>'" /> +</div> + +<script type="text/javascript" src="<%=resource%>/view/network/network.js"></script> diff --git a/modules/luci-mod-network/luasrc/view/admin_network/iface_overview_status.htm b/modules/luci-mod-network/luasrc/view/admin_network/iface_overview_status.htm deleted file mode 100644 index 7427154a04..0000000000 --- a/modules/luci-mod-network/luasrc/view/admin_network/iface_overview_status.htm +++ /dev/null @@ -1,183 +0,0 @@ -<%# - Copyright 2010-2018 Jo-Philipp Wich <jo@mein.io> - Licensed to the public under the Apache License 2.0. --%> - -<script type="text/javascript">//<![CDATA[ - function iface_reconnect(id) { - XHR.halt(); - - var d = document.getElementById(id + '-ifc-description'); - if (d) d.innerHTML = '<em><%:Interface is reconnecting...%></em>'; - - (new XHR()).post('<%=url('admin/network/iface_reconnect')%>/' + id, - { token: '<%=token%>' }, XHR.run); - } - - function iface_delete(ev) { - if (!confirm(<%=luci.http.write_json(translate('Really delete this interface? The deletion cannot be undone! You might lose access to this device if you are connected via this interface'))%>)) { - ev.preventDefault(); - return false; - } - - ev.target.previousElementSibling.value = '1'; - return true; - } - - var networks = []; - - document.querySelectorAll('[data-network]').forEach(function(n) { - networks.push(n.getAttribute('data-network')); - }); - - function render_iface(ifc) { - return E('span', { class: 'cbi-tooltip-container' }, [ - E('img', { 'class' : 'middle', 'src': '<%=resource%>/icons/%s%s.png'.format( - ifc.is_alias ? 'alias' : ifc.type, - ifc.is_up ? '' : '_disabled') }), - E('span', { 'class': 'cbi-tooltip ifacebadge large' }, [ - E('img', { 'src': '<%=resource%>/icons/%s%s.png'.format( - ifc.type, ifc.is_up ? '' : '_disabled') }), - E('span', { 'class': 'left' }, [ - E('strong', '<%:Type%>: '), ifc.typename, E('br'), - E('strong', '<%:Device%>: '), ifc.ifname, E('br'), - E('strong', '<%:Connected%>: '), ifc.is_up ? '<%:yes%>' : '<%:no%>', E('br'), - ifc.macaddr ? E('strong', '<%:MAC%>: ') : '', - ifc.macaddr ? ifc.macaddr : '', - ifc.macaddr ? E('br') : '', - E('strong', '<%:RX%>: '), '%.2mB (%d <%:Pkts.%>)'.format(ifc.rx_bytes, ifc.rx_packets), E('br'), - E('strong', '<%:TX%>: '), '%.2mB (%d <%:Pkts.%>)'.format(ifc.tx_bytes, ifc.tx_packets) - ]) - ]) - ]); - } - - XHR.poll(5, '<%=url('admin/network/iface_status')%>/' + networks.join(','), null, - function(x, ifcs) - { - if (ifcs) - { - for (var idx = 0; idx < ifcs.length; idx++) - { - var ifc = ifcs[idx]; - var html = ''; - - var s = document.getElementById(ifc.id + '-ifc-devices'); - if (s) - { - while (s.firstChild) - s.removeChild(s.firstChild); - - s.appendChild(render_iface(ifc)); - - if (ifc.subdevices && ifc.subdevices.length) - { - var sifs = [ ' (' ]; - - for (var j = 0; j < ifc.subdevices.length; j++) - sifs.push(render_iface(ifc.subdevices[j])); - - sifs.push(')'); - - s.appendChild(E('span', {}, sifs)); - } - - s.appendChild(E('br')); - s.appendChild(E('small', {}, ifc.is_alias ? '<%:Alias of "%s"%>'.format(ifc.is_alias) : ifc.name)); - } - - var d = document.getElementById(ifc.id + '-ifc-description'); - if (d && ifc.proto && ifc.ifname) - { - var desc = null; - - if (ifc.is_dynamic) - desc = '<%:Virtual dynamic interface%>'; - else if (ifc.is_alias) - desc = '<%:Alias Interface%>'; - - if (ifc.desc) - desc = desc ? '%s (%s)'.format(desc, ifc.desc) : ifc.desc; - - html += String.format('<strong><%:Protocol%>:</strong> %h<br />', desc || '?'); - - if (ifc.is_up) - { - html += String.format('<strong><%:Uptime%>:</strong> %t<br />', ifc.uptime); - } - - - if (!ifc.is_dynamic && !ifc.is_alias) - { - if (ifc.macaddr) - html += String.format('<strong><%:MAC%>:</strong> %s<br />', ifc.macaddr); - - html += String.format( - '<strong><%:RX%>:</strong> %.2mB (%d <%:Pkts.%>)<br />' + - '<strong><%:TX%>:</strong> %.2mB (%d <%:Pkts.%>)<br />', - ifc.rx_bytes, ifc.rx_packets, - ifc.tx_bytes, ifc.tx_packets - ); - } - - if (ifc.ipaddrs && ifc.ipaddrs.length) - { - for (var i = 0; i < ifc.ipaddrs.length; i++) - html += String.format( - '<strong><%:IPv4%>:</strong> %s<br />', - ifc.ipaddrs[i] - ); - } - - if (ifc.ip6addrs && ifc.ip6addrs.length) - { - for (var i = 0; i < ifc.ip6addrs.length; i++) - html += String.format( - '<strong><%:IPv6%>:</strong> %s<br />', - ifc.ip6addrs[i] - ); - } - - if (ifc.ip6prefix) - html += String.format('<strong><%:IPv6-PD%>:</strong> %s<br />', ifc.ip6prefix); - - if (ifc.errors) - { - for (var i = 0; i < ifc.errors.length; i++) - html += String.format( - '<em class="error"><strong><%:Error%>:</strong> %h</em><br />', - ifc.errors[i] - ); - } - - d.innerHTML = html; - } - else if (d && !ifc.proto) - { - var e = document.getElementById(ifc.id + '-ifc-edit'); - if (e) - e.disabled = true; - - d.innerHTML = String.format( - '<em><%:Unsupported protocol type.%></em><br />' + - '<a href="%h"><%:Install protocol extensions...%></a>', - '<%=url("admin/system/packages")%>?query=luci-proto&display=available' - ); - } - else if (d && !ifc.ifname) - { - d.innerHTML = String.format( - '<em><%:Network without interfaces.%></em><br />' + - '<a href="<%=url("admin/network/network/%s")%>?tab.network.%s=physical"><%:Assign interfaces...%></a>', - ifc.name, ifc.name - ); - } - else if (d) - { - d.innerHTML = '<em><%:Interface not present or not connected yet.%></em>'; - } - } - } - } - ); -//]]></script> diff --git a/modules/luci-mod-network/luasrc/view/admin_network/wifi_overview.htm b/modules/luci-mod-network/luasrc/view/admin_network/wifi_overview.htm new file mode 100644 index 0000000000..89bb404fd8 --- /dev/null +++ b/modules/luci-mod-network/luasrc/view/admin_network/wifi_overview.htm @@ -0,0 +1,61 @@ +<div class="cbi-section-node"> + <div class="table"> + <!-- physical device --> + <div class="tr cbi-rowstyle-2"> + <div class="td col-2 center middle"> + <span class="ifacebadge"><img src="<%=resource%>/icons/wifi_disabled.png" id="<%=self.dev:name()%>-iw-upstate" /> <%=self.dev:name()%></span> + </div> + <div class="td col-7 left middle"> + <big><strong><%=self.hw%></strong></big><br /> + <span id="<%=self.dev:name()%>-iw-devinfo"></span> + </div> + <div class="td middle cbi-section-actions"> + <div> + <input type="button" class="cbi-button cbi-button-neutral" title="<%:Restart radio interface%>" value="<%:Restart%>" data-radio="<%=self.dev:name()%>" onclick="wifi_restart(event)" /> + <input type="button" class="cbi-button cbi-button-action important" title="<%:Find and join network%>" value="<%:Scan%>" onclick="cbi_submit(this, 'device', '<%=self.dev:name()%>', '<%=url('admin/network/wireless_join')%>')" /> + <input type="button" class="cbi-button cbi-button-add" title="<%:Provide new network%>" value="<%:Add%>" onclick="cbi_submit(this, 'device', '<%=self.dev:name()%>', '<%=url('admin/network/wireless_add')%>')" /> + </div> + </div> + </div> + <!-- /physical device --> + + <!-- network list --> + <% if #self.wnets > 0 then %> + <% for i, net in ipairs(self.wnets) do local disabled = (self.dev:get("disabled") == "1" or net:get("disabled") == "1") %> + <div class="tr cbi-rowstyle-<%=1 + ((i-1) % 2)%>"> + <div class="td col-2 center middle" id="<%=net:id()%>-iw-signal"> + <span class="ifacebadge" title="<%:Not associated%>"><img src="<%=resource%>/icons/signal-<%= disabled and "none" or "0" %>.png" /> 0%</span> + </div> + <div class="td col-7 left middle" id="<%=net:id()%>-iw-status" data-network="<%=net:id()%>" data-disabled="<%= disabled and "true" or "false" %>"> + <em><%= disabled and translate("Wireless is disabled") or translate("Collecting data...") %></em> + </div> + <div class="td middle cbi-section-actions"> + <div> + <% if disabled then %> + <input name="cbid.wireless.<%=net:name()%>.__disable__" type="hidden" value="1" /> + <input name="cbi.apply" type="submit" class="cbi-button cbi-button-neutral" title="<%:Enable this network%>" value="<%:Enable%>" onclick="this.previousElementSibling.value='0'" /> + <% else %> + <input name="cbid.wireless.<%=net:name()%>.__disable__" type="hidden" value="0" /> + <input name="cbi.apply" type="submit" class="cbi-button cbi-button-neutral" title="<%:Disable this network%>" value="<%:Disable%>" onclick="this.previousElementSibling.value='1'" /> + <% end %> + + <input type="button" class="cbi-button cbi-button-action important" onclick="location.href='<%=net:adminlink()%>'" title="<%:Edit this network%>" value="<%:Edit%>" /> + + <input name="cbid.wireless.<%=net:name()%>.__delete__" type="hidden" value="" /> + <input name="cbi.apply" type="submit" class="cbi-button cbi-button-negative" title="<%:Delete this network%>" value="<%:Remove%>" onclick="wifi_delete(event)" /> + </div> + </div> + </div> + <% end %> + <% else %> + <div class="tr placeholder"> + <div class="td"> + <em><%:No network configured on this device%></em> + </div> + </div> + <% end %> + <!-- /network list --> + </div> +</div> + +<script type="text/javascript" src="<%=resource%>/view/network/wireless.js"></script> diff --git a/modules/luci-mod-network/luasrc/view/admin_network/wifi_overview_status.htm b/modules/luci-mod-network/luasrc/view/admin_network/wifi_overview_status.htm deleted file mode 100644 index 9730bc2c92..0000000000 --- a/modules/luci-mod-network/luasrc/view/admin_network/wifi_overview_status.htm +++ /dev/null @@ -1,127 +0,0 @@ -<%# - Copyright 2008-2009 Steven Barth <steven@midlink.org> - Copyright 2008-2018 Jo-Philipp Wich <jo@mein.io> - Licensed to the public under the Apache License 2.0. --%> - -<script type="text/javascript">//<![CDATA[ - function wifi_delete(ev) { - if (!confirm(<%=luci.http.write_json(translate('Really delete this wireless network? The deletion cannot be undone! You might lose access to this device if you are connected via this network.'))%>)) { - ev.preventDefault(); - return false; - } - - ev.target.previousElementSibling.value = '1'; - return true; - } - - function wifi_restart(ev) { - XHR.halt(); - - findParent(ev.target, '.table').querySelectorAll('[data-disabled="false"]').forEach(function(s) { - s.innerHTML = '<em><%:Wireless is restarting...%></em>'; - }); - - (new XHR()).post('<%=url('admin/network/wireless_reconnect')%>/' + ev.target.getAttribute('data-radio'), - { token: '<%=token%>' }, XHR.run); - } - - var networks = [ ]; - - document.querySelectorAll('[data-network]').forEach(function(n) { - networks.push(n.getAttribute('data-network')); - }); - - XHR.poll(5, '<%=url('admin/network/wireless_status')%>/' + networks.join(','), null, - function(x, st) - { - if (st) - { - var rowstyle = 1; - var radiostate = { }; - - st.forEach(function(s) { - var r = radiostate[s.device.device] || (radiostate[s.device.device] = {}); - - s.is_assoc = (s.bssid && s.bssid != '00:00:00:00:00:00' && s.channel && s.mode != 'Unknown' && !s.disabled); - - r.up = r.up || s.is_assoc; - r.channel = r.channel || s.channel; - r.bitrate = r.bitrate || s.bitrate; - r.frequency = r.frequency || s.frequency; - }); - - for( var i = 0; i < st.length; i++ ) - { - var iw = st[i], - sig = document.getElementById(iw.id + '-iw-signal'), - info = document.getElementById(iw.id + '-iw-status'), - disabled = (info && info.getAttribute('data-disabled') === 'true'); - - var p = iw.quality; - var q = disabled ? -1 : p; - - var icon; - if (q < 0) - icon = "<%=resource%>/icons/signal-none.png"; - else if (q == 0) - icon = "<%=resource%>/icons/signal-0.png"; - else if (q < 25) - icon = "<%=resource%>/icons/signal-0-25.png"; - else if (q < 50) - icon = "<%=resource%>/icons/signal-25-50.png"; - else if (q < 75) - icon = "<%=resource%>/icons/signal-50-75.png"; - else - icon = "<%=resource%>/icons/signal-75-100.png"; - - - if (sig) - sig.innerHTML = String.format( - '<span class="ifacebadge" title="<%:Signal%>: %d <%:dBm%> / <%:Noise%>: %d <%:dBm%>"><img src="%s" /> %d%%</span>', - iw.signal, iw.noise, icon, p - ); - - if (info) - { - if (iw.is_assoc) - info.innerHTML = String.format( - '<strong><%:SSID%>:</strong> %h | ' + - '<strong><%:Mode%>:</strong> %s<br />' + - '<strong><%:BSSID%>:</strong> %s | ' + - '<strong><%:Encryption%>:</strong> %s', - iw.ssid, iw.mode, iw.bssid, - iw.encryption ? iw.encryption : '<%:None%>' - ); - else - info.innerHTML = String.format( - '<strong><%:SSID%>:</strong> %h | ' + - '<strong><%:Mode%>:</strong> %s<br />' + - '<em>%s</em>', - iw.ssid || '?', iw.mode, - disabled ? '<em><%:Wireless is disabled%></em>' - : '<em><%:Wireless is not associated%></em>' - ); - } - } - - for (var dev in radiostate) - { - var img = document.getElementById(dev + '-iw-upstate'); - if (img) - img.src = '<%=resource%>/icons/wifi' + (radiostate[dev].up ? '' : '_disabled') + '.png'; - - var stat = document.getElementById(dev + '-iw-devinfo'); - if (stat) - stat.innerHTML = String.format( - '<strong><%:Channel%>:</strong> %s (%s <%:GHz%>) | ' + - '<strong><%:Bitrate%>:</strong> %s <%:Mbit/s%>', - radiostate[dev].channel ? radiostate[dev].channel : '?', - radiostate[dev].frequency ? radiostate[dev].frequency : '?', - radiostate[dev].bitrate ? radiostate[dev].bitrate : '?' - ); - } - } - } - ); -//]]></script> diff --git a/modules/luci-mod-status/htdocs/luci-static/resources/view/status/index.js b/modules/luci-mod-status/htdocs/luci-static/resources/view/status/index.js new file mode 100644 index 0000000000..c2aa3a9b0d --- /dev/null +++ b/modules/luci-mod-status/htdocs/luci-static/resources/view/status/index.js @@ -0,0 +1,215 @@ +function progressbar(q, v, m) +{ + var pg = document.querySelector(q), + vn = parseInt(v) || 0, + mn = parseInt(m) || 100, + pc = Math.floor((100 / mn) * vn); + + if (pg) { + pg.firstElementChild.style.width = pc + '%'; + pg.setAttribute('title', '%s / %s (%d%%)'.format(v, m, pc)); + } +} + +function renderBox(title, active, childs) { + childs = childs || []; + childs.unshift(L.itemlist(E('span'), [].slice.call(arguments, 3))); + + return E('div', { class: 'ifacebox' }, [ + E('div', { class: 'ifacebox-head center ' + (active ? 'active' : '') }, + E('strong', title)), + E('div', { class: 'ifacebox-body left' }, childs) + ]); +} + +function renderBadge(icon, title) { + return E('span', { class: 'ifacebadge' }, [ + E('img', { src: icon, title: title || '' }), + L.itemlist(E('span'), [].slice.call(arguments, 2)) + ]); +} + +L.poll(5, L.location(), { status: 1 }, + function(x, info) + { + var us = document.getElementById('upstream_status_table'); + + while (us.lastElementChild) + us.removeChild(us.lastElementChild); + + var wan_list = info.wan || []; + + for (var i = 0; i < wan_list.length; i++) { + var ifc = wan_list[i]; + + us.appendChild(renderBox( + _('IPv4 Upstream'), + (ifc.ifname && ifc.proto != 'none'), + [ E('div', {}, renderBadge( + L.resource('icons/%s.png').format((ifc && ifc.type) ? ifc.type : 'ethernet_disabled'), null, + _('Device'), ifc ? (ifc.name || ifc.ifname || '-') : '-', + _('MAC-Address'), (ifc && ifc.ether) ? ifc.mac : null)) ], + _('Protocol'), ifc.i18n || E('em', _('Not connected')), + _('Address'), (ifc.ipaddr) ? ifc.ipaddr : '0.0.0.0', + _('Netmask'), (ifc.netmask && ifc.netmask != ifc.ipaddr) ? ifc.netmask : '255.255.255.255', + _('Gateway'), (ifc.gwaddr) ? ifc.gwaddr : '0.0.0.0', + _('DNS') + ' 1', (ifc.dns) ? ifc.dns[0] : null, + _('DNS') + ' 2', (ifc.dns) ? ifc.dns[1] : null, + _('DNS') + ' 3', (ifc.dns) ? ifc.dns[2] : null, + _('DNS') + ' 4', (ifc.dns) ? ifc.dns[3] : null, + _('DNS') + ' 5', (ifc.dns) ? ifc.dns[4] : null, + _('Expires'), (ifc.expires > -1) ? '%t'.format(ifc.expires) : null, + _('Connected'), (ifc.uptime > 0) ? '%t'.format(ifc.uptime) : null)); + } + + var wan6_list = info.wan6 || []; + + for (var i = 0; i < wan6_list.length; i++) { + var ifc6 = wan6_list[i]; + + us.appendChild(renderBox( + _('IPv6 Upstream'), + (ifc6.ifname && ifc6.proto != 'none'), + [ E('div', {}, renderBadge( + L.resource('icons/%s.png').format(ifc6.type || 'ethernet_disabled'), null, + _('Device'), ifc6 ? (ifc6.name || ifc6.ifname || '-') : '-', + _('MAC-Address'), (ifc6 && ifc6.ether) ? ifc6.mac : null)) ], + _('Protocol'), ifc6.i18n ? (ifc6.i18n + (ifc6.proto === 'dhcp' && ifc6.ip6prefix ? '-PD' : '')) : E('em', _('Not connected')), + _('Prefix Delegated'), ifc6.ip6prefix, + _('Address'), (ifc6.ip6prefix) ? (ifc6.ip6addr || null) : (ifc6.ip6addr || '::'), + _('Gateway'), (ifc6.gw6addr) ? ifc6.gw6addr : '::', + _('DNS') + ' 1', (ifc6.dns) ? ifc6.dns[0] : null, + _('DNS') + ' 2', (ifc6.dns) ? ifc6.dns[1] : null, + _('DNS') + ' 3', (ifc6.dns) ? ifc6.dns[2] : null, + _('DNS') + ' 4', (ifc6.dns) ? ifc6.dns[3] : null, + _('DNS') + ' 5', (ifc6.dns) ? ifc6.dns[4] : null, + _('Connected'), (ifc6.uptime > 0) ? '%t'.format(ifc6.uptime) : null)); + } + + var ds = document.getElementById('dsl_status_table'); + if (ds) { + while (ds.lastElementChild) + ds.removeChild(ds.lastElementChild); + + ds.appendChild(renderBox( + _('DSL Status'), + (info.dsl.line_state === 'UP'), [ ], + _('Line State'), '%s [0x%x]'.format(info.dsl.line_state, info.dsl.line_state_detail), + _('Line Mode'), info.dsl.line_mode_s || '-', + _('Line Uptime'), info.dsl.line_uptime_s || '-', + _('Annex'), info.dsl.annex_s || '-', + _('Profile'), info.dsl.profile_s || '-', + _('Data Rate'), '%s/s / %s/s'.format(info.dsl.data_rate_down_s, info.dsl.data_rate_up_s), + _('Max. Attainable Data Rate (ATTNDR)'), '%s/s / %s/s'.format(info.dsl.max_data_rate_down_s, info.dsl.max_data_rate_up_s), + _('Latency'), '%s / %s'.format(info.dsl.latency_num_down, info.dsl.latency_num_up), + _('Line Attenuation (LATN)'), '%.1f dB / %.1f dB'.format(info.dsl.line_attenuation_down, info.dsl.line_attenuation_up), + _('Signal Attenuation (SATN)'), '%.1f dB / %.1f dB'.format(info.dsl.signal_attenuation_down, info.dsl.signal_attenuation_up), + _('Noise Margin (SNR)'), '%.1f dB / %.1f dB'.format(info.dsl.noise_margin_down, info.dsl.noise_margin_up), + _('Aggregate Transmit Power(ACTATP)'), '%.1f dB / %.1f dB'.format(info.dsl.actatp_down, info.dsl.actatp_up), + _('Forward Error Correction Seconds (FECS)'), '%d / %d'.format(info.dsl.errors_fec_near, info.dsl.errors_fec_far), + _('Errored seconds (ES)'), '%d / %d'.format(info.dsl.errors_es_near, info.dsl.errors_es_far), + _('Severely Errored Seconds (SES)'), '%d / %d'.format(info.dsl.errors_ses_near, info.dsl.errors_ses_far), + _('Loss of Signal Seconds (LOSS)'), '%d / %d'.format(info.dsl.errors_loss_near, info.dsl.errors_loss_far), + _('Unavailable Seconds (UAS)'), '%d / %d'.format(info.dsl.errors_uas_near, info.dsl.errors_uas_far), + _('Header Error Code Errors (HEC)'), '%d / %d'.format(info.dsl.errors_hec_near, info.dsl.errors_hec_far), + _('Non Pre-emtive CRC errors (CRC_P)'), '%d / %d'.format(info.dsl.errors_crc_p_near, info.dsl.errors_crc_p_far), + _('Pre-emtive CRC errors (CRCP_P)'), '%d / %d'.format(info.dsl.errors_crcp_p_near, info.dsl.errors_crcp_p_far), + _('ATU-C System Vendor ID'), info.dsl.atuc_vendor_id, + _('Power Management Mode'), info.dsl.power_mode_s)); + } + + var ws = document.getElementById('wifi_status_table'); + if (ws) + { + while (ws.lastElementChild) + ws.removeChild(ws.lastElementChild); + + for (var didx = 0; didx < info.wifinets.length; didx++) + { + var dev = info.wifinets[didx]; + var net0 = (dev.networks && dev.networks[0]) ? dev.networks[0] : {}; + var vifs = []; + + for (var nidx = 0; nidx < dev.networks.length; nidx++) + { + var net = dev.networks[nidx]; + var is_assoc = (net.bssid != '00:00:00:00:00:00' && net.channel && !net.disabled); + + var icon; + if (net.disabled) + icon = L.resource('icons/signal-none.png'); + else if (net.quality <= 0) + icon = L.resource('icons/signal-0.png'); + else if (net.quality < 25) + icon = L.resource('icons/signal-0-25.png'); + else if (net.quality < 50) + icon = L.resource('icons/signal-25-50.png'); + else if (net.quality < 75) + icon = L.resource('icons/signal-50-75.png'); + else + icon = L.resource('icons/signal-75-100.png'); + + vifs.push(renderBadge( + icon, + '%s: %d dBm / %s: %d%%'.format(_('Signal'), net.signal, _('Quality'), net.quality), + _('SSID'), E('a', { href: net.link }, [ net.ssid || '?' ]), + _('Mode'), net.mode, + _('BSSID'), is_assoc ? (net.bssid || '-') : null, + _('Encryption'), is_assoc ? net.encryption : null, + _('Associations'), is_assoc ? (net.num_assoc || '-') : null, + null, is_assoc ? null : E('em', net.disabled ? _('Wireless is disabled') : _('Wireless is not associated')))); + } + + ws.appendChild(renderBox( + dev.device, dev.up || net0.up, + [ E('div', vifs) ], + _('Type'), dev.name.replace(/^Generic | Wireless Controller .+$/g, ''), + _('Channel'), net0.channel ? '%d (%.3f %s)'.format(net0.channel, net0.frequency, _('GHz')) : '-', + _('Bitrate'), net0.bitrate ? '%d %s'.format(net0.bitrate, _('Mbit/s')) : '-')); + } + + if (!ws.lastElementChild) + ws.appendChild(E('em', _('No information available'))); + } + + var e; + + if (e = document.getElementById('localtime')) + e.innerHTML = info.localtime; + + if (e = document.getElementById('uptime')) + e.innerHTML = String.format('%t', info.uptime); + + if (e = document.getElementById('loadavg')) + e.innerHTML = String.format( + '%.02f, %.02f, %.02f', + info.loadavg[0] / 65535.0, + info.loadavg[1] / 65535.0, + info.loadavg[2] / 65535.0 + ); + + progressbar('#memtotal', + ((info.memory.free + info.memory.buffered) / 1024) + ' ' + _('kB'), + (info.memory.total / 1024) + ' ' + _('kB')); + + progressbar('#memfree', + (info.memory.free / 1024) + ' ' + _('kB'), + (info.memory.total / 1024) + ' ' + _('kB')); + + progressbar('#membuff', + (info.memory.buffered / 1024) + ' ' + _('kB'), + (info.memory.total / 1024) + ' ' + _('kB')); + + progressbar('#swaptotal', + (info.swap.free / 1024) + ' ' + _('kB'), + (info.swap.total / 1024) + ' ' + _('kB')); + + progressbar('#swapfree', + (info.swap.free / 1024) + ' ' + _('kB'), + (info.swap.total / 1024) + ' ' + _('kB')); + + progressbar('#conns', + info.conncount, info.connmax); + + } +); diff --git a/modules/luci-mod-status/htdocs/luci-static/resources/view/status/iptables.js b/modules/luci-mod-status/htdocs/luci-static/resources/view/status/iptables.js new file mode 100644 index 0000000000..39ddab3979 --- /dev/null +++ b/modules/luci-mod-status/htdocs/luci-static/resources/view/status/iptables.js @@ -0,0 +1,253 @@ +var table_names = [ 'Filter', 'NAT', 'Mangle', 'Raw' ], + current_mode = document.querySelector('.cbi-tab[data-mode="6"]') ? 6 : 4; + +function create_table_section(table) +{ + var idiv = document.getElementById('iptables'), + tdiv = idiv.querySelector('[data-table="%s"]'.format(table)), + title = '%s: %s'.format(_('Table'), table); + + if (!tdiv) { + tdiv = E('div', { 'data-table': table }, [ + E('h3', {}, title), + E('div') + ]); + + if (idiv.firstElementChild.nodeName.toLowerCase() === 'p') + idiv.removeChild(idiv.firstElementChild); + + var added = false, thisIdx = table_names.indexOf(table); + + idiv.querySelectorAll('[data-table]').forEach(function(child) { + var childIdx = table_names.indexOf(child.getAttribute('data-table')); + + if (added === false && childIdx > thisIdx) { + idiv.insertBefore(tdiv, child); + added = true; + } + }); + + if (added === false) + idiv.appendChild(tdiv); + } + + return tdiv.lastElementChild; +} + +function create_chain_section(table, chain, policy, packets, bytes, references) +{ + var tdiv = create_table_section(table), + cdiv = tdiv.querySelector('[data-chain="%s"]'.format(chain)), + title; + + if (policy) + title = '%s <em>%s</em> <span>(%s: <em>%s</em>, %d %s, %.2mB %s)</span>' + .format(_('Chain'), chain, _('Policy'), policy, packets, _('Packets'), bytes, _('Traffic')); + else + title = '%s <em>%s</em> <span class="references">(%d %s)</span>' + .format(_('Chain'), chain, references, _('References')); + + if (!cdiv) { + cdiv = E('div', { 'data-chain': chain }, [ + E('h4', { 'id': 'rule_%s_%s'.format(table.toLowerCase(), chain) }, title), + E('div', { 'class': 'table' }, [ + E('div', { 'class': 'tr table-titles' }, [ + E('div', { 'class': 'th center' }, _('Pkts.')), + E('div', { 'class': 'th center' }, _('Traffic')), + E('div', { 'class': 'th' }, _('Target')), + E('div', { 'class': 'th' }, _('Prot.')), + E('div', { 'class': 'th' }, _('In')), + E('div', { 'class': 'th' }, _('Out')), + E('div', { 'class': 'th' }, _('Source')), + E('div', { 'class': 'th' }, _('Destination')), + E('div', { 'class': 'th' }, _('Options')), + E('div', { 'class': 'th' }, _('Comment')) + ]) + ]) + ]); + + tdiv.appendChild(cdiv); + } + else { + cdiv.firstElementChild.innerHTML = title; + } + + return cdiv.lastElementChild; +} + +function update_chain_section(chaintable, rows) +{ + if (!chaintable) + return; + + cbi_update_table(chaintable, rows, _('No rules in this chain.')); + + if (rows.length === 0 && + document.querySelector('form > [data-hide-empty="true"]')) + chaintable.parentNode.style.display = 'none'; + else + chaintable.parentNode.style.display = ''; + + chaintable.parentNode.setAttribute('data-empty', rows.length === 0); +} + +function hide_empty(btn) +{ + var hide = (btn.getAttribute('data-hide-empty') === 'false'); + + btn.setAttribute('data-hide-empty', hide); + btn.value = hide ? _('Show empty chains') : _('Hide empty chains'); + btn.blur(); + + document.querySelectorAll('[data-chain][data-empty="true"]') + .forEach(function(chaintable) { + chaintable.style.display = hide ? 'none' : ''; + }); +} + +function jump_target(ev) +{ + var link = ev.target, + table = findParent(link, '[data-table]').getAttribute('data-table'), + chain = link.textContent, + num = +link.getAttribute('data-num'), + elem = document.getElementById('rule_%s_%s'.format(table.toLowerCase(), chain)); + + if (elem) { + (document.documentElement || document.body.parentNode || document.body).scrollTop = elem.offsetTop - 40; + elem.classList.remove('flash'); + void elem.offsetWidth; + elem.classList.add('flash'); + + if (num) { + var rule = elem.nextElementSibling.childNodes[num]; + if (rule) { + rule.classList.remove('flash'); + void rule.offsetWidth; + rule.classList.add('flash'); + } + } + } +} + +function parse_output(table, s) +{ + var current_chain = null; + var current_rules = []; + var seen_chains = {}; + var chain_refs = {}; + var re = /([^\n]*)\n/g; + var m, m2; + + while ((m = re.exec(s)) != null) { + if (m[1].match(/^Chain (.+) \(policy (\w+) (\d+) packets, (\d+) bytes\)$/)) { + var chain = RegExp.$1, + policy = RegExp.$2, + packets = +RegExp.$3, + bytes = +RegExp.$4; + + update_chain_section(current_chain, current_rules); + + seen_chains[chain] = true; + current_chain = create_chain_section(table, chain, policy, packets, bytes); + current_rules = []; + } + else if (m[1].match(/^Chain (.+) \((\d+) references\)$/)) { + var chain = RegExp.$1, + references = +RegExp.$2; + + update_chain_section(current_chain, current_rules); + + seen_chains[chain] = true; + current_chain = create_chain_section(table, chain, null, null, null, references); + current_rules = []; + } + else if (m[1].match(/^num /)) { + continue; + } + else if ((m2 = m[1].match(/^(\d+) +(\d+) +(\d+) +(.*?) +(\S+) +(\S*) +(\S+) +(\S+) +([a-f0-9:.]+\/\d+) +([a-f0-9:.]+\/\d+) +(.+)$/)) !== null) { + var num = +m2[1], + pkts = +m2[2], + bytes = +m2[3], + target = m2[4], + proto = m2[5], + indev = m2[7], + outdev = m2[8], + srcnet = m2[9], + dstnet = m2[10], + options = m2[11] || '-', + comment = '-'; + + options = options.trim().replace(/(?:^| )\/\* (.+) \*\//, + function(m1, m2) { + comment = m2.replace(/^!fw3(: |$)/, '').trim() || '-'; + return ''; + }) || '-'; + + current_rules.push([ + '%.2m'.format(pkts).nobr(), + '%.2mB'.format(bytes).nobr(), + target ? '<span class="target">%s</span>'.format(target) : '-', + proto, + (indev !== '*') ? '<span class="ifacebadge">%s</span>'.format(indev) : '*', + (outdev !== '*') ? '<span class="ifacebadge">%s</span>'.format(outdev) : '*', + srcnet, + dstnet, + options, + comment + ]); + + if (target) { + chain_refs[target] = chain_refs[target] || []; + chain_refs[target].push([ current_chain, num ]); + } + } + } + + update_chain_section(current_chain, current_rules); + + document.querySelectorAll('[data-table="%s"] [data-chain]'.format(table)) + .forEach(function(cdiv) { + if (!seen_chains[cdiv.getAttribute('data-chain')]) { + cdiv.parentNode.removeChild(cdiv); + return; + } + + cdiv.querySelectorAll('.target').forEach(function(tspan) { + if (seen_chains[tspan.textContent]) { + tspan.classList.add('jump'); + tspan.addEventListener('click', jump_target); + } + }); + + cdiv.querySelectorAll('.references').forEach(function(rspan) { + var refs = chain_refs[cdiv.getAttribute('data-chain')]; + if (refs && refs.length) { + rspan.classList.add('cbi-tooltip-container'); + rspan.appendChild(E('small', { 'class': 'cbi-tooltip ifacebadge', 'style': 'top:1em; left:auto' }, [ E('ul') ])); + + refs.forEach(function(ref) { + var chain = ref[0].parentNode.getAttribute('data-chain'), + num = ref[1]; + + rspan.lastElementChild.lastElementChild.appendChild(E('li', {}, [ + _('Chain'), ' ', + E('span', { + 'class': 'jump', + 'data-num': num, + 'onclick': 'jump_target(event)' + }, chain), + ', %s #%d'.format(_('Rule'), num) + ])); + }); + } + }); + }); +} + +table_names.forEach(function(table) { + L.poll(5, L.url('admin/status/iptables_dump', current_mode, table.toLowerCase()), null, + function (xhr) { + parse_output(table, xhr.responseText); + }); +}); diff --git a/modules/luci-mod-status/luasrc/view/admin_status/index.htm b/modules/luci-mod-status/luasrc/view/admin_status/index.htm index b98ba76f89..b11956a8af 100644 --- a/modules/luci-mod-status/luasrc/view/admin_status/index.htm +++ b/modules/luci-mod-status/luasrc/view/admin_status/index.htm @@ -1,6 +1,6 @@ <%# Copyright 2008 Steven Barth <steven@midlink.org> - Copyright 2008-2011 Jo-Philipp Wich <jow@openwrt.org> + Copyright 2008-2018 Jo-Philipp Wich <jo@mein.io> Licensed to the public under the Apache License 2.0. -%> @@ -131,248 +131,6 @@ <%+header%> -<script type="text/javascript">//<![CDATA[ - function progressbar(q, v, m) - { - var pg = document.querySelector(q), - vn = parseInt(v) || 0, - mn = parseInt(m) || 100, - pc = Math.floor((100 / mn) * vn); - - if (pg) { - pg.firstElementChild.style.width = pc + '%'; - pg.setAttribute('title', '%s / %s (%d%%)'.format(v, m, pc)); - } - } - - function labelList(items, offset) { - var rv = [ ]; - - for (var i = offset || 0; i < items.length; i += 2) { - var label = items[i], - value = items[i+1]; - - if (value === undefined || value === null) - continue; - - if (label) - rv.push(E('strong', [label, ': '])); - - rv.push(value, E('br')); - } - - return rv; - } - - function renderBox(title, active, childs) { - childs = childs || []; - childs.unshift(E('span', labelList(arguments, 3))); - - return E('div', { class: 'ifacebox' }, [ - E('div', { class: 'ifacebox-head center ' + (active ? 'active' : '') }, - E('strong', title)), - E('div', { class: 'ifacebox-body left' }, childs) - ]); - } - - function renderBadge(icon, title) { - return E('span', { class: 'ifacebadge' }, [ - E('img', { src: icon, title: title || '' }), - E('span', labelList(arguments, 2)) - ]); - } - - XHR.poll(5, '<%=REQUEST_URI%>', { status: 1 }, - function(x, info) - { - var us = document.getElementById('upstream_status_table'); - - while (us.lastElementChild) - us.removeChild(us.lastElementChild); - - var wan_list = info.wan || []; - - for (var i = 0; i < wan_list.length; i++) { - var ifc = wan_list[i]; - - us.appendChild(renderBox( - '<%:IPv4 Upstream%>', - (ifc.ifname && ifc.proto != 'none'), - [ E('div', {}, renderBadge( - '<%=resource%>' + '/icons/%s.png'.format((ifc && ifc.type) ? ifc.type : 'ethernet_disabled'), null, - '<%:Device%>', ifc ? (ifc.name || ifc.ifname || '-') : '-', - '<%:MAC-Address%>', (ifc && ifc.ether) ? ifc.mac : null)) ], - '<%:Protocol%>', ifc.i18n || E('em', '<%:Not connected%>'), - '<%:Address%>', (ifc.ipaddr) ? ifc.ipaddr : '0.0.0.0', - '<%:Netmask%>', (ifc.netmask && ifc.netmask != ifc.ipaddr) ? ifc.netmask : '255.255.255.255', - '<%:Gateway%>', (ifc.gwaddr) ? ifc.gwaddr : '0.0.0.0', - '<%:DNS%> 1', (ifc.dns) ? ifc.dns[0] : null, - '<%:DNS%> 2', (ifc.dns) ? ifc.dns[1] : null, - '<%:DNS%> 3', (ifc.dns) ? ifc.dns[2] : null, - '<%:DNS%> 4', (ifc.dns) ? ifc.dns[3] : null, - '<%:DNS%> 5', (ifc.dns) ? ifc.dns[4] : null, - '<%:Expires%>', (ifc.expires > -1) ? '%t'.format(ifc.expires) : null, - '<%:Connected%>', (ifc.uptime > 0) ? '%t'.format(ifc.uptime) : null)); - } - - <% if has_ipv6 then %> - var wan6_list = info.wan6 || []; - - for (var i = 0; i < wan6_list.length; i++) { - var ifc6 = wan6_list[i]; - - us.appendChild(renderBox( - '<%:IPv6 Upstream%>', - (ifc6.ifname && ifc6.proto != 'none'), - [ E('div', {}, renderBadge( - '<%=resource%>/icons/%s.png'.format(ifc6.type || 'ethernet_disabled'), null, - '<%:Device%>', ifc6 ? (ifc6.name || ifc6.ifname || '-') : '-', - '<%:MAC-Address%>', (ifc6 && ifc6.ether) ? ifc6.mac : null)) ], - '<%:Protocol%>', ifc6.i18n ? (ifc6.i18n + (ifc6.proto === 'dhcp' && ifc6.ip6prefix ? '-PD' : '')) : E('em', '<%:Not connected%>'), - '<%:Prefix Delegated%>', ifc6.ip6prefix, - '<%:Address%>', (ifc6.ip6prefix) ? (ifc6.ip6addr || null) : (ifc6.ip6addr || '::'), - '<%:Gateway%>', (ifc6.gw6addr) ? ifc6.gw6addr : '::', - '<%:DNS%> 1', (ifc6.dns) ? ifc6.dns[0] : null, - '<%:DNS%> 2', (ifc6.dns) ? ifc6.dns[1] : null, - '<%:DNS%> 3', (ifc6.dns) ? ifc6.dns[2] : null, - '<%:DNS%> 4', (ifc6.dns) ? ifc6.dns[3] : null, - '<%:DNS%> 5', (ifc6.dns) ? ifc6.dns[4] : null, - '<%:Connected%>', (ifc6.uptime > 0) ? '%t'.format(ifc6.uptime) : null)); - } - <% end %> - - <% if has_dsl then %> - var ds = document.getElementById('dsl_status_table'); - - while (ds.lastElementChild) - ds.removeChild(ds.lastElementChild); - - ds.appendChild(renderBox( - '<%:DSL Status%>', - (info.dsl.line_state === 'UP'), [ ], - '<%:Line State%>', '%s [0x%x]'.format(info.dsl.line_state, info.dsl.line_state_detail), - '<%:Line Mode%>', info.dsl.line_mode_s || '-', - '<%:Line Uptime%>', info.dsl.line_uptime_s || '-', - '<%:Annex%>', info.dsl.annex_s || '-', - '<%:Profile%>', info.dsl.profile_s || '-', - '<%:Data Rate%>', '%s/s / %s/s'.format(info.dsl.data_rate_down_s, info.dsl.data_rate_up_s), - '<%:Max. Attainable Data Rate (ATTNDR)%>', '%s/s / %s/s'.format(info.dsl.max_data_rate_down_s, info.dsl.max_data_rate_up_s), - '<%:Latency%>', '%s / %s'.format(info.dsl.latency_num_down, info.dsl.latency_num_up), - '<%:Line Attenuation (LATN)%>', '%.1f dB / %.1f dB'.format(info.dsl.line_attenuation_down, info.dsl.line_attenuation_up), - '<%:Signal Attenuation (SATN)%>', '%.1f dB / %.1f dB'.format(info.dsl.signal_attenuation_down, info.dsl.signal_attenuation_up), - '<%:Noise Margin (SNR)%>', '%.1f dB / %.1f dB'.format(info.dsl.noise_margin_down, info.dsl.noise_margin_up), - '<%:Aggregate Transmit Power(ACTATP)%>', '%.1f dB / %.1f dB'.format(info.dsl.actatp_down, info.dsl.actatp_up), - '<%:Forward Error Correction Seconds (FECS)%>', '%d / %d'.format(info.dsl.errors_fec_near, info.dsl.errors_fec_far), - '<%:Errored seconds (ES)%>', '%d / %d'.format(info.dsl.errors_es_near, info.dsl.errors_es_far), - '<%:Severely Errored Seconds (SES)%>', '%d / %d'.format(info.dsl.errors_ses_near, info.dsl.errors_ses_far), - '<%:Loss of Signal Seconds (LOSS)%>', '%d / %d'.format(info.dsl.errors_loss_near, info.dsl.errors_loss_far), - '<%:Unavailable Seconds (UAS)%>', '%d / %d'.format(info.dsl.errors_uas_near, info.dsl.errors_uas_far), - '<%:Header Error Code Errors (HEC)%>', '%d / %d'.format(info.dsl.errors_hec_near, info.dsl.errors_hec_far), - '<%:Non Pre-emtive CRC errors (CRC_P)%>', '%d / %d'.format(info.dsl.errors_crc_p_near, info.dsl.errors_crc_p_far), - '<%:Pre-emtive CRC errors (CRCP_P)%>', '%d / %d'.format(info.dsl.errors_crcp_p_near, info.dsl.errors_crcp_p_far), - '<%:ATU-C System Vendor ID%>', info.dsl.atuc_vendor_id, - '<%:Power Management Mode%>', info.dsl.power_mode_s)); - <% end %> - - <% if has_wifi then %> - var ws = document.getElementById('wifi_status_table'); - if (ws) - { - while (ws.lastElementChild) - ws.removeChild(ws.lastElementChild); - - for (var didx = 0; didx < info.wifinets.length; didx++) - { - var dev = info.wifinets[didx]; - var net0 = (dev.networks && dev.networks[0]) ? dev.networks[0] : {}; - var vifs = []; - - for (var nidx = 0; nidx < dev.networks.length; nidx++) - { - var net = dev.networks[nidx]; - var is_assoc = (net.bssid != '00:00:00:00:00:00' && net.channel && !net.disabled); - - var icon; - if (net.disabled) - icon = "<%=resource%>/icons/signal-none.png"; - else if (net.quality <= 0) - icon = "<%=resource%>/icons/signal-0.png"; - else if (net.quality < 25) - icon = "<%=resource%>/icons/signal-0-25.png"; - else if (net.quality < 50) - icon = "<%=resource%>/icons/signal-25-50.png"; - else if (net.quality < 75) - icon = "<%=resource%>/icons/signal-50-75.png"; - else - icon = "<%=resource%>/icons/signal-75-100.png"; - - vifs.push(renderBadge( - icon, - '<%:Signal%>: %d dBm / <%:Quality%>: %d%%'.format(net.signal, net.quality), - '<%:SSID%>', E('a', { href: net.link }, [ net.ssid || '?' ]), - '<%:Mode%>', net.mode, - '<%:BSSID%>', is_assoc ? (net.bssid || '-') : null, - '<%:Encryption%>', is_assoc ? net.encryption : null, - '<%:Associations%>', is_assoc ? (net.num_assoc || '-') : null, - null, is_assoc ? null : E('em', net.disabled ? '<%:Wireless is disabled%>' : '<%:Wireless is not associated%>'))); - } - - ws.appendChild(renderBox( - dev.device, dev.up || net0.up, - [ E('div', vifs) ], - '<%:Type%>', dev.name.replace(/^Generic | Wireless Controller .+$/g, ''), - '<%:Channel%>', net0.channel ? '%d (%.3f <%:GHz%>)'.format(net0.channel, net0.frequency) : '-', - '<%:Bitrate%>', net0.bitrate ? '%d <%:Mbit/s%>'.format(net0.bitrate) : '-')); - } - - if (!ws.lastElementChild) - ws.appendChild(E('<em><%:No information available%></em>')); - } - <% end %> - - var e; - - if (e = document.getElementById('localtime')) - e.innerHTML = info.localtime; - - if (e = document.getElementById('uptime')) - e.innerHTML = String.format('%t', info.uptime); - - if (e = document.getElementById('loadavg')) - e.innerHTML = String.format( - '%.02f, %.02f, %.02f', - info.loadavg[0] / 65535.0, - info.loadavg[1] / 65535.0, - info.loadavg[2] / 65535.0 - ); - - progressbar('#memtotal', - ((info.memory.free + info.memory.buffered) / 1024) + " <%:kB%>", - (info.memory.total / 1024) + " <%:kB%>"); - - progressbar('#memfree', - (info.memory.free / 1024) + " <%:kB%>", - (info.memory.total / 1024) + " <%:kB%>"); - - progressbar('#membuff', - (info.memory.buffered / 1024) + " <%:kB%>", - (info.memory.total / 1024) + " <%:kB%>"); - - progressbar('#swaptotal', - (info.swap.free / 1024) + " <%:kB%>", - (info.swap.total / 1024) + " <%:kB%>"); - - progressbar('#swapfree', - (info.swap.free / 1024) + " <%:kB%>", - (info.swap.total / 1024) + " <%:kB%>"); - - progressbar('#conns', - info.conncount, info.connmax); - - } - ); -//]]></script> - <h2 name="content"><%:Status%></h2> <div class="cbi-section"> @@ -470,4 +228,6 @@ end -%> +<script type="text/javascript" src="<%=resource%>/view/status/index.js"></script> + <%+footer%> diff --git a/modules/luci-mod-status/luasrc/view/admin_status/iptables.htm b/modules/luci-mod-status/luasrc/view/admin_status/iptables.htm index 50defac90e..89f229f3ba 100644 --- a/modules/luci-mod-status/luasrc/view/admin_status/iptables.htm +++ b/modules/luci-mod-status/luasrc/view/admin_status/iptables.htm @@ -41,265 +41,16 @@ } </style> -<script type="text/javascript">//<![CDATA[ - var table_names = [ 'Filter', 'NAT', 'Mangle', 'Raw' ]; - - function create_table_section(table) - { - var idiv = document.getElementById('iptables'), - tdiv = idiv.querySelector('[data-table="%s"]'.format(table)), - title = '<%:Table%>: %s'.format(table); - - if (!tdiv) { - tdiv = E('div', { 'data-table': table }, [ - E('h3', {}, title), - E('div') - ]); - - if (idiv.firstElementChild.nodeName.toLowerCase() === 'p') - idiv.removeChild(idiv.firstElementChild); - - var added = false, thisIdx = table_names.indexOf(table); - - idiv.querySelectorAll('[data-table]').forEach(function(child) { - var childIdx = table_names.indexOf(child.getAttribute('data-table')); - - if (added === false && childIdx > thisIdx) { - idiv.insertBefore(tdiv, child); - added = true; - } - }); - - if (added === false) - idiv.appendChild(tdiv); - } - - return tdiv.lastElementChild; - } - - function create_chain_section(table, chain, policy, packets, bytes, references) - { - var tdiv = create_table_section(table), - cdiv = tdiv.querySelector('[data-chain="%s"]'.format(chain)), - title; - - if (policy) - title = '<%:Chain%> <em>%s</em> <span>(<%:Policy%>: <em>%s</em>, %d <%:Packets%>, %.2mB <%:Traffic%>)</span>'.format(chain, policy, packets, bytes); - else - title = '<%:Chain%> <em>%s</em> <span class="references">(%d <%:References%>)</span>'.format(chain, references); - - if (!cdiv) { - cdiv = E('div', { 'data-chain': chain }, [ - E('h4', { 'id': 'rule_%s_%s'.format(table.toLowerCase(), chain) }, title), - E('div', { 'class': 'table' }, [ - E('div', { 'class': 'tr table-titles' }, [ - E('div', { 'class': 'th center' }, '<%:Pkts.%>'), - E('div', { 'class': 'th center' }, '<%:Traffic%>'), - E('div', { 'class': 'th' }, '<%:Target%>'), - E('div', { 'class': 'th' }, '<%:Prot.%>'), - E('div', { 'class': 'th' }, '<%:In%>'), - E('div', { 'class': 'th' }, '<%:Out%>'), - E('div', { 'class': 'th' }, '<%:Source%>'), - E('div', { 'class': 'th' }, '<%:Destination%>'), - E('div', { 'class': 'th' }, '<%:Options%>'), - E('div', { 'class': 'th' }, '<%:Comment%>') - ]) - ]) - ]); - - tdiv.appendChild(cdiv); - } - else { - cdiv.firstElementChild.innerHTML = title; - } - - return cdiv.lastElementChild; - } - - function update_chain_section(chaintable, rows) - { - if (!chaintable) - return; - - cbi_update_table(chaintable, rows, '<%:No rules in this chain.%>'); - - if (rows.length === 0 && - document.querySelector('form > [data-hide-empty="true"]')) - chaintable.parentNode.style.display = 'none'; - else - chaintable.parentNode.style.display = ''; - - chaintable.parentNode.setAttribute('data-empty', rows.length === 0); - } - - function hide_empty(btn) - { - var hide = (btn.getAttribute('data-hide-empty') === 'false'); - - btn.setAttribute('data-hide-empty', hide); - btn.value = hide ? '<%:Show empty chains%>' : '<%:Hide empty chains%>'; - btn.blur(); - - document.querySelectorAll('[data-chain][data-empty="true"]') - .forEach(function(chaintable) { - chaintable.style.display = hide ? 'none' : ''; - }); - } - - function jump_target(ev) - { - var link = ev.target, - table = findParent(link, '[data-table]').getAttribute('data-table'), - chain = link.textContent, - num = +link.getAttribute('data-num'), - elem = document.getElementById('rule_%s_%s'.format(table.toLowerCase(), chain)); - - if (elem) { - (document.documentElement || document.body.parentNode || document.body).scrollTop = elem.offsetTop - 40; - elem.classList.remove('flash'); - void elem.offsetWidth; - elem.classList.add('flash'); - - if (num) { - var rule = elem.nextElementSibling.childNodes[num]; - if (rule) { - rule.classList.remove('flash'); - void rule.offsetWidth; - rule.classList.add('flash'); - } - } - } - } - - function parse_output(table, s) - { - var current_chain = null; - var current_rules = []; - var seen_chains = {}; - var chain_refs = {}; - var re = /([^\n]*)\n/g; - var m, m2; - - while ((m = re.exec(s)) != null) { - if (m[1].match(/^Chain (.+) \(policy (\w+) (\d+) packets, (\d+) bytes\)$/)) { - var chain = RegExp.$1, - policy = RegExp.$2, - packets = +RegExp.$3, - bytes = +RegExp.$4; - - update_chain_section(current_chain, current_rules); - - seen_chains[chain] = true; - current_chain = create_chain_section(table, chain, policy, packets, bytes); - current_rules = []; - } - else if (m[1].match(/^Chain (.+) \((\d+) references\)$/)) { - var chain = RegExp.$1, - references = +RegExp.$2; - - update_chain_section(current_chain, current_rules); - - seen_chains[chain] = true; - current_chain = create_chain_section(table, chain, null, null, null, references); - current_rules = []; - } - else if (m[1].match(/^num /)) { - continue; - } - else if ((m2 = m[1].match(/^(\d+) +(\d+) +(\d+) +(.*?) +(\S+) +(\S*) +(\S+) +(\S+) +([a-f0-9:.]+\/\d+) +([a-f0-9:.]+\/\d+) +(.+)$/)) !== null) { - var num = +m2[1], - pkts = +m2[2], - bytes = +m2[3], - target = m2[4], - proto = m2[5], - indev = m2[7], - outdev = m2[8], - srcnet = m2[9], - dstnet = m2[10], - options = m2[11] || '-', - comment = '-'; - - options = options.trim().replace(/(?:^| )\/\* (.+) \*\//, - function(m1, m2) { - comment = m2.replace(/^!fw3(: |$)/, '').trim() || '-'; - return ''; - }) || '-'; - - current_rules.push([ - '%.2m'.format(pkts).nobr(), - '%.2mB'.format(bytes).nobr(), - target ? '<span class="target">%s</span>'.format(target) : '-', - proto, - (indev !== '*') ? '<span class="ifacebadge">%s</span>'.format(indev) : '*', - (outdev !== '*') ? '<span class="ifacebadge">%s</span>'.format(outdev) : '*', - srcnet, - dstnet, - options, - comment - ]); - - if (target) { - chain_refs[target] = chain_refs[target] || []; - chain_refs[target].push([ current_chain, num ]); - } - } - } - - update_chain_section(current_chain, current_rules); - - document.querySelectorAll('[data-table="%s"] [data-chain]'.format(table)) - .forEach(function(cdiv) { - if (!seen_chains[cdiv.getAttribute('data-chain')]) { - cdiv.parentNode.removeChild(cdiv); - return; - } - - cdiv.querySelectorAll('.target').forEach(function(tspan) { - if (seen_chains[tspan.textContent]) { - tspan.classList.add('jump'); - tspan.addEventListener('click', jump_target); - } - }); - - cdiv.querySelectorAll('.references').forEach(function(rspan) { - var refs = chain_refs[cdiv.getAttribute('data-chain')]; - if (refs && refs.length) { - rspan.classList.add('cbi-tooltip-container'); - rspan.appendChild(E('small', { 'class': 'cbi-tooltip ifacebadge', 'style': 'top:1em; left:auto' }, [ E('ul') ])); - - refs.forEach(function(ref) { - var chain = ref[0].parentNode.getAttribute('data-chain'), - num = ref[1]; - - rspan.lastElementChild.lastElementChild.appendChild(E('li', {}, [ - '<%:Chain%> ', - E('span', { - 'class': 'jump', - 'data-num': num, - 'onclick': 'jump_target(event)' - }, chain), - ', <%:Rule%> #%d'.format(num) - ])); - }); - } - }); - }); - } - - table_names.forEach(function(table) { - XHR.poll(5, '<%=url("admin/status/iptables_dump", tostring(mode))%>/' + table.toLowerCase(), null, - function (xhr) { - parse_output(table, xhr.responseText); - }); - }); -//]]></script> - <h2 name="content"><%:Firewall Status%></h2> <% if has_ip6tables then %> <ul class="cbi-tabmenu"> - <li class="cbi-tab<%= mode ~= 4 and "-disabled" %>"><a href="<%=url("admin/status/iptables/4")%>"><%:IPv4 Firewall%></a></li> - <li class="cbi-tab<%= mode ~= 6 and "-disabled" %>"><a href="<%=url("admin/status/iptables/6")%>"><%:IPv6 Firewall%></a></li> + <li data-mode="4" class="cbi-tab<%= mode ~= 4 and "-disabled" %>"> + <a href="<%=url("admin/status/iptables/4")%>"><%:IPv4 Firewall%></a> + </li> + <li data-mode="6" class="cbi-tab<%= mode ~= 6 and "-disabled" %>"> + <a href="<%=url("admin/status/iptables/6")%>"><%:IPv6 Firewall%></a> + </li> </ul> <% end %> @@ -314,7 +65,9 @@ </div> <div id="iptables"> - <p><em><%:Collecting data...%></em></p> + <p><em class="spinning"><%:Collecting data...%></em></p> </div> +<script type="text/javascript" src="<%=resource%>/view/status/iptables.js"></script> + <%+footer%> diff --git a/modules/luci-mod-system/htdocs/luci-static/resources/view/system/password.js b/modules/luci-mod-system/htdocs/luci-static/resources/view/system/password.js new file mode 100644 index 0000000000..7a79d7e2da --- /dev/null +++ b/modules/luci-mod-system/htdocs/luci-static/resources/view/system/password.js @@ -0,0 +1,31 @@ +function submitPassword(ev) { + var pw1 = document.body.querySelector('[name="pw1"]'), + pw2 = document.body.querySelector('[name="pw2"]'); + + if (!pw1.value.length || !pw2.value.length) + return; + + if (pw1.value === pw2.value) { + L.showModal(_('Change login password'), + E('p', { class: 'spinning' }, _('Changing password…'))); + + L.post('admin/system/admin/password/json', { password: pw1.value }, + function() { + showModal(_('Change login password'), [ + E('div', _('The system password has been successfully changed.')), + E('div', { 'class': 'right' }, + E('div', { class: 'btn', click: L.hideModal }, _('Dismiss'))) + ]); + + pw1.value = pw2.value = ''; + }); + } + else { + L.showModal(_('Change login password'), [ + E('div', { class: 'alert-message warning' }, + _('Given password confirmation did not match, password not changed!')), + E('div', { 'class': 'right' }, + E('div', { class: 'btn', click: L.hideModal }, _('Dismiss'))) + ]); + } +} diff --git a/modules/luci-mod-system/htdocs/luci-static/resources/view/system/sshkeys.js b/modules/luci-mod-system/htdocs/luci-static/resources/view/system/sshkeys.js new file mode 100644 index 0000000000..d298b3be98 --- /dev/null +++ b/modules/luci-mod-system/htdocs/luci-static/resources/view/system/sshkeys.js @@ -0,0 +1,215 @@ +SSHPubkeyDecoder.prototype = { + lengthDecode: function(s, off) + { + var l = (s.charCodeAt(off++) << 24) | + (s.charCodeAt(off++) << 16) | + (s.charCodeAt(off++) << 8) | + s.charCodeAt(off++); + + if (l < 0 || (off + l) > s.length) + return -1; + + return l; + }, + + decode: function(s) + { + var parts = s.split(/\s+/); + if (parts.length < 2) + return null; + + var key = null; + try { key = atob(parts[1]); } catch(e) {} + if (!key) + return null; + + var off, len; + + off = 0; + len = this.lengthDecode(key, off); + + if (len <= 0) + return null; + + var type = key.substr(off + 4, len); + if (type !== parts[0]) + return null; + + off += 4 + len; + + var len1 = off < key.length ? this.lengthDecode(key, off) : 0; + if (len1 <= 0) + return null; + + var curve = null; + if (type.indexOf('ecdsa-sha2-') === 0) { + curve = key.substr(off + 4, len1); + + if (!len1 || type.substr(11) !== curve) + return null; + + type = 'ecdsa-sha2'; + curve = curve.replace(/^nistp(\d+)$/, 'NIST P-$1'); + } + + off += 4 + len1; + + var len2 = off < key.length ? this.lengthDecode(key, off) : 0; + if (len2 < 0) + return null; + + if (len1 & 1) + len1--; + + if (len2 & 1) + len2--; + + var comment = parts.slice(2).join(' '), + fprint = parts[1].length > 68 ? parts[1].substr(0, 33) + '…' + parts[1].substr(-34) : parts[1]; + + switch (type) + { + case 'ssh-rsa': + return { type: 'RSA', bits: len2 * 8, comment: comment, fprint: fprint }; + + case 'ssh-dss': + return { type: 'DSA', bits: len1 * 8, comment: comment, fprint: fprint }; + + case 'ssh-ed25519': + return { type: 'ECDH', curve: 'Curve25519', comment: comment, fprint: fprint }; + + case 'ecdsa-sha2': + return { type: 'ECDSA', curve: curve, comment: comment, fprint: fprint }; + + default: + return null; + } + } +}; + +function SSHPubkeyDecoder() {} + +function renderKeys(keys) { + var list = document.querySelector('.cbi-dynlist[name="sshkeys"]'), + decoder = new SSHPubkeyDecoder(); + + while (!matchesElem(list.firstElementChild, '.add-item')) + list.removeChild(list.firstElementChild); + + keys.forEach(function(key) { + var pubkey = decoder.decode(key); + if (pubkey) + list.insertBefore(E('div', { + class: 'item', + click: removeKey, + 'data-key': key + }, [ + E('strong', pubkey.comment || _('Unnamed key')), E('br'), + E('small', [ + '%s, %s'.format(pubkey.type, pubkey.curve || _('%d Bit').format(pubkey.bits)), + E('br'), E('code', pubkey.fprint) + ]) + ]), list.lastElementChild); + }); + + if (list.firstElementChild === list.lastElementChild) + list.insertBefore(E('p', _('No public keys present yet.')), list.lastElementChild); +} + +function saveKeys(keys) { + L.showModal(_('Add key'), E('div', { class: 'spinning' }, _('Saving keys…'))); + L.post('admin/system/admin/sshkeys/json', { keys: JSON.stringify(keys) }, function(xhr, keys) { + renderKeys(keys); + L.hideModal(); + }); +} + +function addKey(ev) { + var decoder = new SSHPubkeyDecoder(), + list = findParent(ev.target, '.cbi-dynlist'), + input = list.querySelector('input[type="text"]'), + key = input.value.trim(), + pubkey = decoder.decode(key), + keys = []; + + if (!key.length) + return; + + list.querySelectorAll('.item').forEach(function(item) { + keys.push(item.getAttribute('data-key')); + }); + + if (keys.indexOf(key) !== -1) { + L.showModal(_('Add key'), [ + E('div', { class: 'alert-message warning' }, _('The given SSH public key has already been added.')), + E('div', { class: 'right' }, E('div', { class: 'btn', click: L.hideModal }, _('Close'))) + ]); + } + else if (!pubkey) { + L.showModal(_('Add key'), [ + E('div', { class: 'alert-message warning' }, _('The given SSH public key is invalid. Please supply proper public RSA or ECDSA keys.')), + E('div', { class: 'right' }, E('div', { class: 'btn', click: L.hideModal }, _('Close'))) + ]); + } + else { + keys.push(key); + saveKeys(keys); + input.value = ''; + } +} + +function removeKey(ev) { + var list = findParent(ev.target, '.cbi-dynlist'), + delkey = ev.target.getAttribute('data-key'), + keys = []; + + list.querySelectorAll('.item').forEach(function(item) { + var key = item.getAttribute('data-key'); + if (key !== delkey) + keys.push(key); + }); + + L.showModal(_('Delete key'), [ + E('div', _('Do you really want to delete the following SSH key?')), + E('pre', delkey), + E('div', { class: 'right' }, [ + E('div', { class: 'btn', click: L.hideModal }, _('Cancel')), + ' ', + E('div', { class: 'btn danger', click: function(ev) { saveKeys(keys) } }, _('Delete key')), + ]) + ]); +} + +function dragKey(ev) { + ev.stopPropagation(); + ev.preventDefault(); + ev.dataTransfer.dropEffect = 'copy'; +} + +function dropKey(ev) { + var file = ev.dataTransfer.files[0], + input = ev.currentTarget.querySelector('input[type="text"]'), + reader = new FileReader(); + + if (file) { + reader.onload = function(rev) { + input.value = rev.target.result.trim(); + addKey(ev); + input.value = ''; + }; + + reader.readAsText(file); + } + + ev.stopPropagation(); + ev.preventDefault(); +} + +window.addEventListener('dragover', function(ev) { ev.preventDefault() }); +window.addEventListener('drop', function(ev) { ev.preventDefault() }); + +requestAnimationFrame(function() { + L.get('admin/system/admin/sshkeys/json', null, function(xhr, keys) { + renderKeys(keys); + }); +}); diff --git a/modules/luci-mod-system/luasrc/view/admin_system/password.htm b/modules/luci-mod-system/luasrc/view/admin_system/password.htm index db35fb01e8..09cea4f74a 100644 --- a/modules/luci-mod-system/luasrc/view/admin_system/password.htm +++ b/modules/luci-mod-system/luasrc/view/admin_system/password.htm @@ -1,40 +1,5 @@ <%+header%> -<script type="application/javascript">//<![CDATA[ - function submitPassword(ev) { - var pw1 = document.body.querySelector('[name="pw1"]'), - pw2 = document.body.querySelector('[name="pw2"]'); - - if (!pw1.value.length || !pw2.value.length) - return; - - if (pw1.value === pw2.value) { - showModal('<%:Change login password%>', - E('p', { class: 'spinning' }, '<%:Changing password…%>')); - - (new XHR()).post('<%=url("admin/system/admin/password/json")%>', - { token: '<%=token%>', password: pw1.value }, - function() { - showModal('<%:Change login password%>', [ - E('div', _('The system password has been successfully changed.')), - E('div', { 'class': 'right' }, - E('div', { class: 'btn', click: hideModal }, '<%:Dismiss%>')) - ]); - - pw1.value = pw2.value = ''; - }); - } - else { - showModal('<%:Change login password%>', [ - E('div', { class: 'alert-message warning' }, - _('Given password confirmation did not match, password not changed!')), - E('div', { 'class': 'right' }, - E('div', { class: 'btn', click: hideModal }, '<%:Dismiss%>')) - ]); - } - } -//]]></script> - <input type="password" aria-hidden="true" style="position:absolute; left:-10000px" /> <div class="cbi-map"> @@ -67,4 +32,6 @@ <button class="btn cbi-button-apply" onclick="submitPassword(event)"><%:Save%></button> </div> +<script type="application/javascript" src="<%=resource%>/view/system/password.js"></script> + <%+footer%> diff --git a/modules/luci-mod-system/luasrc/view/admin_system/sshkeys.htm b/modules/luci-mod-system/luasrc/view/admin_system/sshkeys.htm index acf008adf3..77efa11a0f 100644 --- a/modules/luci-mod-system/luasrc/view/admin_system/sshkeys.htm +++ b/modules/luci-mod-system/luasrc/view/admin_system/sshkeys.htm @@ -6,224 +6,6 @@ } </style> -<script type="application/javascript">//<![CDATA[ - SSHPubkeyDecoder.prototype = { - lengthDecode: function(s, off) - { - var l = (s.charCodeAt(off++) << 24) | - (s.charCodeAt(off++) << 16) | - (s.charCodeAt(off++) << 8) | - s.charCodeAt(off++); - - if (l < 0 || (off + l) > s.length) - return -1; - - return l; - }, - - decode: function(s) - { - var parts = s.split(/\s+/); - if (parts.length < 2) - return null; - - var key = null; - try { key = atob(parts[1]); } catch(e) {} - if (!key) - return null; - - var off, len; - - off = 0; - len = this.lengthDecode(key, off); - - if (len <= 0) - return null; - - var type = key.substr(off + 4, len); - if (type !== parts[0]) - return null; - - off += 4 + len; - - var len1 = off < key.length ? this.lengthDecode(key, off) : 0; - if (len1 <= 0) - return null; - - var curve = null; - if (type.indexOf('ecdsa-sha2-') === 0) { - curve = key.substr(off + 4, len1); - - if (!len1 || type.substr(11) !== curve) - return null; - - type = 'ecdsa-sha2'; - curve = curve.replace(/^nistp(\d+)$/, 'NIST P-$1'); - } - - off += 4 + len1; - - var len2 = off < key.length ? this.lengthDecode(key, off) : 0; - if (len2 < 0) - return null; - - if (len1 & 1) - len1--; - - if (len2 & 1) - len2--; - - var comment = parts.slice(2).join(' '), - fprint = parts[1].length > 68 ? parts[1].substr(0, 33) + '…' + parts[1].substr(-34) : parts[1]; - - switch (type) - { - case 'ssh-rsa': - return { type: 'RSA', bits: len2 * 8, comment: comment, fprint: fprint }; - - case 'ssh-dss': - return { type: 'DSA', bits: len1 * 8, comment: comment, fprint: fprint }; - - case 'ssh-ed25519': - return { type: 'ECDH', curve: 'Curve25519', comment: comment, fprint: fprint }; - - case 'ecdsa-sha2': - return { type: 'ECDSA', curve: curve, comment: comment, fprint: fprint }; - - default: - return null; - } - } - }; - - function SSHPubkeyDecoder() {} - - function renderKeys(keys) { - var list = document.querySelector('.cbi-dynlist[name="sshkeys"]'), - decoder = new SSHPubkeyDecoder(); - - while (!matchesElem(list.firstElementChild, '.add-item')) - list.removeChild(list.firstElementChild); - - keys.forEach(function(key) { - var pubkey = decoder.decode(key); - if (pubkey) - list.insertBefore(E('div', { - class: 'item', - click: removeKey, - 'data-key': key - }, [ - E('strong', pubkey.comment || _('Unnamed key')), E('br'), - E('small', [ - '%s, %s'.format(pubkey.type, pubkey.curve || _('%d Bit').format(pubkey.bits)), - E('br'), E('code', pubkey.fprint) - ]) - ]), list.lastElementChild); - }); - - if (list.firstElementChild === list.lastElementChild) - list.insertBefore(E('p', _('No public keys present yet.')), list.lastElementChild); - } - - function saveKeys(keys) { - showModal('<%:Add key%>', E('div', { class: 'spinning' }, _('Saving keys…'))); - (new XHR()).post('<%=url("admin/system/admin/sshkeys/json")%>', { token: '<%=token%>', keys: JSON.stringify(keys) }, function(xhr, keys) { - renderKeys(keys); - hideModal(); - }); - } - - function addKey(ev) { - var decoder = new SSHPubkeyDecoder(), - list = findParent(ev.target, '.cbi-dynlist'), - input = list.querySelector('input[type="text"]'), - key = input.value.trim(), - pubkey = decoder.decode(key), - keys = []; - - if (!key.length) - return; - - list.querySelectorAll('.item').forEach(function(item) { - keys.push(item.getAttribute('data-key')); - }); - - if (keys.indexOf(key) !== -1) { - showModal('<%:Add key%>', [ - E('div', { class: 'alert-message warning' }, _('The given SSH public key has already been added.')), - E('div', { class: 'right' }, E('div', { class: 'btn', click: hideModal }, _('Close'))) - ]); - } - else if (!pubkey) { - showModal('<%:Add key%>', [ - E('div', { class: 'alert-message warning' }, _('The given SSH public key is invalid. Please supply proper public RSA or ECDSA keys.')), - E('div', { class: 'right' }, E('div', { class: 'btn', click: hideModal }, _('Close'))) - ]); - } - else { - keys.push(key); - saveKeys(keys); - input.value = ''; - } - } - - function removeKey(ev) { - var list = findParent(ev.target, '.cbi-dynlist'), - delkey = ev.target.getAttribute('data-key'), - keys = []; - - list.querySelectorAll('.item').forEach(function(item) { - var key = item.getAttribute('data-key'); - if (key !== delkey) - keys.push(key); - }); - - showModal('<%:Delete key%>', [ - E('div', _('Do you really want to delete the following SSH key?')), - E('pre', delkey), - E('div', { class: 'right' }, [ - E('div', { class: 'btn', click: hideModal }, _('Cancel')), - ' ', - E('div', { class: 'btn danger', click: function(ev) { saveKeys(keys) } }, _('Delete key')), - ]) - ]); - } - - function dragKey(ev) { - ev.stopPropagation(); - ev.preventDefault(); - ev.dataTransfer.dropEffect = 'copy'; - } - - function dropKey(ev) { - var file = ev.dataTransfer.files[0], - input = ev.currentTarget.querySelector('input[type="text"]'), - reader = new FileReader(); - - if (file) { - reader.onload = function(rev) { - input.value = rev.target.result.trim(); - addKey(ev); - input.value = ''; - }; - - reader.readAsText(file); - } - - ev.stopPropagation(); - ev.preventDefault(); - } - - window.addEventListener('dragover', function(ev) { ev.preventDefault() }); - window.addEventListener('drop', function(ev) { ev.preventDefault() }); - - requestAnimationFrame(function() { - XHR.get('<%=url("admin/system/admin/sshkeys/json")%>', null, function(xhr, keys) { - renderKeys(keys); - }); - }); -//]]></script> - <div class="cbi-map"> <h2><%:SSH-Keys%></h2> @@ -235,11 +17,13 @@ <div class="cbi-dynlist" name="sshkeys"> <p class="spinning"><%:Loading SSH keys…%></p> <div class="add-item" ondragover="dragKey(event)" ondrop="dropKey(event)"> - <input class="cbi-input-text" type="text" placeholder="<%:Paste or drag SSH key file…%>" onkeydown="if (event.keyCode === 13) addKey(event)" /><!-- - --><div class="cbi-button" onclick="addKey(event)"><%:Add key%></div> + <input class="cbi-input-text" type="text" placeholder="<%:Paste or drag SSH key file…%>" onkeydown="if (event.keyCode === 13) addKey(event)" /> + <button class="cbi-button" onclick="addKey(event)"><%:Add key%></button> </div> </div> </div> </div> +<script type="application/javascript" src="<%=resource%>/view/system/sshkeys.js"></script> + <%+footer%> diff --git a/themes/luci-theme-bootstrap/htdocs/luci-static/bootstrap/cascade.css b/themes/luci-theme-bootstrap/htdocs/luci-static/bootstrap/cascade.css index bb0a0987da..2322a73857 100644 --- a/themes/luci-theme-bootstrap/htdocs/luci-static/bootstrap/cascade.css +++ b/themes/luci-theme-bootstrap/htdocs/luci-static/bootstrap/cascade.css @@ -1993,7 +1993,7 @@ table table td, margin: -.125em; } -#dsl_status_table .ifacebox-body > span > strong { +#dsl_status_table .ifacebox-body span > strong { display: inline-block; min-width: 35%; } diff --git a/themes/luci-theme-openwrt/htdocs/luci-static/openwrt.org/cascade.css b/themes/luci-theme-openwrt/htdocs/luci-static/openwrt.org/cascade.css index 4203f03624..f6ea9645ff 100644 --- a/themes/luci-theme-openwrt/htdocs/luci-static/openwrt.org/cascade.css +++ b/themes/luci-theme-openwrt/htdocs/luci-static/openwrt.org/cascade.css @@ -1511,7 +1511,7 @@ select + .cbi-button { margin: .5em 0 0 0; } -#dsl_status_table .ifacebox-body > span > strong { +#dsl_status_table .ifacebox-body span > strong { display: inline-block; min-width: 35%; } |