summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--applications/luci-app-opkg/htdocs/luci-static/resources/view/opkg.js812
-rw-r--r--applications/luci-app-opkg/luasrc/view/opkg.htm824
-rw-r--r--luci.mk14
-rw-r--r--modules/luci-base/Makefile3
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/cbi.js190
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/luci.js323
-rw-r--r--modules/luci-base/luasrc/view/cbi/value.htm2
-rw-r--r--modules/luci-base/luasrc/view/header.htm11
-rw-r--r--modules/luci-base/src/Makefile3
-rw-r--r--modules/luci-base/src/jsmin.c292
-rw-r--r--modules/luci-mod-network/htdocs/luci-static/resources/view/network/network.js135
-rw-r--r--modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js93
-rw-r--r--modules/luci-mod-network/luasrc/model/cbi/admin_network/network.lua63
-rw-r--r--modules/luci-mod-network/luasrc/model/cbi/admin_network/wifi_overview.lua76
-rw-r--r--modules/luci-mod-network/luasrc/view/admin_network/iface_overview.htm53
-rw-r--r--modules/luci-mod-network/luasrc/view/admin_network/iface_overview_status.htm183
-rw-r--r--modules/luci-mod-network/luasrc/view/admin_network/wifi_overview.htm61
-rw-r--r--modules/luci-mod-network/luasrc/view/admin_network/wifi_overview_status.htm127
-rw-r--r--modules/luci-mod-status/htdocs/luci-static/resources/view/status/index.js215
-rw-r--r--modules/luci-mod-status/htdocs/luci-static/resources/view/status/iptables.js253
-rw-r--r--modules/luci-mod-status/luasrc/view/admin_status/index.htm246
-rw-r--r--modules/luci-mod-status/luasrc/view/admin_status/iptables.htm265
-rw-r--r--modules/luci-mod-system/htdocs/luci-static/resources/view/system/password.js31
-rw-r--r--modules/luci-mod-system/htdocs/luci-static/resources/view/system/sshkeys.js215
-rw-r--r--modules/luci-mod-system/luasrc/view/admin_system/password.htm37
-rw-r--r--modules/luci-mod-system/luasrc/view/admin_system/sshkeys.htm224
-rw-r--r--themes/luci-theme-bootstrap/htdocs/luci-static/bootstrap/cascade.css2
-rw-r--r--themes/luci-theme-openwrt/htdocs/luci-static/openwrt.org/cascade.css2
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%>
diff --git a/luci.mk b/luci.mk
index f9153819ee..aa2e195e27 100644
--- a/luci.mk
+++ b/luci.mk
@@ -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%;
}