summaryrefslogtreecommitdiffhomepage
path: root/modules/luci-base/htdocs/luci-static/resources/ui.js
diff options
context:
space:
mode:
Diffstat (limited to 'modules/luci-base/htdocs/luci-static/resources/ui.js')
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/ui.js447
1 files changed, 403 insertions, 44 deletions
diff --git a/modules/luci-base/htdocs/luci-static/resources/ui.js b/modules/luci-base/htdocs/luci-static/resources/ui.js
index ac158f5260..ef6e334216 100644
--- a/modules/luci-base/htdocs/luci-static/resources/ui.js
+++ b/modules/luci-base/htdocs/luci-static/resources/ui.js
@@ -777,7 +777,7 @@ var UISelect = UIElement.extend(/** @lends LuCI.ui.Select.prototype */ {
keys = Object.keys(this.choices);
if (this.options.sort === true)
- keys.sort();
+ keys.sort(L.naturalCompare);
else if (Array.isArray(this.options.sort))
keys = this.options.sort;
@@ -1056,7 +1056,7 @@ var UIDropdown = UIElement.extend(/** @lends LuCI.ui.Dropdown.prototype */ {
var keys = Object.keys(this.choices);
if (this.options.sort === true)
- keys.sort();
+ keys.sort(L.naturalCompare);
else if (Array.isArray(this.options.sort))
keys = this.options.sort;
@@ -2208,7 +2208,7 @@ var UIDynamicList = UIElement.extend(/** @lends LuCI.ui.DynamicList.prototype */
'id': this.options.id,
'class': 'cbi-dynlist',
'disabled': this.options.disabled ? '' : null
- }, E('div', { 'class': 'add-item' }));
+ }, E('div', { 'class': 'add-item control-group' }));
if (this.choices) {
if (this.options.placeholder != null)
@@ -2859,13 +2859,8 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
rows = E('ul');
list.sort(function(a, b) {
- var isDirA = (a.type == 'directory'),
- isDirB = (b.type == 'directory');
-
- if (isDirA != isDirB)
- return isDirA < isDirB;
-
- return a.name > b.name;
+ return L.naturalCompare(a.type == 'directory', b.type == 'directory') ||
+ L.naturalCompare(a.name, b.name);
});
for (var i = 0; i < list.length; i++) {
@@ -3125,7 +3120,24 @@ var UIMenu = baseclass.singleton(/** @lends LuCI.ui.menu.prototype */ {
if (!node.children[k].hasOwnProperty('title'))
continue;
- children.push(Object.assign(node.children[k], { name: k }));
+ var subnode = Object.assign(node.children[k], { name: k });
+
+ if (L.isObject(subnode.action) && subnode.action.path != null &&
+ (subnode.action.type == 'alias' || subnode.action.type == 'rewrite')) {
+ var root = this.menu,
+ path = subnode.action.path.split('/');
+
+ for (var i = 0; root != null && i < path.length; i++)
+ root = L.isObject(root.children) ? root.children[path[i]] : null;
+
+ if (root)
+ subnode = Object.assign({}, subnode, {
+ children: root.children,
+ action: root.action
+ });
+ }
+
+ children.push(subnode);
}
return children.sort(function(a, b) {
@@ -3135,11 +3147,300 @@ var UIMenu = baseclass.singleton(/** @lends LuCI.ui.menu.prototype */ {
if (wA != wB)
return wA - wB;
- return a.name > b.name;
+ return L.naturalCompare(a.name, b.name);
});
}
});
+var UITable = baseclass.extend(/** @lends LuCI.ui.table.prototype */ {
+ __init__: function(captions, options, placeholder) {
+ if (!Array.isArray(captions)) {
+ this.initFromMarkup(captions);
+
+ return;
+ }
+
+ var id = options.id || 'table%08x'.format(Math.random() * 0xffffffff);
+
+ var table = E('table', { 'id': id, 'class': 'table' }, [
+ E('tr', { 'class': 'tr table-titles', 'click': UI.prototype.createHandlerFn(this, 'handleSort') })
+ ]);
+
+ this.id = id;
+ this.node = table
+ this.options = options;
+
+ var sorting = this.getActiveSortState();
+
+ for (var i = 0; i < captions.length; i++) {
+ if (captions[i] == null)
+ continue;
+
+ var th = E('th', { 'class': 'th' }, [ captions[i] ]);
+
+ if (typeof(options.captionClasses) == 'object')
+ DOMTokenList.prototype.add.apply(th.classList, L.toArray(options.captionClasses[i]));
+
+ if (options.sortable !== false && (typeof(options.sortable) != 'object' || options.sortable[i] !== false)) {
+ th.setAttribute('data-sortable-row', true);
+
+ if (sorting && sorting[0] == i)
+ th.setAttribute('data-sort-direction', sorting[1] ? 'desc' : 'asc');
+ }
+
+ table.firstElementChild.appendChild(th);
+ }
+
+ if (placeholder) {
+ var trow = table.appendChild(E('tr', { 'class': 'tr placeholder' })),
+ td = trow.appendChild(E('td', { 'class': 'td' }, placeholder));
+
+ if (typeof(captionClasses) == 'object')
+ DOMTokenList.prototype.add.apply(td.classList, L.toArray(captionClasses[0]));
+ }
+
+ DOMTokenList.prototype.add.apply(table.classList, L.toArray(options.classes));
+ },
+
+ update: function(data, placeholder) {
+ var placeholder = placeholder || this.options.placeholder || _('No data', 'empty table placeholder'),
+ sorting = this.getActiveSortState();
+
+ if (!Array.isArray(data))
+ return;
+
+ if (sorting) {
+ var list = data.map(L.bind(function(row) {
+ return [ this.deriveSortKey(row[sorting[0]], sorting[0]), row ];
+ }, this));
+
+ list.sort(function(a, b) {
+ return sorting[1]
+ ? -L.naturalCompare(a[0], b[0])
+ : L.naturalCompare(a[0], b[0]);
+ });
+
+ data.length = 0;
+
+ list.forEach(function(item) {
+ data.push(item[1]);
+ });
+ }
+
+ this.data = data;
+ this.placeholder = placeholder;
+
+ var n = 0,
+ rows = this.node.querySelectorAll('tr'),
+ trows = [],
+ headings = [].slice.call(this.node.firstElementChild.querySelectorAll('th')),
+ captionClasses = this.options.captionClasses;
+
+ data.forEach(function(row) {
+ trows[n] = E('tr', { 'class': 'tr' });
+
+ for (var i = 0; i < headings.length; i++) {
+ var text = (headings[i].innerText || '').trim();
+ var td = trows[n].appendChild(E('td', {
+ 'class': 'td',
+ 'data-title': (text !== '') ? text : null
+ }, (row[i] != null) ? row[i] : ''));
+
+ if (typeof(captionClasses) == 'object')
+ DOMTokenList.prototype.add.apply(td.classList, L.toArray(captionClasses[i]));
+
+ if (!td.classList.contains('cbi-section-actions'))
+ headings[i].setAttribute('data-sortable-row', true);
+ }
+
+ trows[n].classList.add('cbi-rowstyle-%d'.format((n++ % 2) ? 2 : 1));
+ });
+
+ for (var i = 0; i < n; i++) {
+ if (rows[i+1])
+ this.node.replaceChild(trows[i], rows[i+1]);
+ else
+ this.node.appendChild(trows[i]);
+ }
+
+ while (rows[++n])
+ this.node.removeChild(rows[n]);
+
+ if (placeholder && this.node.firstElementChild === this.node.lastElementChild) {
+ var trow = this.node.appendChild(E('tr', { 'class': 'tr placeholder' })),
+ td = trow.appendChild(E('td', { 'class': 'td' }, placeholder));
+
+ if (typeof(captionClasses) == 'object')
+ DOMTokenList.prototype.add.apply(td.classList, L.toArray(captionClasses[0]));
+ }
+
+ return this.node;
+ },
+
+ render: function() {
+ return this.node;
+ },
+
+ /** @private */
+ initFromMarkup: function(node) {
+ if (!dom.elem(node))
+ node = document.querySelector(node);
+
+ if (!node)
+ throw 'Invalid table selector';
+
+ var options = {},
+ headrow = node.querySelector('tr, .tr');
+
+ if (!headrow)
+ return;
+
+ options.classes = [].slice.call(node.classList).filter(function(c) { return c != 'table' });
+ options.sortable = [];
+ options.captionClasses = [];
+
+ headrow.querySelectorAll('th, .th').forEach(function(th, i) {
+ options.sortable[i] = !th.classList.contains('cbi-section-actions');
+ options.captionClasses[i] = [].slice.call(th.classList).filter(function(c) { return c != 'th' });
+ });
+
+ headrow.addEventListener('click', UI.prototype.createHandlerFn(this, 'handleSort'));
+
+ this.id = node.id;
+ this.node = node;
+ this.options = options;
+ },
+
+ /** @private */
+ deriveSortKey: function(value, index) {
+ var opts = this.options || {},
+ hint, m;
+
+ if (opts.sortable == true || opts.sortable == null)
+ hint = 'auto';
+ else if (typeof( opts.sortable) == 'object')
+ hint = opts.sortable[index];
+
+ if (dom.elem(value))
+ value = value.innerText.trim();
+
+ switch (hint || 'auto') {
+ case true:
+ case 'auto':
+ m = /^([0-9a-fA-F:.]+)(?:\/([0-9a-fA-F:.]+))?$/.exec(value);
+
+ if (m) {
+ var addr, mask;
+
+ addr = validation.parseIPv6(m[1]);
+ mask = m[2] ? validation.parseIPv6(m[2]) : null;
+
+ if (addr && mask != null)
+ return '%04x%04x%04x%04x%04x%04x%04x%04x%04x%04x%04x%04x%04x%04x%04x%04x'.format(
+ addr[0], addr[1], addr[2], addr[3], addr[4], addr[5], addr[6], addr[7],
+ mask[0], mask[1], mask[2], mask[3], mask[4], mask[5], mask[6], mask[7]
+ );
+ else if (addr)
+ return '%04x%04x%04x%04x%04x%04x%04x%04x%02x'.format(
+ addr[0], addr[1], addr[2], addr[3], addr[4], addr[5], addr[6], addr[7],
+ m[2] ? +m[2] : 128
+ );
+
+ addr = validation.parseIPv4(m[1]);
+ mask = m[2] ? validation.parseIPv4(m[2]) : null;
+
+ if (addr && mask != null)
+ return '%03d%03d%03d%03d%03d%03d%03d%03d'.format(
+ addr[0], addr[1], addr[2], addr[3],
+ mask[0], mask[1], mask[2], mask[3]
+ );
+ else if (addr)
+ return '%03d%03d%03d%03d%02d'.format(
+ addr[0], addr[1], addr[2], addr[3],
+ m[2] ? +m[2] : 32
+ );
+ }
+
+ m = /^(?:(\d+)d )?(\d+)h (\d+)m (\d+)s$/.exec(value);
+
+ if (m)
+ return '%05d%02d%02d%02d'.format(+m[1], +m[2], +m[3], +m[4]);
+
+ m = /^(\d+)\b(\D*)$/.exec(value);
+
+ if (m)
+ return '%010d%s'.format(+m[1], m[2]);
+
+ return String(value);
+
+ case 'ignorecase':
+ return String(value).toLowerCase();
+
+ case 'numeric':
+ return +value;
+
+ default:
+ return String(value);
+ }
+ },
+
+ /** @private */
+ getActiveSortState: function() {
+ if (this.sortState)
+ return this.sortState;
+
+ var page = document.body.getAttribute('data-page'),
+ key = page + '.' + this.id,
+ state = session.getLocalData('tablesort');
+
+ if (L.isObject(state) && Array.isArray(state[key]))
+ return state[key];
+
+ return null;
+ },
+
+ /** @private */
+ setActiveSortState: function(index, descending) {
+ this.sortState = [ index, descending ];
+
+ if (!this.options.id)
+ return;
+
+ var page = document.body.getAttribute('data-page'),
+ key = page + '.' + this.id,
+ state = session.getLocalData('tablesort');
+
+ if (!L.isObject(state))
+ state = {};
+
+ state[key] = this.sortState;
+
+ session.setLocalData('tablesort', state);
+ },
+
+ /** @private */
+ handleSort: function(ev) {
+ if (!ev.target.matches('th[data-sortable-row]'))
+ return;
+
+ var th = ev.target,
+ direction = (th.getAttribute('data-sort-direction') == 'asc'),
+ index = 0;
+
+ this.node.firstElementChild.querySelectorAll('th').forEach(function(other_th, i) {
+ if (other_th !== th)
+ other_th.removeAttribute('data-sort-direction');
+ else
+ index = i;
+ });
+
+ th.setAttribute('data-sort-direction', direction ? 'desc' : 'asc');
+
+ this.setActiveSortState(index, direction);
+ this.update(this.data, this.placeholder);
+ }
+});
+
/**
* @class ui
* @memberof LuCI
@@ -4089,10 +4390,16 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
'class': 'btn',
'click': UI.prototype.hideModal
}, [ _('Close') ]), ' ',
- E('button', {
- 'class': 'cbi-button cbi-button-positive important',
- 'click': L.bind(this.apply, this, true)
- }, [ _('Save & Apply') ]), ' ',
+ new UIComboButton('0', {
+ 0: [ _('Save & Apply') ],
+ 1: [ _('Apply unchecked') ]
+ }, {
+ classes: {
+ 0: 'btn cbi-button cbi-button-positive important',
+ 1: 'btn cbi-button cbi-button-negative important'
+ },
+ click: L.bind(function(ev, mode) { this.apply(mode == '0') }, this)
+ }).render(), ' ',
E('button', {
'class': 'cbi-button cbi-button-reset',
'click': L.bind(this.revert, this)
@@ -4162,6 +4469,26 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
},
/** @private */
+ checkConnectivityAffected: function() {
+ return L.resolveDefault(fs.exec_direct('/usr/libexec/luci-peeraddr', null, 'json')).then(L.bind(function(info) {
+ if (L.isObject(info) && Array.isArray(info.inbound_interfaces)) {
+ for (var i = 0; i < info.inbound_interfaces.length; i++) {
+ var iif = info.inbound_interfaces[i];
+
+ for (var j = 0; this.changes && this.changes.network && j < this.changes.network.length; j++) {
+ var chg = this.changes.network[j];
+
+ if (chg[0] == 'set' && chg[1] == iif && (chg[2] == 'proto' || chg[2] == 'ipaddr' || chg[2] == 'netmask'))
+ return iif;
+ }
+ }
+ }
+
+ return null;
+ }, this));
+ },
+
+ /** @private */
rollback: function(checked) {
if (checked) {
this.displayStatus('warning spinning',
@@ -4198,7 +4525,7 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
method: 'post',
timeout: L.env.apply_timeout * 1000,
query: { sid: L.env.sessionid, token: L.env.token }
- }).then(call);
+ }).then(call, call.bind(null, { status: 0 }, null, 0));
}, delay);
};
@@ -4298,35 +4625,65 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
this.displayStatus('notice spinning',
E('p', _('Starting configuration apply…')));
- request.request(L.url('admin/uci', checked ? 'apply_rollback' : 'apply_unchecked'), {
- method: 'post',
- query: { sid: L.env.sessionid, token: L.env.token }
- }).then(function(r) {
- if (r.status === (checked ? 200 : 204)) {
- var tok = null; try { tok = r.json(); } catch(e) {}
- if (checked && tok !== null && typeof(tok) === 'object' && typeof(tok.token) === 'string')
- UI.prototype.changes.confirm_auth = tok;
-
- UI.prototype.changes.confirm(checked, Date.now() + L.env.apply_rollback * 1000);
- }
- else if (checked && r.status === 204) {
- UI.prototype.changes.displayStatus('notice',
- E('p', _('There are no changes to apply')));
+ (new Promise(function(resolveFn, rejectFn) {
+ if (!checked)
+ return resolveFn(false);
+
+ UI.prototype.changes.checkConnectivityAffected().then(function(affected) {
+ if (!affected)
+ return resolveFn(true);
+
+ UI.prototype.changes.displayStatus('warning', [
+ E('h4', _('Connectivity change')),
+ E('p', _('The network access to this device could be interrupted by changing settings of the "%h" interface.').format(affected)),
+ E('p', _('If the IP address used to access LuCI changes, a <strong>manual reconnect to the new IP</strong> is required within %d seconds to confirm the settings, otherwise modifications will be reverted.').format(L.env.apply_rollback)),
+ E('div', { 'class': 'right' }, [
+ E('button', {
+ 'class': 'btn',
+ 'click': rejectFn,
+ }, [ _('Cancel') ]), ' ',
+ E('button', {
+ 'class': 'btn cbi-button-action important',
+ 'click': resolveFn.bind(null, true)
+ }, [ _('Apply with revert after connectivity loss') ]), ' ',
+ E('button', {
+ 'class': 'btn cbi-button-negative important',
+ 'click': resolveFn.bind(null, false)
+ }, [ _('Apply and keep settings') ])
+ ])
+ ]);
+ });
+ })).then(function(checked) {
+ request.request(L.url('admin/uci', checked ? 'apply_rollback' : 'apply_unchecked'), {
+ method: 'post',
+ query: { sid: L.env.sessionid, token: L.env.token }
+ }).then(function(r) {
+ if (r.status === (checked ? 200 : 204)) {
+ var tok = null; try { tok = r.json(); } catch(e) {}
+ if (checked && tok !== null && typeof(tok) === 'object' && typeof(tok.token) === 'string')
+ UI.prototype.changes.confirm_auth = tok;
+
+ UI.prototype.changes.confirm(checked, Date.now() + L.env.apply_rollback * 1000);
+ }
+ else if (checked && r.status === 204) {
+ UI.prototype.changes.displayStatus('notice',
+ E('p', _('There are no changes to apply')));
- window.setTimeout(function() {
- UI.prototype.changes.displayStatus(false);
- }, L.env.apply_display * 1000);
- }
- else {
- UI.prototype.changes.displayStatus('warning',
- E('p', _('Apply request failed with status <code>%h</code>')
- .format(r.responseText || r.statusText || r.status)));
+ window.setTimeout(function() {
+ UI.prototype.changes.displayStatus(false);
+ }, L.env.apply_display * 1000);
+ }
+ else {
+ UI.prototype.changes.displayStatus('warning',
+ E('p', _('Apply request failed with status <code>%h</code>')
+ .format(r.responseText || r.statusText || r.status)));
- window.setTimeout(function() {
- UI.prototype.changes.displayStatus(false);
- }, L.env.apply_display * 1000);
- }
- });
+ window.setTimeout(function() {
+ UI.prototype.changes.displayStatus(false);
+ }, L.env.apply_display * 1000);
+ }
+ });
+ }, this.displayStatus.bind(this, false));
},
/**
@@ -4519,6 +4876,8 @@ var UI = baseclass.extend(/** @lends LuCI.ui.prototype */ {
menu: UIMenu,
+ Table: UITable,
+
AbstractElement: UIElement,
/* Widgets */