/*
LuCI2 - OpenWrt Web Interface
- Copyright 2013 Jo-Philipp Wich <jow@openwrt.org>
+ Copyright 2013-2014 Jo-Philipp Wich <jow@openwrt.org>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
return out + str;
}
+if (!window.location.origin)
+ window.location.origin = '%s//%s%s'.format(
+ window.location.protocol,
+ window.location.hostname,
+ (window.location.port ? ':' + window.location.port : '')
+ );
+
function LuCI2()
{
- var _luci2 = this;
+ var L = this;
var Class = function() { };
_class.prototype = prototype;
_class.prototype.constructor = _class;
- _class.extend = arguments.callee;
+ _class.extend = Class.extend;
return _class;
};
plural: function(n) { return 0 + (n != 1) },
init: function() {
- if (_luci2.i18n.loaded)
+ if (L.i18n.loaded)
return;
var lang = (navigator.userLanguage || navigator.language || 'en').toLowerCase();
var langs = (lang.indexOf('-') > -1) ? [ lang, lang.split(/-/)[0] ] : [ lang ];
for (var i = 0; i < langs.length; i++)
- $.ajax('%s/i18n/base.%s.json'.format(_luci2.globals.resource, langs[i]), {
+ $.ajax('%s/i18n/base.%s.json'.format(L.globals.resource, langs[i]), {
async: false,
cache: true,
dataType: 'json',
success: function(data) {
- $.extend(_luci2.i18n.catalog, data);
+ $.extend(L.i18n.catalog, data);
- var pe = _luci2.i18n.catalog[''];
+ var pe = L.i18n.catalog[''];
if (pe)
{
- delete _luci2.i18n.catalog[''];
+ delete L.i18n.catalog[''];
try {
var pf = new Function('n', 'return 0 + (' + pe + ')');
- _luci2.i18n.plural = pf;
+ L.i18n.plural = pf;
} catch (e) { };
}
}
});
- _luci2.i18n.loaded = true;
+ L.i18n.loaded = true;
}
};
this.tr = function(msgid)
{
- _luci2.i18n.init();
+ L.i18n.init();
- var msgstr = _luci2.i18n.catalog[msgid];
+ var msgstr = L.i18n.catalog[msgid];
if (typeof(msgstr) == 'undefined')
return msgid;
this.trp = function(msgid, msgid_plural, count)
{
- _luci2.i18n.init();
+ L.i18n.init();
- var msgstr = _luci2.i18n.catalog[msgid];
+ var msgstr = L.i18n.catalog[msgid];
if (typeof(msgstr) == 'undefined')
return (count == 1) ? msgid : msgid_plural;
else if (typeof(msgstr) == 'string')
return msgstr;
else
- return msgstr[_luci2.i18n.plural(count)];
+ return msgstr[L.i18n.plural(count)];
};
this.trc = function(msgctx, msgid)
{
- _luci2.i18n.init();
+ L.i18n.init();
- var msgstr = _luci2.i18n.catalog[msgid + '\u0004' + msgctx];
+ var msgstr = L.i18n.catalog[msgid + '\u0004' + msgctx];
if (typeof(msgstr) == 'undefined')
return msgid;
this.trcp = function(msgctx, msgid, msgid_plural, count)
{
- _luci2.i18n.init();
+ L.i18n.init();
- var msgstr = _luci2.i18n.catalog[msgid + '\u0004' + msgctx];
+ var msgstr = L.i18n.catalog[msgid + '\u0004' + msgctx];
if (typeof(msgstr) == 'undefined')
return (count == 1) ? msgid : msgid_plural;
else if (typeof(msgstr) == 'string')
return msgstr;
else
- return msgstr[_luci2.i18n.plural(count)];
+ return msgstr[L.i18n.plural(count)];
};
this.setHash = function(key, value)
h += keys[i] + ':' + data[keys[i]];
}
- if (h)
+ if (h.length)
location.hash = '#' + h;
+ else
+ location.hash = '';
};
this.getHash = function(key)
return data;
};
+ this.toArray = function(x)
+ {
+ switch (typeof(x))
+ {
+ case 'number':
+ case 'boolean':
+ return [ x ];
+
+ case 'string':
+ var r = [ ];
+ var l = x.split(/\s+/);
+ for (var i = 0; i < l.length; i++)
+ if (l[i].length > 0)
+ r.push(l[i]);
+ return r;
+
+ case 'object':
+ if ($.isArray(x))
+ {
+ var r = [ ];
+ for (var i = 0; i < x.length; i++)
+ r.push(x[i]);
+ return r;
+ }
+ else if ($.isPlainObject(x))
+ {
+ var r = [ ];
+ for (var k in x)
+ if (x.hasOwnProperty(k))
+ r.push(k);
+ return r.sort();
+ }
+ }
+
+ return [ ];
+ };
+
+ this.toObject = function(x)
+ {
+ switch (typeof(x))
+ {
+ case 'number':
+ case 'boolean':
+ return { x: true };
+
+ case 'string':
+ var r = { };
+ var l = x.split(/\x+/);
+ for (var i = 0; i < l.length; i++)
+ if (l[i].length > 0)
+ r[l[i]] = true;
+ return r;
+
+ case 'object':
+ if ($.isArray(x))
+ {
+ var r = { };
+ for (var i = 0; i < x.length; i++)
+ r[x[i]] = true;
+ return r;
+ }
+ else if ($.isPlainObject(x))
+ {
+ return x;
+ }
+ }
+
+ return { };
+ };
+
+ this.filterArray = function(array, item)
+ {
+ if (!$.isArray(array))
+ return [ ];
+
+ for (var i = 0; i < array.length; i++)
+ if (array[i] === item)
+ array.splice(i--, 1);
+
+ return array;
+ };
+
+ this.toClassName = function(str, suffix)
+ {
+ var n = '';
+ var l = str.split(/[\/.]/);
+
+ for (var i = 0; i < l.length; i++)
+ if (l[i].length > 0)
+ n += l[i].charAt(0).toUpperCase() + l[i].substr(1).toLowerCase();
+
+ if (typeof(suffix) == 'string')
+ n += suffix;
+
+ return n;
+ };
+
+ this.toColor = function(str)
+ {
+ if (typeof(str) != 'string' || str.length == 0)
+ return '#CCCCCC';
+
+ if (str == 'wan')
+ return '#F09090';
+ else if (str == 'lan')
+ return '#90F090';
+
+ var i = 0, hash = 0;
+
+ while (i < str.length)
+ hash = str.charCodeAt(i++) + ((hash << 5) - hash);
+
+ var r = (hash & 0xFF) % 128;
+ var g = ((hash >> 8) & 0xFF) % 128;
+
+ var min = 0;
+ var max = 128;
+
+ if ((r + g) < 128)
+ min = 128 - r - g;
+ else
+ max = 255 - r - g;
+
+ var b = min + (((hash >> 16) & 0xFF) % (max - min));
+
+ return '#%02X%02X%02X'.format(0xFF - r, 0xFF - g, 0xFF - b);
+ };
+
+ this.parseIPv4 = function(str)
+ {
+ if ((typeof(str) != 'string' && !(str instanceof String)) ||
+ !str.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/))
+ return undefined;
+
+ var num = [ ];
+ var parts = str.split(/\./);
+
+ for (var i = 0; i < parts.length; i++)
+ {
+ var n = parseInt(parts[i], 10);
+ if (isNaN(n) || n > 255)
+ return undefined;
+
+ num.push(n);
+ }
+
+ return num;
+ };
+
+ this.parseIPv6 = function(str)
+ {
+ if ((typeof(str) != 'string' && !(str instanceof String)) ||
+ !str.match(/^[a-fA-F0-9:]+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})?$/))
+ return undefined;
+
+ var parts = str.split(/::/);
+ if (parts.length == 0 || parts.length > 2)
+ return undefined;
+
+ var lnum = [ ];
+ if (parts[0].length > 0)
+ {
+ var left = parts[0].split(/:/);
+ for (var i = 0; i < left.length; i++)
+ {
+ var n = parseInt(left[i], 16);
+ if (isNaN(n))
+ return undefined;
+
+ lnum.push((n / 256) >> 0);
+ lnum.push(n % 256);
+ }
+ }
+
+ var rnum = [ ];
+ if (parts.length > 1 && parts[1].length > 0)
+ {
+ var right = parts[1].split(/:/);
+
+ for (var i = 0; i < right.length; i++)
+ {
+ if (right[i].indexOf('.') > 0)
+ {
+ var addr = L.parseIPv4(right[i]);
+ if (!addr)
+ return undefined;
+
+ rnum.push.apply(rnum, addr);
+ continue;
+ }
+
+ var n = parseInt(right[i], 16);
+ if (isNaN(n))
+ return undefined;
+
+ rnum.push((n / 256) >> 0);
+ rnum.push(n % 256);
+ }
+ }
+
+ if (rnum.length > 0 && (lnum.length + rnum.length) > 15)
+ return undefined;
+
+ var num = [ ];
+
+ num.push.apply(num, lnum);
+
+ for (var i = 0; i < (16 - lnum.length - rnum.length); i++)
+ num.push(0);
+
+ num.push.apply(num, rnum);
+
+ if (num.length > 16)
+ return undefined;
+
+ return num;
+ };
+
+ this.isNetmask = function(addr)
+ {
+ if (!$.isArray(addr))
+ return false;
+
+ var c;
+
+ for (c = 0; (c < addr.length) && (addr[c] == 255); c++);
+
+ if (c == addr.length)
+ return true;
+
+ if ((addr[c] == 254) || (addr[c] == 252) || (addr[c] == 248) ||
+ (addr[c] == 240) || (addr[c] == 224) || (addr[c] == 192) ||
+ (addr[c] == 128) || (addr[c] == 0))
+ {
+ for (c++; (c < addr.length) && (addr[c] == 0); c++);
+
+ if (c == addr.length)
+ return true;
+ }
+
+ return false;
+ };
+
this.globals = {
- timeout: 3000,
+ timeout: 15000,
resource: '/luci2',
sid: '00000000000000000000000000000000'
};
data: JSON.stringify(req),
dataType: 'json',
type: 'POST',
- timeout: _luci2.globals.timeout
- }).then(cb);
+ timeout: L.globals.timeout,
+ _rpc_req: req
+ }).then(cb, cb);
},
_list_cb: function(msg)
{
+ var list = msg.result;
+
/* verify message frame */
- if (typeof(msg) != 'object' || msg.jsonrpc != '2.0' || !msg.id)
- throw 'Invalid JSON response';
+ if (typeof(msg) != 'object' || msg.jsonrpc != '2.0' || !msg.id || !$.isArray(list))
+ list = [ ];
- return msg.result;
+ return $.Deferred().resolveWith(this, [ list ]);
},
_call_cb: function(msg)
{
var data = [ ];
var type = Object.prototype.toString;
+ var reqs = this._rpc_req;
- if (!$.isArray(msg))
+ if (!$.isArray(reqs))
+ {
msg = [ msg ];
+ reqs = [ reqs ];
+ }
for (var i = 0; i < msg.length; i++)
{
- /* verify message frame */
- if (typeof(msg[i]) != 'object' || msg[i].jsonrpc != '2.0' || !msg[i].id)
- throw 'Invalid JSON response';
-
/* fetch related request info */
- var req = _luci2.rpc._requests[msg[i].id];
+ var req = L.rpc._requests[reqs[i].id];
if (typeof(req) != 'object')
throw 'No related request for JSON response';
/* fetch response attribute and verify returned type */
var ret = undefined;
- if ($.isArray(msg[i].result) && msg[i].result[0] == 0)
- ret = (msg[i].result.length > 1) ? msg[i].result[1] : msg[i].result[0];
+ /* verify message frame */
+ if (typeof(msg[i]) == 'object' && msg[i].jsonrpc == '2.0')
+ if ($.isArray(msg[i].result) && msg[i].result[0] == 0)
+ ret = (msg[i].result.length > 1) ? msg[i].result[1] : msg[i].result[0];
if (req.expect)
{
if (typeof(ret) != 'undefined' && key != '')
ret = ret[key];
- if (type.call(ret) != type.call(req.expect[key]))
+ if (typeof(ret) == 'undefined' || type.call(ret) != type.call(req.expect[key]))
ret = req.expect[key];
break;
{
req.priv[0] = ret;
req.priv[1] = req.params;
- ret = req.filter.apply(_luci2.rpc, req.priv);
+ ret = req.filter.apply(L.rpc, req.priv);
}
/* store response data */
data = ret;
/* delete request object */
- delete _luci2.rpc._requests[msg[i].id];
+ delete L.rpc._requests[reqs[i].id];
}
- return data;
+ return $.Deferred().resolveWith(this, [ data ]);
},
list: function()
flush: function()
{
if (!$.isArray(this._batch))
- return _luci2.deferrable([ ]);
+ return L.deferrable([ ]);
var req = this._batch;
delete this._batch;
id: _rpc._id++,
method: 'call',
params: [
- _luci2.globals.sid,
+ L.globals.sid,
options.object,
options.method,
params
if ($.isArray(_rpc._batch))
{
req.index = _rpc._batch.push(msg) - 1;
- return _luci2.deferrable(msg);
+ return L.deferrable(msg);
}
/* call rpc */
}
};
- this.uci = {
+ this.UCIContext = Class.extend({
- writable: function()
+ init: function()
{
- return _luci2.session.access('ubus', 'uci', 'commit');
+ this.state = {
+ newidx: 0,
+ values: { },
+ creates: { },
+ changes: { },
+ deletes: { },
+ reorder: { }
+ };
},
- add: _luci2.rpc.declare({
+ callLoad: L.rpc.declare({
object: 'uci',
- method: 'add',
- params: [ 'config', 'type', 'name', 'values' ],
- expect: { section: '' }
+ method: 'get',
+ params: [ 'config' ],
+ expect: { values: { } }
}),
- apply: function()
- {
-
- },
-
- changes: _luci2.rpc.declare({
+ callOrder: L.rpc.declare({
object: 'uci',
- method: 'changes',
- params: [ 'config' ],
- expect: { changes: [ ] }
+ method: 'order',
+ params: [ 'config', 'sections' ]
}),
- commit: _luci2.rpc.declare({
+ callAdd: L.rpc.declare({
object: 'uci',
- method: 'commit',
- params: [ 'config' ]
+ method: 'add',
+ params: [ 'config', 'type', 'name', 'values' ],
+ expect: { section: '' }
}),
- _delete_one: _luci2.rpc.declare({
+ callSet: L.rpc.declare({
object: 'uci',
- method: 'delete',
- params: [ 'config', 'section', 'option' ]
+ method: 'set',
+ params: [ 'config', 'section', 'values' ]
}),
- _delete_multiple: _luci2.rpc.declare({
+ callDelete: L.rpc.declare({
object: 'uci',
method: 'delete',
params: [ 'config', 'section', 'options' ]
}),
- 'delete': function(config, section, option)
- {
- if ($.isArray(option))
- return this._delete_multiple(config, section, option);
- else
- return this._delete_one(config, section, option);
- },
-
- delete_all: _luci2.rpc.declare({
+ callApply: L.rpc.declare({
object: 'uci',
- method: 'delete',
- params: [ 'config', 'type', 'match' ]
+ method: 'apply',
+ params: [ 'timeout', 'rollback' ]
}),
- _foreach: _luci2.rpc.declare({
+ callConfirm: L.rpc.declare({
object: 'uci',
- method: 'get',
- params: [ 'config', 'type' ],
- expect: { values: { } }
+ method: 'confirm'
}),
- foreach: function(config, type, cb)
+ createSID: function(conf)
{
- return this._foreach(config, type).then(function(sections) {
- for (var s in sections)
- cb(sections[s]);
- });
- },
+ var v = this.state.values;
+ var n = this.state.creates;
+ var sid;
- get: _luci2.rpc.declare({
- object: 'uci',
- method: 'get',
- params: [ 'config', 'section', 'option' ],
- expect: { '': { } },
- filter: function(data, params) {
- if (typeof(params.option) == 'undefined')
- return data.values ? data.values['.type'] : undefined;
- else
- return data.value;
- }
- }),
+ do {
+ sid = "new%06x".format(Math.random() * 0xFFFFFF);
+ } while ((n[conf] && n[conf][sid]) || (v[conf] && v[conf][sid]));
- get_all: _luci2.rpc.declare({
- object: 'uci',
- method: 'get',
- params: [ 'config', 'section' ],
- expect: { values: { } },
- filter: function(data, params) {
- if (typeof(params.section) == 'string')
- data['.section'] = params.section;
- else if (typeof(params.config) == 'string')
- data['.package'] = params.config;
- return data;
- }
- }),
+ return sid;
+ },
- get_first: function(config, type, option)
+ reorderSections: function()
{
- return this._foreach(config, type).then(function(sections) {
- for (var s in sections)
- {
- var val = (typeof(option) == 'string') ? sections[s][option] : sections[s]['.name'];
-
- if (typeof(val) != 'undefined')
- return val;
- }
+ var v = this.state.values;
+ var n = this.state.creates;
+ var r = this.state.reorder;
- return undefined;
- });
- },
+ if ($.isEmptyObject(r))
+ return L.deferrable();
- section: _luci2.rpc.declare({
- object: 'uci',
- method: 'add',
- params: [ 'config', 'type', 'name', 'values' ],
- expect: { section: '' }
- }),
+ L.rpc.batch();
- _set: _luci2.rpc.declare({
- object: 'uci',
- method: 'set',
- params: [ 'config', 'section', 'values' ]
- }),
+ /*
+ gather all created and existing sections, sort them according
+ to their index value and issue an uci order call
+ */
+ for (var c in r)
+ {
+ var o = [ ];
- set: function(config, section, option, value)
- {
- if (typeof(value) == 'undefined' && typeof(option) == 'string')
- return this.section(config, section, option); /* option -> type */
- else if ($.isPlainObject(option))
- return this._set(config, section, option); /* option -> values */
+ if (n[c])
+ for (var s in n[c])
+ o.push(n[c][s]);
- var values = { };
- values[option] = value;
+ for (var s in v[c])
+ o.push(v[c][s]);
- return this._set(config, section, values);
- },
+ if (o.length > 0)
+ {
+ o.sort(function(a, b) {
+ return (a['.index'] - b['.index']);
+ });
- order: _luci2.rpc.declare({
- object: 'uci',
- method: 'order',
- params: [ 'config', 'sections' ]
- })
- };
+ var sids = [ ];
- this.network = {
- listNetworkNames: function() {
- return _luci2.rpc.list('network.interface.*').then(function(list) {
- var names = [ ];
- for (var name in list)
- if (name != 'network.interface.loopback')
- names.push(name.substring(18));
- names.sort();
- return names;
- });
- },
+ for (var i = 0; i < o.length; i++)
+ sids.push(o[i]['.name']);
- listDeviceNames: _luci2.rpc.declare({
- object: 'network.device',
- method: 'status',
- expect: { '': { } },
- filter: function(data) {
- var names = [ ];
- for (var name in data)
- if (name != 'lo')
- names.push(name);
- names.sort();
- return names;
+ this.callOrder(c, sids);
+ }
}
- }),
- getNetworkStatus: function()
+ this.state.reorder = { };
+ return L.rpc.flush();
+ },
+
+ load: function(packages)
{
- var nets = [ ];
- var devs = { };
+ var self = this;
+ var seen = { };
+ var pkgs = [ ];
- return this.listNetworkNames().then(function(names) {
- _luci2.rpc.batch();
+ if (!$.isArray(packages))
+ packages = [ packages ];
- for (var i = 0; i < names.length; i++)
- _luci2.network.getInterfaceStatus(names[i]);
+ L.rpc.batch();
- return _luci2.rpc.flush();
- }).then(function(networks) {
- for (var i = 0; i < networks.length; i++)
+ for (var i = 0; i < packages.length; i++)
+ if (!seen[packages[i]] && !self.state.values[packages[i]])
{
- var net = nets[i] = networks[i];
- var dev = net.l3_device || net.l2_device;
- if (dev)
- net.device = devs[dev] = { };
+ pkgs.push(packages[i]);
+ seen[packages[i]] = true;
+ self.callLoad(packages[i]);
}
- _luci2.rpc.batch();
-
- for (var dev in devs)
- _luci2.network.listDeviceNamestatus(dev);
-
- return _luci2.rpc.flush();
- }).then(function(devices) {
- _luci2.rpc.batch();
-
- for (var i = 0; i < devices.length; i++)
- {
- var brm = devices[i]['bridge-members'];
- delete devices[i]['bridge-members'];
+ return L.rpc.flush().then(function(responses) {
+ for (var i = 0; i < responses.length; i++)
+ self.state.values[pkgs[i]] = responses[i];
- $.extend(devs[devices[i]['device']], devices[i]);
+ return pkgs;
+ });
+ },
- if (!brm)
- continue;
+ unload: function(packages)
+ {
+ if (!$.isArray(packages))
+ packages = [ packages ];
- devs[devices[i]['device']].subdevices = [ ];
+ for (var i = 0; i < packages.length; i++)
+ {
+ delete this.state.values[packages[i]];
+ delete this.state.creates[packages[i]];
+ delete this.state.changes[packages[i]];
+ delete this.state.deletes[packages[i]];
+ }
+ },
- for (var j = 0; j < brm.length; j++)
- {
- if (!devs[brm[j]])
- {
- devs[brm[j]] = { };
- _luci2.network.listDeviceNamestatus(brm[j]);
- }
+ add: function(conf, type, name)
+ {
+ var n = this.state.creates;
+ var sid = name || this.createSID(conf);
- devs[devices[i]['device']].subdevices[j] = devs[brm[j]];
- }
- }
+ if (!n[conf])
+ n[conf] = { };
- return _luci2.rpc.flush();
- }).then(function(subdevices) {
- for (var i = 0; i < subdevices.length; i++)
- $.extend(devs[subdevices[i]['device']], subdevices[i]);
+ n[conf][sid] = {
+ '.type': type,
+ '.name': sid,
+ '.create': name,
+ '.anonymous': !name,
+ '.index': 1000 + this.state.newidx++
+ };
- _luci2.rpc.batch();
+ return sid;
+ },
- for (var dev in devs)
- _luci2.wireless.getDeviceStatus(dev);
+ remove: function(conf, sid)
+ {
+ var n = this.state.creates;
+ var c = this.state.changes;
+ var d = this.state.deletes;
- return _luci2.rpc.flush();
- }).then(function(wifidevices) {
- for (var i = 0; i < wifidevices.length; i++)
- if (wifidevices[i])
- devs[wifidevices[i]['device']].wireless = wifidevices[i];
+ /* requested deletion of a just created section */
+ if (n[conf] && n[conf][sid])
+ {
+ delete n[conf][sid];
+ }
+ else
+ {
+ if (c[conf])
+ delete c[conf][sid];
- nets.sort(function(a, b) {
- if (a['interface'] < b['interface'])
- return -1;
- else if (a['interface'] > b['interface'])
- return 1;
- else
- return 0;
- });
+ if (!d[conf])
+ d[conf] = { };
- return nets;
- });
+ d[conf][sid] = true;
+ }
},
- findWanInterfaces: function(cb)
+ sections: function(conf, type, cb)
{
- return this.listNetworkNames().then(function(names) {
- _luci2.rpc.batch();
+ var sa = [ ];
+ var v = this.state.values[conf];
+ var n = this.state.creates[conf];
+ var c = this.state.changes[conf];
+ var d = this.state.deletes[conf];
- for (var i = 0; i < names.length; i++)
- _luci2.network.getInterfaceStatus(names[i]);
+ if (!v)
+ return sa;
- return _luci2.rpc.flush();
- }).then(function(interfaces) {
- var rv = [ undefined, undefined ];
+ for (var s in v)
+ if (!d || d[s] !== true)
+ if (!type || v[s]['.type'] == type)
+ sa.push($.extend({ }, v[s], c ? c[s] : undefined));
- for (var i = 0; i < interfaces.length; i++)
- {
- for (var j = 0; j < interfaces[i].route.length; j++)
- {
- var rt = interfaces[i].route[j];
+ if (n)
+ for (var s in n)
+ if (!type || n[s]['.type'] == type)
+ sa.push(n[s]);
- if (typeof(rt.table) != 'undefined')
- continue;
+ sa.sort(function(a, b) {
+ return a['.index'] - b['.index'];
+ });
- if (rt.target == '0.0.0.0' && rt.mask == 0)
- rv[0] = interfaces[i];
- else if (rt.target == '::' && rt.mask == 0)
- rv[1] = interfaces[i];
- }
- }
+ for (var i = 0; i < sa.length; i++)
+ sa[i]['.index'] = i;
- return rv;
- });
+ if (typeof(cb) == 'function')
+ for (var i = 0; i < sa.length; i++)
+ cb.call(this, sa[i], sa[i]['.name']);
+
+ return sa;
},
- getDHCPLeases: _luci2.rpc.declare({
- object: 'luci2.network',
- method: 'dhcp_leases',
- expect: { leases: [ ] }
- }),
+ get: function(conf, sid, opt)
+ {
+ var v = this.state.values;
+ var n = this.state.creates;
+ var c = this.state.changes;
+ var d = this.state.deletes;
- getDHCPv6Leases: _luci2.rpc.declare({
- object: 'luci2.network',
- method: 'dhcp6_leases',
- expect: { leases: [ ] }
- }),
+ if (typeof(sid) == 'undefined')
+ return undefined;
- getRoutes: _luci2.rpc.declare({
- object: 'luci2.network',
- method: 'routes',
- expect: { routes: [ ] }
- }),
+ /* requested option in a just created section */
+ if (n[conf] && n[conf][sid])
+ {
+ if (!n[conf])
+ return undefined;
- getIPv6Routes: _luci2.rpc.declare({
- object: 'luci2.network',
- method: 'routes',
- expect: { routes: [ ] }
- }),
+ if (typeof(opt) == 'undefined')
+ return n[conf][sid];
- getARPTable: _luci2.rpc.declare({
- object: 'luci2.network',
- method: 'arp_table',
- expect: { entries: [ ] }
- }),
+ return n[conf][sid][opt];
+ }
- getInterfaceStatus: _luci2.rpc.declare({
- object: 'network.interface',
- method: 'status',
- params: [ 'interface' ],
- expect: { '': { } },
- filter: function(data, params) {
- data['interface'] = params['interface'];
- data['l2_device'] = data['device'];
- delete data['device'];
- return data;
+ /* requested an option value */
+ if (typeof(opt) != 'undefined')
+ {
+ /* check whether option was deleted */
+ if (d[conf] && d[conf][sid])
+ {
+ if (d[conf][sid] === true)
+ return undefined;
+
+ for (var i = 0; i < d[conf][sid].length; i++)
+ if (d[conf][sid][i] == opt)
+ return undefined;
+ }
+
+ /* check whether option was changed */
+ if (c[conf] && c[conf][sid] && typeof(c[conf][sid][opt]) != 'undefined')
+ return c[conf][sid][opt];
+
+ /* return base value */
+ if (v[conf] && v[conf][sid])
+ return v[conf][sid][opt];
+
+ return undefined;
}
- }),
- listDeviceNamestatus: _luci2.rpc.declare({
- object: 'network.device',
- method: 'status',
- params: [ 'name' ],
- expect: { '': { } },
- filter: function(data, params) {
- data['device'] = params['name'];
- return data;
+ /* requested an entire section */
+ if (v[conf])
+ return v[conf][sid];
+
+ return undefined;
+ },
+
+ set: function(conf, sid, opt, val)
+ {
+ var v = this.state.values;
+ var n = this.state.creates;
+ var c = this.state.changes;
+ var d = this.state.deletes;
+
+ if (typeof(sid) == 'undefined' ||
+ typeof(opt) == 'undefined' ||
+ opt.charAt(0) == '.')
+ return;
+
+ if (n[conf] && n[conf][sid])
+ {
+ if (typeof(val) != 'undefined')
+ n[conf][sid][opt] = val;
+ else
+ delete n[conf][sid][opt];
+ }
+ else if (typeof(val) != 'undefined')
+ {
+ /* do not set within deleted section */
+ if (d[conf] && d[conf][sid] === true)
+ return;
+
+ /* only set in existing sections */
+ if (!v[conf] || !v[conf][sid])
+ return;
+
+ if (!c[conf])
+ c[conf] = { };
+
+ if (!c[conf][sid])
+ c[conf][sid] = { };
+
+ /* undelete option */
+ if (d[conf] && d[conf][sid])
+ d[conf][sid] = L.filterArray(d[conf][sid], opt);
+
+ c[conf][sid][opt] = val;
+ }
+ else
+ {
+ /* only delete in existing sections */
+ if (!v[conf] || !v[conf][sid])
+ return;
+
+ if (!d[conf])
+ d[conf] = { };
+
+ if (!d[conf][sid])
+ d[conf][sid] = [ ];
+
+ if (d[conf][sid] !== true)
+ d[conf][sid].push(opt);
}
+ },
+
+ unset: function(conf, sid, opt)
+ {
+ return this.set(conf, sid, opt, undefined);
+ },
+
+ get_first: function(conf, type, opt)
+ {
+ var sid = undefined;
+
+ L.uci.sections(conf, type, function(s) {
+ if (typeof(sid) != 'string')
+ sid = s['.name'];
+ });
+
+ return this.get(conf, sid, opt);
+ },
+
+ set_first: function(conf, type, opt, val)
+ {
+ var sid = undefined;
+
+ L.uci.sections(conf, type, function(s) {
+ if (typeof(sid) != 'string')
+ sid = s['.name'];
+ });
+
+ return this.set(conf, sid, opt, val);
+ },
+
+ unset_first: function(conf, type, opt)
+ {
+ return this.set_first(conf, type, opt, undefined);
+ },
+
+ swap: function(conf, sid1, sid2)
+ {
+ var s1 = this.get(conf, sid1);
+ var s2 = this.get(conf, sid2);
+ var n1 = s1 ? s1['.index'] : NaN;
+ var n2 = s2 ? s2['.index'] : NaN;
+
+ if (isNaN(n1) || isNaN(n2))
+ return false;
+
+ s1['.index'] = n2;
+ s2['.index'] = n1;
+
+ this.state.reorder[conf] = true;
+
+ return true;
+ },
+
+ save: function()
+ {
+ L.rpc.batch();
+
+ var v = this.state.values;
+ var n = this.state.creates;
+ var c = this.state.changes;
+ var d = this.state.deletes;
+
+ var self = this;
+ var snew = [ ];
+ var pkgs = { };
+
+ if (n)
+ for (var conf in n)
+ {
+ for (var sid in n[conf])
+ {
+ var r = {
+ config: conf,
+ values: { }
+ };
+
+ for (var k in n[conf][sid])
+ {
+ if (k == '.type')
+ r.type = n[conf][sid][k];
+ else if (k == '.create')
+ r.name = n[conf][sid][k];
+ else if (k.charAt(0) != '.')
+ r.values[k] = n[conf][sid][k];
+ }
+
+ snew.push(n[conf][sid]);
+
+ self.callAdd(r.config, r.type, r.name, r.values);
+ }
+
+ pkgs[conf] = true;
+ }
+
+ if (c)
+ for (var conf in c)
+ {
+ for (var sid in c[conf])
+ self.callSet(conf, sid, c[conf][sid]);
+
+ pkgs[conf] = true;
+ }
+
+ if (d)
+ for (var conf in d)
+ {
+ for (var sid in d[conf])
+ {
+ var o = d[conf][sid];
+ self.callDelete(conf, sid, (o === true) ? undefined : o);
+ }
+
+ pkgs[conf] = true;
+ }
+
+ return L.rpc.flush().then(function(responses) {
+ /*
+ array "snew" holds references to the created uci sections,
+ use it to assign the returned names of the new sections
+ */
+ for (var i = 0; i < snew.length; i++)
+ snew[i]['.name'] = responses[i];
+
+ return self.reorderSections();
+ }).then(function() {
+ pkgs = L.toArray(pkgs);
+
+ self.unload(pkgs);
+
+ return self.load(pkgs);
+ });
+ },
+
+ apply: function(timeout)
+ {
+ var self = this;
+ var date = new Date();
+ var deferred = $.Deferred();
+
+ if (typeof(timeout) != 'number' || timeout < 1)
+ timeout = 10;
+
+ self.callApply(timeout, true).then(function(rv) {
+ if (rv != 0)
+ {
+ deferred.rejectWith(self, [ rv ]);
+ return;
+ }
+
+ var try_deadline = date.getTime() + 1000 * timeout;
+ var try_confirm = function()
+ {
+ return self.callConfirm().then(function(rv) {
+ if (rv != 0)
+ {
+ if (date.getTime() < try_deadline)
+ window.setTimeout(try_confirm, 250);
+ else
+ deferred.rejectWith(self, [ rv ]);
+
+ return;
+ }
+
+ deferred.resolveWith(self, [ rv ]);
+ });
+ };
+
+ window.setTimeout(try_confirm, 1000);
+ });
+
+ return deferred;
+ },
+
+ changes: L.rpc.declare({
+ object: 'uci',
+ method: 'changes',
+ expect: { changes: { } }
}),
- getConntrackCount: _luci2.rpc.declare({
- object: 'luci2.network',
- method: 'conntrack_count',
- expect: { '': { count: 0, limit: 0 } }
- })
- };
+ readable: function(conf)
+ {
+ return L.session.hasACL('uci', conf, 'read');
+ },
+
+ writable: function(conf)
+ {
+ return L.session.hasACL('uci', conf, 'write');
+ }
+ });
+
+ this.uci = new this.UCIContext();
this.wireless = {
- listDeviceNames: _luci2.rpc.declare({
+ listDeviceNames: L.rpc.declare({
object: 'iwinfo',
method: 'devices',
expect: { 'devices': [ ] },
}
}),
- getDeviceStatus: _luci2.rpc.declare({
+ getDeviceStatus: L.rpc.declare({
object: 'iwinfo',
method: 'info',
params: [ 'device' ],
}
}),
- getAssocList: _luci2.rpc.declare({
+ getAssocList: L.rpc.declare({
object: 'iwinfo',
method: 'assoclist',
params: [ 'device' ],
getWirelessStatus: function() {
return this.listDeviceNames().then(function(names) {
- _luci2.rpc.batch();
+ L.rpc.batch();
for (var i = 0; i < names.length; i++)
- _luci2.wireless.getDeviceStatus(names[i]);
+ L.wireless.getDeviceStatus(names[i]);
- return _luci2.rpc.flush();
+ return L.rpc.flush();
}).then(function(networks) {
var rv = { };
getAssocLists: function()
{
return this.listDeviceNames().then(function(names) {
- _luci2.rpc.batch();
+ L.rpc.batch();
for (var i = 0; i < names.length; i++)
- _luci2.wireless.getAssocList(names[i]);
+ L.wireless.getAssocList(names[i]);
- return _luci2.rpc.flush();
+ return L.rpc.flush();
}).then(function(assoclists) {
var rv = [ ];
}
if (!enc || !enc.enabled)
- return _luci2.tr('None');
+ return L.tr('None');
if (enc.wep)
{
if (enc.wep.length == 2)
- return _luci2.tr('WEP Open/Shared') + ' (%s)'.format(format_list(enc.ciphers, ', '));
+ return L.tr('WEP Open/Shared') + ' (%s)'.format(format_list(enc.ciphers, ', '));
else if (enc.wep[0] == 'shared')
- return _luci2.tr('WEP Shared Auth') + ' (%s)'.format(format_list(enc.ciphers, ', '));
+ return L.tr('WEP Shared Auth') + ' (%s)'.format(format_list(enc.ciphers, ', '));
else
- return _luci2.tr('WEP Open System') + ' (%s)'.format(format_list(enc.ciphers, ', '));
+ return L.tr('WEP Open System') + ' (%s)'.format(format_list(enc.ciphers, ', '));
}
else if (enc.wpa)
{
if (enc.wpa.length == 2)
- return _luci2.tr('mixed WPA/WPA2') + ' %s (%s)'.format(
+ return L.tr('mixed WPA/WPA2') + ' %s (%s)'.format(
format_list(enc.authentication, '/'),
format_list(enc.ciphers, ', ')
);
);
}
- return _luci2.tr('Unknown');
+ return L.tr('Unknown');
}
};
- this.system = {
- getSystemInfo: _luci2.rpc.declare({
- object: 'system',
- method: 'info',
- expect: { '': { } }
- }),
+ this.firewall = {
+ getZoneColor: function(zone)
+ {
+ if ($.isPlainObject(zone))
+ zone = zone.name;
- getBoardInfo: _luci2.rpc.declare({
- object: 'system',
- method: 'board',
- expect: { '': { } }
- }),
+ if (zone == 'lan')
+ return '#90f090';
+ else if (zone == 'wan')
+ return '#f09090';
- getDiskInfo: _luci2.rpc.declare({
- object: 'luci2.system',
- method: 'diskfree',
- expect: { '': { } }
- }),
+ for (var i = 0, hash = 0;
+ i < zone.length;
+ hash = zone.charCodeAt(i++) + ((hash << 5) - hash));
- getInfo: function(cb)
+ for (var i = 0, color = '#';
+ i < 3;
+ color += ('00' + ((hash >> i++ * 8) & 0xFF).tostring(16)).slice(-2));
+
+ return color;
+ },
+
+ findZoneByNetwork: function(network)
{
- _luci2.rpc.batch();
+ var self = this;
+ var zone = undefined;
- this.getSystemInfo();
- this.getBoardInfo();
- this.getDiskInfo();
+ return L.uci.sections('firewall', 'zone', function(z) {
+ if (!z.name || !z.network)
+ return;
- return _luci2.rpc.flush().then(function(info) {
- var rv = { };
+ if (!$.isArray(z.network))
+ z.network = z.network.split(/\s+/);
- $.extend(rv, info[0]);
- $.extend(rv, info[1]);
- $.extend(rv, info[2]);
+ for (var i = 0; i < z.network.length; i++)
+ {
+ if (z.network[i] == network)
+ {
+ zone = z;
+ break;
+ }
+ }
+ }).then(function() {
+ if (zone)
+ zone.color = self.getZoneColor(zone);
- return rv;
+ return zone;
});
- },
+ }
+ };
- getProcessList: _luci2.rpc.declare({
- object: 'luci2.system',
- method: 'process_list',
- expect: { processes: [ ] },
- filter: function(data) {
- data.sort(function(a, b) { return a.pid - b.pid });
- return data;
- }
- }),
+ this.NetworkModel = {
+ deviceBlacklist: [
+ /^gre[0-9]+$/,
+ /^gretap[0-9]+$/,
+ /^ifb[0-9]+$/,
+ /^ip6tnl[0-9]+$/,
+ /^sit[0-9]+$/,
+ /^wlan[0-9]+\.sta[0-9]+$/
+ ],
+
+ rpcCacheFunctions: [
+ 'protolist', 0, L.rpc.declare({
+ object: 'network',
+ method: 'get_proto_handlers',
+ expect: { '': { } }
+ }),
+ 'ifstate', 1, L.rpc.declare({
+ object: 'network.interface',
+ method: 'dump',
+ expect: { 'interface': [ ] }
+ }),
+ 'devstate', 2, L.rpc.declare({
+ object: 'network.device',
+ method: 'status',
+ expect: { '': { } }
+ }),
+ 'wifistate', 0, L.rpc.declare({
+ object: 'network.wireless',
+ method: 'status',
+ expect: { '': { } }
+ }),
+ 'bwstate', 2, L.rpc.declare({
+ object: 'luci2.network.bwmon',
+ method: 'statistics',
+ expect: { 'statistics': { } }
+ }),
+ 'devlist', 2, L.rpc.declare({
+ object: 'luci2.network',
+ method: 'device_list',
+ expect: { 'devices': [ ] }
+ }),
+ 'swlist', 0, L.rpc.declare({
+ object: 'luci2.network',
+ method: 'switch_list',
+ expect: { 'switches': [ ] }
+ })
+ ],
+
+ loadProtocolHandler: function(proto)
+ {
+ var url = L.globals.resource + '/proto/' + proto + '.js';
+ var self = L.NetworkModel;
+
+ var def = $.Deferred();
+
+ $.ajax(url, {
+ method: 'GET',
+ cache: true,
+ dataType: 'text'
+ }).then(function(data) {
+ try {
+ var protoConstructorSource = (
+ '(function(L, $) { ' +
+ 'return %s' +
+ '})(L, $);\n\n' +
+ '//@ sourceURL=%s'
+ ).format(data, url);
- getSystemLog: _luci2.rpc.declare({
- object: 'luci2.system',
- method: 'syslog',
- expect: { log: '' }
- }),
+ var protoClass = eval(protoConstructorSource);
- getKernelLog: _luci2.rpc.declare({
- object: 'luci2.system',
- method: 'dmesg',
- expect: { log: '' }
- }),
+ self.protocolHandlers[proto] = new protoClass();
+ }
+ catch(e) {
+ alert('Unable to instantiate proto "%s": %s'.format(url, e));
+ };
+
+ def.resolve();
+ }).fail(function() {
+ def.resolve();
+ });
- getZoneInfo: function(cb)
+ return def;
+ },
+
+ loadProtocolHandlers: function()
{
- return $.getJSON(_luci2.globals.resource + '/zoneinfo.json', cb);
+ var self = L.NetworkModel;
+ var deferreds = [
+ self.loadProtocolHandler('none')
+ ];
+
+ for (var proto in self.rpcCache.protolist)
+ deferreds.push(self.loadProtocolHandler(proto));
+
+ return $.when.apply($, deferreds);
},
- sendSignal: _luci2.rpc.declare({
- object: 'luci2.system',
- method: 'process_signal',
- params: [ 'pid', 'signal' ],
- filter: function(data) {
- return (data == 0);
- }
+ callSwitchInfo: L.rpc.declare({
+ object: 'luci2.network',
+ method: 'switch_info',
+ params: [ 'switch' ],
+ expect: { 'info': { } }
}),
- initList: _luci2.rpc.declare({
- object: 'luci2.system',
- method: 'init_list',
- expect: { initscripts: [ ] },
- filter: function(data) {
- data.sort(function(a, b) { return (a.start || 0) - (b.start || 0) });
- return data;
- }
- }),
+ callSwitchInfoCallback: function(responses) {
+ var self = L.NetworkModel;
+ var swlist = self.rpcCache.swlist;
+ var swstate = self.rpcCache.swstate = { };
- initEnabled: function(init, cb)
+ for (var i = 0; i < responses.length; i++)
+ swstate[swlist[i]] = responses[i];
+ },
+
+ loadCacheCallback: function(level)
{
- return this.initList().then(function(list) {
- for (var i = 0; i < list.length; i++)
- if (list[i].name == init)
- return !!list[i].enabled;
+ var self = L.NetworkModel;
+ var name = '_fetch_cache_cb_' + level;
- return false;
+ return self[name] || (
+ self[name] = function(responses)
+ {
+ for (var i = 0; i < self.rpcCacheFunctions.length; i += 3)
+ if (!level || self.rpcCacheFunctions[i + 1] == level)
+ self.rpcCache[self.rpcCacheFunctions[i]] = responses.shift();
+
+ if (!level)
+ {
+ L.rpc.batch();
+
+ for (var i = 0; i < self.rpcCache.swlist.length; i++)
+ self.callSwitchInfo(self.rpcCache.swlist[i]);
+
+ return L.rpc.flush().then(self.callSwitchInfoCallback);
+ }
+
+ return L.deferrable();
+ }
+ );
+ },
+
+ loadCache: function(level)
+ {
+ var self = L.NetworkModel;
+
+ return L.uci.load(['network', 'wireless']).then(function() {
+ L.rpc.batch();
+
+ for (var i = 0; i < self.rpcCacheFunctions.length; i += 3)
+ if (!level || self.rpcCacheFunctions[i + 1] == level)
+ self.rpcCacheFunctions[i + 2]();
+
+ return L.rpc.flush().then(self.loadCacheCallback(level || 0));
});
},
- initRun: _luci2.rpc.declare({
- object: 'luci2.system',
- method: 'init_action',
- params: [ 'name', 'action' ],
- filter: function(data) {
- return (data == 0);
+ isBlacklistedDevice: function(dev)
+ {
+ for (var i = 0; i < this.deviceBlacklist.length; i++)
+ if (dev.match(this.deviceBlacklist[i]))
+ return true;
+
+ return false;
+ },
+
+ sortDevicesCallback: function(a, b)
+ {
+ if (a.options.kind < b.options.kind)
+ return -1;
+ else if (a.options.kind > b.options.kind)
+ return 1;
+
+ if (a.options.name < b.options.name)
+ return -1;
+ else if (a.options.name > b.options.name)
+ return 1;
+
+ return 0;
+ },
+
+ getDeviceObject: function(ifname)
+ {
+ var alias = (ifname.charAt(0) == '@');
+ return this.deviceObjects[ifname] || (
+ this.deviceObjects[ifname] = {
+ ifname: ifname,
+ kind: alias ? 'alias' : 'ethernet',
+ type: alias ? 0 : 1,
+ up: false,
+ changed: { }
+ }
+ );
+ },
+
+ getInterfaceObject: function(name)
+ {
+ return this.interfaceObjects[name] || (
+ this.interfaceObjects[name] = {
+ name: name,
+ proto: this.protocolHandlers.none,
+ changed: { }
+ }
+ );
+ },
+
+ loadDevicesCallback: function()
+ {
+ var self = L.NetworkModel;
+ var wificount = { };
+
+ for (var ifname in self.rpcCache.devstate)
+ {
+ if (self.isBlacklistedDevice(ifname))
+ continue;
+
+ var dev = self.rpcCache.devstate[ifname];
+ var entry = self.getDeviceObject(ifname);
+
+ entry.up = dev.up;
+
+ switch (dev.type)
+ {
+ case 'IP tunnel':
+ entry.kind = 'tunnel';
+ break;
+
+ case 'Bridge':
+ entry.kind = 'bridge';
+ //entry.ports = dev['bridge-members'].sort();
+ break;
+ }
}
- }),
- initStart: function(init, cb) { return _luci2.system.initRun(init, 'start', cb) },
- initStop: function(init, cb) { return _luci2.system.initRun(init, 'stop', cb) },
- initRestart: function(init, cb) { return _luci2.system.initRun(init, 'restart', cb) },
- initReload: function(init, cb) { return _luci2.system.initRun(init, 'reload', cb) },
- initEnable: function(init, cb) { return _luci2.system.initRun(init, 'enable', cb) },
- initDisable: function(init, cb) { return _luci2.system.initRun(init, 'disable', cb) },
+ for (var i = 0; i < self.rpcCache.devlist.length; i++)
+ {
+ var dev = self.rpcCache.devlist[i];
+ if (self.isBlacklistedDevice(dev.device))
+ continue;
- getRcLocal: _luci2.rpc.declare({
- object: 'luci2.system',
- method: 'rclocal_get',
- expect: { data: '' }
- }),
+ var entry = self.getDeviceObject(dev.device);
- setRcLocal: _luci2.rpc.declare({
- object: 'luci2.system',
- method: 'rclocal_set',
- params: [ 'data' ]
- }),
+ entry.up = dev.is_up;
+ entry.type = dev.type;
+
+ switch (dev.type)
+ {
+ case 1: /* Ethernet */
+ if (dev.is_bridge)
+ entry.kind = 'bridge';
+ else if (dev.is_tuntap)
+ entry.kind = 'tunnel';
+ else if (dev.is_wireless)
+ entry.kind = 'wifi';
+ break;
+ case 512: /* PPP */
+ case 768: /* IP-IP Tunnel */
+ case 769: /* IP6-IP6 Tunnel */
+ case 776: /* IPv6-in-IPv4 */
+ case 778: /* GRE over IP */
+ entry.kind = 'tunnel';
+ break;
+ }
+ }
- getCrontab: _luci2.rpc.declare({
- object: 'luci2.system',
- method: 'crontab_get',
- expect: { data: '' }
- }),
+ var net = L.uci.sections('network');
+ for (var i = 0; i < net.length; i++)
+ {
+ var s = net[i];
+ var sid = s['.name'];
- setCrontab: _luci2.rpc.declare({
- object: 'luci2.system',
- method: 'crontab_set',
- params: [ 'data' ]
- }),
+ if (s['.type'] == 'device' && s.name)
+ {
+ var entry = self.getDeviceObject(s.name);
+ switch (s.type)
+ {
+ case 'macvlan':
+ case 'tunnel':
+ entry.kind = 'tunnel';
+ break;
+ }
- getSSHKeys: _luci2.rpc.declare({
- object: 'luci2.system',
- method: 'sshkeys_get',
- expect: { keys: [ ] }
- }),
+ entry.sid = sid;
+ }
+ else if (s['.type'] == 'interface' && !s['.anonymous'] && s.ifname)
+ {
+ var ifnames = L.toArray(s.ifname);
- setSSHKeys: _luci2.rpc.declare({
- object: 'luci2.system',
- method: 'sshkeys_set',
- params: [ 'keys' ]
- }),
+ for (var j = 0; j < ifnames.length; j++)
+ self.getDeviceObject(ifnames[j]);
+ if (s['.name'] != 'loopback')
+ {
+ var entry = self.getDeviceObject('@%s'.format(s['.name']));
- setPassword: _luci2.rpc.declare({
- object: 'luci2.system',
- method: 'password_set',
- params: [ 'user', 'password' ]
- }),
+ entry.type = 0;
+ entry.kind = 'alias';
+ entry.sid = sid;
+ }
+ }
+ else if (s['.type'] == 'switch_vlan' && s.device)
+ {
+ var sw = self.rpcCache.swstate[s.device];
+ var vid = parseInt(s.vid || s.vlan);
+ var ports = L.toArray(s.ports);
+ if (!sw || !ports.length || isNaN(vid))
+ continue;
- listLEDs: _luci2.rpc.declare({
- object: 'luci2.system',
- method: 'led_list',
- expect: { leds: [ ] }
- }),
+ var ifname = undefined;
- listUSBDevices: _luci2.rpc.declare({
- object: 'luci2.system',
- method: 'usb_list',
- expect: { devices: [ ] }
- }),
+ for (var j = 0; j < ports.length; j++)
+ {
+ var port = parseInt(ports[j]);
+ var tag = (ports[j].replace(/[^tu]/g, '') == 't');
+ if (port == sw.cpu_port)
+ {
+ // XXX: need a way to map switch to netdev
+ if (tag)
+ ifname = 'eth0.%d'.format(vid);
+ else
+ ifname = 'eth0';
- testUpgrade: _luci2.rpc.declare({
- object: 'luci2.system',
- method: 'upgrade_test',
- expect: { '': { } }
- }),
+ break;
+ }
+ }
- startUpgrade: _luci2.rpc.declare({
- object: 'luci2.system',
- method: 'upgrade_start',
- params: [ 'keep' ]
- }),
+ if (!ifname)
+ continue;
- cleanUpgrade: _luci2.rpc.declare({
- object: 'luci2.system',
- method: 'upgrade_clean'
- }),
+ var entry = self.getDeviceObject(ifname);
+ entry.kind = 'vlan';
+ entry.sid = sid;
+ entry.vsw = sw;
+ entry.vid = vid;
+ }
+ }
- restoreBackup: _luci2.rpc.declare({
- object: 'luci2.system',
- method: 'backup_restore'
- }),
+ var wifi = L.uci.sections('wireless');
+ for (var i = 0; i < wifi.length; i++)
+ {
+ var s = wifi[i];
+ var sid = s['.name'];
- cleanBackup: _luci2.rpc.declare({
- object: 'luci2.system',
- method: 'backup_clean'
- }),
+ if (s['.type'] == 'wifi-iface' && s.device)
+ {
+ var r = parseInt(s.device.replace(/^[^0-9]+/, ''));
+ var n = wificount[s.device] = (wificount[s.device] || 0) + 1;
+ var id = 'radio%d.network%d'.format(r, n);
+ var ifname = id;
+
+ if (self.rpcCache.wifistate[s.device])
+ {
+ var ifcs = self.rpcCache.wifistate[s.device].interfaces;
+ for (var ifc in ifcs)
+ {
+ if (ifcs[ifc].section == sid && ifcs[ifc].ifname)
+ {
+ ifname = ifcs[ifc].ifname;
+ break;
+ }
+ }
+ }
+
+ var entry = self.getDeviceObject(ifname);
+
+ entry.kind = 'wifi';
+ entry.sid = sid;
+ entry.wid = id;
+ entry.wdev = s.device;
+ entry.wmode = s.mode;
+ entry.wssid = s.ssid;
+ entry.wbssid = s.bssid;
+ }
+ }
+
+ for (var i = 0; i < net.length; i++)
+ {
+ var s = net[i];
+ var sid = s['.name'];
+
+ if (s['.type'] == 'interface' && !s['.anonymous'] && s.type == 'bridge')
+ {
+ var ifnames = L.toArray(s.ifname);
+
+ for (var ifname in self.deviceObjects)
+ {
+ var dev = self.deviceObjects[ifname];
+
+ if (dev.kind != 'wifi')
+ continue;
+
+ var wnets = L.toArray(L.uci.get('wireless', dev.sid, 'network'));
+ if ($.inArray(sid, wnets) > -1)
+ ifnames.push(ifname);
+ }
+
+ entry = self.getDeviceObject('br-%s'.format(s['.name']));
+ entry.type = 1;
+ entry.kind = 'bridge';
+ entry.sid = sid;
+ entry.ports = ifnames.sort();
+ }
+ }
+ },
+
+ loadInterfacesCallback: function()
+ {
+ var self = L.NetworkModel;
+ var net = L.uci.sections('network');
+
+ for (var i = 0; i < net.length; i++)
+ {
+ var s = net[i];
+ var sid = s['.name'];
+
+ if (s['.type'] == 'interface' && !s['.anonymous'] && s.proto)
+ {
+ var entry = self.getInterfaceObject(s['.name']);
+ var proto = self.protocolHandlers[s.proto] || self.protocolHandlers.none;
+
+ var l3dev = undefined;
+ var l2dev = undefined;
+
+ var ifnames = L.toArray(s.ifname);
+
+ for (var ifname in self.deviceObjects)
+ {
+ var dev = self.deviceObjects[ifname];
+
+ if (dev.kind != 'wifi')
+ continue;
+
+ var wnets = L.toArray(L.uci.get('wireless', dev.sid, 'network'));
+ if ($.inArray(entry.name, wnets) > -1)
+ ifnames.push(ifname);
+ }
+
+ if (proto.virtual)
+ l3dev = '%s-%s'.format(s.proto, entry.name);
+ else if (s.type == 'bridge')
+ l3dev = 'br-%s'.format(entry.name);
+ else
+ l3dev = ifnames[0];
+
+ if (!proto.virtual && s.type == 'bridge')
+ l2dev = 'br-%s'.format(entry.name);
+ else if (!proto.virtual)
+ l2dev = ifnames[0];
+
+ entry.proto = proto;
+ entry.sid = sid;
+ entry.l3dev = l3dev;
+ entry.l2dev = l2dev;
+ }
+ }
+
+ for (var i = 0; i < self.rpcCache.ifstate.length; i++)
+ {
+ var iface = self.rpcCache.ifstate[i];
+ var entry = self.getInterfaceObject(iface['interface']);
+ var proto = self.protocolHandlers[iface.proto] || self.protocolHandlers.none;
+
+ /* this is a virtual interface, either deleted from config but
+ not applied yet or set up from external tools (6rd) */
+ if (!entry.sid)
+ {
+ entry.proto = proto;
+ entry.l2dev = iface.device;
+ entry.l3dev = iface.l3_device;
+ }
+ }
+ },
+
+ init: function()
+ {
+ var self = this;
+
+ if (self.rpcCache)
+ return L.deferrable();
+
+ self.rpcCache = { };
+ self.deviceObjects = { };
+ self.interfaceObjects = { };
+ self.protocolHandlers = { };
+
+ return self.loadCache()
+ .then(self.loadProtocolHandlers)
+ .then(self.loadDevicesCallback)
+ .then(self.loadInterfacesCallback);
+ },
+
+ update: function()
+ {
+ delete this.rpcCache;
+ return this.init();
+ },
+
+ refreshInterfaceStatus: function()
+ {
+ return this.loadCache(1).then(this.loadInterfacesCallback);
+ },
+
+ refreshDeviceStatus: function()
+ {
+ return this.loadCache(2).then(this.loadDevicesCallback);
+ },
+
+ refreshStatus: function()
+ {
+ return this.loadCache(1)
+ .then(this.loadCache(2))
+ .then(this.loadDevicesCallback)
+ .then(this.loadInterfacesCallback);
+ },
+
+ getDevices: function()
+ {
+ var devs = [ ];
+
+ for (var ifname in this.deviceObjects)
+ if (ifname != 'lo')
+ devs.push(new L.NetworkModel.Device(this.deviceObjects[ifname]));
+
+ return devs.sort(this.sortDevicesCallback);
+ },
+
+ getDeviceByInterface: function(iface)
+ {
+ if (iface instanceof L.NetworkModel.Interface)
+ iface = iface.name();
+
+ if (this.interfaceObjects[iface])
+ return this.getDevice(this.interfaceObjects[iface].l3dev) ||
+ this.getDevice(this.interfaceObjects[iface].l2dev);
+
+ return undefined;
+ },
+
+ getDevice: function(ifname)
+ {
+ if (this.deviceObjects[ifname])
+ return new L.NetworkModel.Device(this.deviceObjects[ifname]);
+
+ return undefined;
+ },
+
+ createDevice: function(name)
+ {
+ return new L.NetworkModel.Device(this.getDeviceObject(name));
+ },
+
+ getInterfaces: function()
+ {
+ var ifaces = [ ];
+
+ for (var name in this.interfaceObjects)
+ if (name != 'loopback')
+ ifaces.push(this.getInterface(name));
+
+ ifaces.sort(function(a, b) {
+ if (a.name() < b.name())
+ return -1;
+ else if (a.name() > b.name())
+ return 1;
+ else
+ return 0;
+ });
+
+ return ifaces;
+ },
+
+ getInterfacesByDevice: function(dev)
+ {
+ var ifaces = [ ];
+
+ if (dev instanceof L.NetworkModel.Device)
+ dev = dev.name();
+
+ for (var name in this.interfaceObjects)
+ {
+ var iface = this.interfaceObjects[name];
+ if (iface.l2dev == dev || iface.l3dev == dev)
+ ifaces.push(this.getInterface(name));
+ }
+
+ ifaces.sort(function(a, b) {
+ if (a.name() < b.name())
+ return -1;
+ else if (a.name() > b.name())
+ return 1;
+ else
+ return 0;
+ });
+
+ return ifaces;
+ },
+
+ getInterface: function(iface)
+ {
+ if (this.interfaceObjects[iface])
+ return new L.NetworkModel.Interface(this.interfaceObjects[iface]);
+
+ return undefined;
+ },
+
+ getProtocols: function()
+ {
+ var rv = [ ];
+
+ for (var proto in this.protocolHandlers)
+ {
+ var pr = this.protocolHandlers[proto];
+
+ rv.push({
+ name: proto,
+ description: pr.description,
+ virtual: pr.virtual,
+ tunnel: pr.tunnel
+ });
+ }
+
+ return rv.sort(function(a, b) {
+ if (a.name < b.name)
+ return -1;
+ else if (a.name > b.name)
+ return 1;
+ else
+ return 0;
+ });
+ },
+
+ findWANByAddr: function(ipaddr)
+ {
+ for (var i = 0; i < this.rpcCache.ifstate.length; i++)
+ {
+ var ifstate = this.rpcCache.ifstate[i];
+
+ if (!ifstate.route)
+ continue;
+
+ for (var j = 0; j < ifstate.route.length; j++)
+ if (ifstate.route[j].mask == 0 &&
+ ifstate.route[j].target == ipaddr &&
+ typeof(ifstate.route[j].table) == 'undefined')
+ {
+ return this.getInterface(ifstate['interface']);
+ }
+ }
+
+ return undefined;
+ },
+
+ findWAN: function()
+ {
+ return this.findWANByAddr('0.0.0.0');
+ },
+
+ findWAN6: function()
+ {
+ return this.findWANByAddr('::');
+ },
+
+ resolveAlias: function(ifname)
+ {
+ if (ifname instanceof L.NetworkModel.Device)
+ ifname = ifname.name();
+
+ var dev = this.deviceObjects[ifname];
+ var seen = { };
+
+ while (dev && dev.kind == 'alias')
+ {
+ // loop
+ if (seen[dev.ifname])
+ return undefined;
+
+ var ifc = this.interfaceObjects[dev.sid];
+
+ seen[dev.ifname] = true;
+ dev = ifc ? this.deviceObjects[ifc.l3dev] : undefined;
+ }
+
+ return dev ? this.getDevice(dev.ifname) : undefined;
+ }
+ };
+
+ this.NetworkModel.Device = Class.extend({
+ wifiModeStrings: {
+ ap: L.tr('Master'),
+ sta: L.tr('Client'),
+ adhoc: L.tr('Ad-Hoc'),
+ monitor: L.tr('Monitor'),
+ wds: L.tr('Static WDS')
+ },
+
+ getStatus: function(key)
+ {
+ var s = L.NetworkModel.rpcCache.devstate[this.options.ifname];
+
+ if (s)
+ return key ? s[key] : s;
+
+ return undefined;
+ },
+
+ get: function(key)
+ {
+ var sid = this.options.sid;
+ var pkg = (this.options.kind == 'wifi') ? 'wireless' : 'network';
+ return L.uci.get(pkg, sid, key);
+ },
+
+ set: function(key, val)
+ {
+ var sid = this.options.sid;
+ var pkg = (this.options.kind == 'wifi') ? 'wireless' : 'network';
+ return L.uci.set(pkg, sid, key, val);
+ },
+
+ init: function()
+ {
+ if (typeof(this.options.type) == 'undefined')
+ this.options.type = 1;
+
+ if (typeof(this.options.kind) == 'undefined')
+ this.options.kind = 'ethernet';
+
+ if (typeof(this.options.networks) == 'undefined')
+ this.options.networks = [ ];
+ },
+
+ name: function()
+ {
+ return this.options.ifname;
+ },
+
+ description: function()
+ {
+ switch (this.options.kind)
+ {
+ case 'alias':
+ return L.tr('Alias for network "%s"').format(this.options.ifname.substring(1));
+
+ case 'bridge':
+ return L.tr('Network bridge');
+
+ case 'ethernet':
+ return L.tr('Network device');
+
+ case 'tunnel':
+ switch (this.options.type)
+ {
+ case 1: /* tuntap */
+ return L.tr('TAP device');
+
+ case 512: /* PPP */
+ return L.tr('PPP tunnel');
+
+ case 768: /* IP-IP Tunnel */
+ return L.tr('IP-in-IP tunnel');
+
+ case 769: /* IP6-IP6 Tunnel */
+ return L.tr('IPv6-in-IPv6 tunnel');
+
+ case 776: /* IPv6-in-IPv4 */
+ return L.tr('IPv6-over-IPv4 tunnel');
+ break;
+
+ case 778: /* GRE over IP */
+ return L.tr('GRE-over-IP tunnel');
+
+ default:
+ return L.tr('Tunnel device');
+ }
+
+ case 'vlan':
+ return L.tr('VLAN %d on %s').format(this.options.vid, this.options.vsw.model);
+
+ case 'wifi':
+ var o = this.options;
+ return L.trc('(Wifi-Mode) "(SSID)" on (radioX)', '%s "%h" on %s').format(
+ o.wmode ? this.wifiModeStrings[o.wmode] : L.tr('Unknown mode'),
+ o.wssid || '?', o.wdev
+ );
+ }
+
+ return L.tr('Unknown device');
+ },
+
+ icon: function(up)
+ {
+ var kind = this.options.kind;
+
+ if (kind == 'alias')
+ kind = 'ethernet';
+
+ if (typeof(up) == 'undefined')
+ up = this.isUp();
+
+ return L.globals.resource + '/icons/%s%s.png'.format(kind, up ? '' : '_disabled');
+ },
+
+ isUp: function()
+ {
+ var l = L.NetworkModel.rpcCache.devlist;
+
+ for (var i = 0; i < l.length; i++)
+ if (l[i].device == this.options.ifname)
+ return (l[i].is_up === true);
+
+ return false;
+ },
+
+ isAlias: function()
+ {
+ return (this.options.kind == 'alias');
+ },
+
+ isBridge: function()
+ {
+ return (this.options.kind == 'bridge');
+ },
+
+ isBridgeable: function()
+ {
+ return (this.options.type == 1 && this.options.kind != 'bridge');
+ },
+
+ isWireless: function()
+ {
+ return (this.options.kind == 'wifi');
+ },
+
+ isInNetwork: function(net)
+ {
+ if (!(net instanceof L.NetworkModel.Interface))
+ net = L.NetworkModel.getInterface(net);
+
+ if (net)
+ {
+ if (net.options.l3dev == this.options.ifname ||
+ net.options.l2dev == this.options.ifname)
+ return true;
+
+ var dev = L.NetworkModel.deviceObjects[net.options.l2dev];
+ if (dev && dev.kind == 'bridge' && dev.ports)
+ return ($.inArray(this.options.ifname, dev.ports) > -1);
+ }
+
+ return false;
+ },
+
+ getMTU: function()
+ {
+ var dev = L.NetworkModel.rpcCache.devstate[this.options.ifname];
+ if (dev && !isNaN(dev.mtu))
+ return dev.mtu;
+
+ return undefined;
+ },
+
+ getMACAddress: function()
+ {
+ if (this.options.type != 1)
+ return undefined;
+
+ var dev = L.NetworkModel.rpcCache.devstate[this.options.ifname];
+ if (dev && dev.macaddr)
+ return dev.macaddr.toUpperCase();
+
+ return undefined;
+ },
+
+ getInterfaces: function()
+ {
+ return L.NetworkModel.getInterfacesByDevice(this.options.name);
+ },
+
+ getStatistics: function()
+ {
+ var s = this.getStatus('statistics') || { };
+ return {
+ rx_bytes: (s.rx_bytes || 0),
+ tx_bytes: (s.tx_bytes || 0),
+ rx_packets: (s.rx_packets || 0),
+ tx_packets: (s.tx_packets || 0)
+ };
+ },
+
+ getTrafficHistory: function()
+ {
+ var def = new Array(120);
+
+ for (var i = 0; i < 120; i++)
+ def[i] = 0;
+
+ var h = L.NetworkModel.rpcCache.bwstate[this.options.ifname] || { };
+ return {
+ rx_bytes: (h.rx_bytes || def),
+ tx_bytes: (h.tx_bytes || def),
+ rx_packets: (h.rx_packets || def),
+ tx_packets: (h.tx_packets || def)
+ };
+ },
+
+ removeFromInterface: function(iface)
+ {
+ if (!(iface instanceof L.NetworkModel.Interface))
+ iface = L.NetworkModel.getInterface(iface);
+
+ if (!iface)
+ return;
+
+ var ifnames = L.toArray(iface.get('ifname'));
+ if ($.inArray(this.options.ifname, ifnames) > -1)
+ iface.set('ifname', L.filterArray(ifnames, this.options.ifname));
+
+ if (this.options.kind != 'wifi')
+ return;
+
+ var networks = L.toArray(this.get('network'));
+ if ($.inArray(iface.name(), networks) > -1)
+ this.set('network', L.filterArray(networks, iface.name()));
+ },
+
+ attachToInterface: function(iface)
+ {
+ if (!(iface instanceof L.NetworkModel.Interface))
+ iface = L.NetworkModel.getInterface(iface);
+
+ if (!iface)
+ return;
+
+ if (this.options.kind != 'wifi')
+ {
+ var ifnames = L.toArray(iface.get('ifname'));
+ if ($.inArray(this.options.ifname, ifnames) < 0)
+ {
+ ifnames.push(this.options.ifname);
+ iface.set('ifname', (ifnames.length > 1) ? ifnames : ifnames[0]);
+ }
+ }
+ else
+ {
+ var networks = L.toArray(this.get('network'));
+ if ($.inArray(iface.name(), networks) < 0)
+ {
+ networks.push(iface.name());
+ this.set('network', (networks.length > 1) ? networks : networks[0]);
+ }
+ }
+ }
+ });
+
+ this.NetworkModel.Interface = Class.extend({
+ getStatus: function(key)
+ {
+ var s = L.NetworkModel.rpcCache.ifstate;
+
+ for (var i = 0; i < s.length; i++)
+ if (s[i]['interface'] == this.options.name)
+ return key ? s[i][key] : s[i];
+
+ return undefined;
+ },
+
+ get: function(key)
+ {
+ return L.uci.get('network', this.options.name, key);
+ },
+
+ set: function(key, val)
+ {
+ return L.uci.set('network', this.options.name, key, val);
+ },
+
+ name: function()
+ {
+ return this.options.name;
+ },
+
+ protocol: function()
+ {
+ return (this.get('proto') || 'none');
+ },
+
+ isUp: function()
+ {
+ return (this.getStatus('up') === true);
+ },
+
+ isVirtual: function()
+ {
+ return (typeof(this.options.sid) != 'string');
+ },
+
+ getProtocol: function()
+ {
+ var prname = this.get('proto') || 'none';
+ return L.NetworkModel.protocolHandlers[prname] || L.NetworkModel.protocolHandlers.none;
+ },
+
+ getUptime: function()
+ {
+ var uptime = this.getStatus('uptime');
+ return isNaN(uptime) ? 0 : uptime;
+ },
+
+ getDevice: function(resolveAlias)
+ {
+ if (this.options.l3dev)
+ return L.NetworkModel.getDevice(this.options.l3dev);
+
+ return undefined;
+ },
+
+ getPhysdev: function()
+ {
+ if (this.options.l2dev)
+ return L.NetworkModel.getDevice(this.options.l2dev);
+
+ return undefined;
+ },
+
+ getSubdevices: function()
+ {
+ var rv = [ ];
+ var dev = this.options.l2dev ?
+ L.NetworkModel.deviceObjects[this.options.l2dev] : undefined;
+
+ if (dev && dev.kind == 'bridge' && dev.ports && dev.ports.length)
+ for (var i = 0; i < dev.ports.length; i++)
+ rv.push(L.NetworkModel.getDevice(dev.ports[i]));
+
+ return rv;
+ },
+
+ getIPv4Addrs: function(mask)
+ {
+ var rv = [ ];
+ var addrs = this.getStatus('ipv4-address');
+
+ if (addrs)
+ for (var i = 0; i < addrs.length; i++)
+ if (!mask)
+ rv.push(addrs[i].address);
+ else
+ rv.push('%s/%d'.format(addrs[i].address, addrs[i].mask));
+
+ return rv;
+ },
+
+ getIPv6Addrs: function(mask)
+ {
+ var rv = [ ];
+ var addrs;
+
+ addrs = this.getStatus('ipv6-address');
+
+ if (addrs)
+ for (var i = 0; i < addrs.length; i++)
+ if (!mask)
+ rv.push(addrs[i].address);
+ else
+ rv.push('%s/%d'.format(addrs[i].address, addrs[i].mask));
+
+ addrs = this.getStatus('ipv6-prefix-assignment');
+
+ if (addrs)
+ for (var i = 0; i < addrs.length; i++)
+ if (!mask)
+ rv.push('%s1'.format(addrs[i].address));
+ else
+ rv.push('%s1/%d'.format(addrs[i].address, addrs[i].mask));
+
+ return rv;
+ },
+
+ getDNSAddrs: function()
+ {
+ var rv = [ ];
+ var addrs = this.getStatus('dns-server');
+
+ if (addrs)
+ for (var i = 0; i < addrs.length; i++)
+ rv.push(addrs[i]);
+
+ return rv;
+ },
+
+ getIPv4DNS: function()
+ {
+ var rv = [ ];
+ var dns = this.getStatus('dns-server');
+
+ if (dns)
+ for (var i = 0; i < dns.length; i++)
+ if (dns[i].indexOf(':') == -1)
+ rv.push(dns[i]);
+
+ return rv;
+ },
+
+ getIPv6DNS: function()
+ {
+ var rv = [ ];
+ var dns = this.getStatus('dns-server');
+
+ if (dns)
+ for (var i = 0; i < dns.length; i++)
+ if (dns[i].indexOf(':') > -1)
+ rv.push(dns[i]);
+
+ return rv;
+ },
+
+ getIPv4Gateway: function()
+ {
+ var rt = this.getStatus('route');
+
+ if (rt)
+ for (var i = 0; i < rt.length; i++)
+ if (rt[i].target == '0.0.0.0' && rt[i].mask == 0)
+ return rt[i].nexthop;
+
+ return undefined;
+ },
+
+ getIPv6Gateway: function()
+ {
+ var rt = this.getStatus('route');
+
+ if (rt)
+ for (var i = 0; i < rt.length; i++)
+ if (rt[i].target == '::' && rt[i].mask == 0)
+ return rt[i].nexthop;
+
+ return undefined;
+ },
+
+ getStatistics: function()
+ {
+ var dev = this.getDevice() || new L.NetworkModel.Device({});
+ return dev.getStatistics();
+ },
+
+ getTrafficHistory: function()
+ {
+ var dev = this.getDevice() || new L.NetworkModel.Device({});
+ return dev.getTrafficHistory();
+ },
+
+ renderBadge: function()
+ {
+ var badge = $('<span />')
+ .addClass('badge')
+ .text('%s: '.format(this.name()));
+
+ var dev = this.getDevice();
+ var subdevs = this.getSubdevices();
+
+ if (subdevs.length)
+ for (var j = 0; j < subdevs.length; j++)
+ badge.append($('<img />')
+ .attr('src', subdevs[j].icon())
+ .attr('title', '%s (%s)'.format(subdevs[j].description(), subdevs[j].name() || '?')));
+ else if (dev)
+ badge.append($('<img />')
+ .attr('src', dev.icon())
+ .attr('title', '%s (%s)'.format(dev.description(), dev.name() || '?')));
+ else
+ badge.append($('<em />').text(L.tr('(No devices attached)')));
+
+ return badge;
+ },
+
+ setDevices: function(devs)
+ {
+ var dev = this.getPhysdev();
+ var old_devs = [ ];
+ var changed = false;
+
+ if (dev && dev.isBridge())
+ old_devs = this.getSubdevices();
+ else if (dev)
+ old_devs = [ dev ];
+
+ if (old_devs.length != devs.length)
+ changed = true;
+ else
+ for (var i = 0; i < old_devs.length; i++)
+ {
+ var dev = devs[i];
+
+ if (dev instanceof L.NetworkModel.Device)
+ dev = dev.name();
+
+ if (!dev || old_devs[i].name() != dev)
+ {
+ changed = true;
+ break;
+ }
+ }
+
+ if (changed)
+ {
+ for (var i = 0; i < old_devs.length; i++)
+ old_devs[i].removeFromInterface(this);
+
+ for (var i = 0; i < devs.length; i++)
+ {
+ var dev = devs[i];
+
+ if (!(dev instanceof L.NetworkModel.Device))
+ dev = L.NetworkModel.getDevice(dev);
+
+ if (dev)
+ dev.attachToInterface(this);
+ }
+ }
+ },
+
+ changeProtocol: function(proto)
+ {
+ var pr = L.NetworkModel.protocolHandlers[proto];
+
+ if (!pr)
+ return;
+
+ for (var opt in (this.get() || { }))
+ {
+ switch (opt)
+ {
+ case 'type':
+ case 'ifname':
+ case 'macaddr':
+ if (pr.virtual)
+ this.set(opt, undefined);
+ break;
+
+ case 'auto':
+ case 'mtu':
+ break;
+
+ case 'proto':
+ this.set(opt, pr.protocol);
+ break;
+
+ default:
+ this.set(opt, undefined);
+ break;
+ }
+ }
+ },
+
+ createForm: function(mapwidget)
+ {
+ var self = this;
+ var proto = self.getProtocol();
+ var device = self.getDevice();
+
+ if (!mapwidget)
+ mapwidget = L.cbi.Map;
+
+ var map = new mapwidget('network', {
+ caption: L.tr('Configure "%s"').format(self.name())
+ });
+
+ var section = map.section(L.cbi.SingleSection, self.name(), {
+ anonymous: true
+ });
+
+ section.tab({
+ id: 'general',
+ caption: L.tr('General Settings')
+ });
+
+ section.tab({
+ id: 'advanced',
+ caption: L.tr('Advanced Settings')
+ });
+
+ section.tab({
+ id: 'ipv6',
+ caption: L.tr('IPv6')
+ });
+
+ section.tab({
+ id: 'physical',
+ caption: L.tr('Physical Settings')
+ });
+
+
+ section.taboption('general', L.cbi.CheckboxValue, 'auto', {
+ caption: L.tr('Start on boot'),
+ optional: true,
+ initial: true
+ });
+
+ var pr = section.taboption('general', L.cbi.ListValue, 'proto', {
+ caption: L.tr('Protocol')
+ });
+
+ pr.ucivalue = function(sid) {
+ return self.get('proto') || 'none';
+ };
+
+ var ok = section.taboption('general', L.cbi.ButtonValue, '_confirm', {
+ caption: L.tr('Really switch?'),
+ description: L.tr('Changing the protocol will clear all configuration for this interface!'),
+ text: L.tr('Change protocol')
+ });
+
+ ok.on('click', function(ev) {
+ self.changeProtocol(pr.formvalue(ev.data.sid));
+ self.createForm(mapwidget).show();
+ });
+
+ var protos = L.NetworkModel.getProtocols();
+
+ for (var i = 0; i < protos.length; i++)
+ pr.value(protos[i].name, protos[i].description);
+
+ proto.populateForm(section, self);
+
+ if (!proto.virtual)
+ {
+ var br = section.taboption('physical', L.cbi.CheckboxValue, 'type', {
+ caption: L.tr('Network bridge'),
+ description: L.tr('Merges multiple devices into one logical bridge'),
+ optional: true,
+ enabled: 'bridge',
+ disabled: '',
+ initial: ''
+ });
+
+ section.taboption('physical', L.cbi.DeviceList, '__iface_multi', {
+ caption: L.tr('Devices'),
+ multiple: true,
+ bridges: false
+ }).depends('type', true);
+
+ section.taboption('physical', L.cbi.DeviceList, '__iface_single', {
+ caption: L.tr('Device'),
+ multiple: false,
+ bridges: true
+ }).depends('type', false);
+
+ var mac = section.taboption('physical', L.cbi.InputValue, 'macaddr', {
+ caption: L.tr('Override MAC'),
+ optional: true,
+ placeholder: device ? device.getMACAddress() : undefined,
+ datatype: 'macaddr'
+ })
+
+ mac.ucivalue = function(sid)
+ {
+ if (device)
+ return device.get('macaddr');
+
+ return this.callSuper('ucivalue', sid);
+ };
+
+ mac.save = function(sid)
+ {
+ if (!this.changed(sid))
+ return false;
+
+ if (device)
+ device.set('macaddr', this.formvalue(sid));
+ else
+ this.callSuper('set', sid);
+
+ return true;
+ };
+ }
+
+ section.taboption('physical', L.cbi.InputValue, 'mtu', {
+ caption: L.tr('Override MTU'),
+ optional: true,
+ placeholder: device ? device.getMTU() : undefined,
+ datatype: 'range(1, 9000)'
+ });
+ section.taboption('physical', L.cbi.InputValue, 'metric', {
+ caption: L.tr('Override Metric'),
+ optional: true,
+ placeholder: 0,
+ datatype: 'uinteger'
+ });
- getBackupConfig: _luci2.rpc.declare({
- object: 'luci2.system',
- method: 'backup_config_get',
- expect: { config: '' }
- }),
+ for (var field in section.fields)
+ {
+ switch (field)
+ {
+ case 'proto':
+ break;
- setBackupConfig: _luci2.rpc.declare({
- object: 'luci2.system',
- method: 'backup_config_set',
- params: [ 'data' ]
- }),
+ case '_confirm':
+ for (var i = 0; i < protos.length; i++)
+ if (protos[i].name != (this.get('proto') || 'none'))
+ section.fields[field].depends('proto', protos[i].name);
+ break;
+ default:
+ section.fields[field].depends('proto', this.get('proto') || 'none', true);
+ break;
+ }
+ }
- listBackup: _luci2.rpc.declare({
- object: 'luci2.system',
- method: 'backup_list',
- expect: { files: [ ] }
- }),
+ return map;
+ }
+ });
+ this.NetworkModel.Protocol = this.NetworkModel.Interface.extend({
+ description: '__unknown__',
+ tunnel: false,
+ virtual: false,
- performReboot: _luci2.rpc.declare({
- object: 'luci2.system',
- method: 'reboot'
- })
- };
+ populateForm: function(section, iface)
+ {
- this.opkg = {
- updateLists: _luci2.rpc.declare({
- object: 'luci2.opkg',
- method: 'update',
- expect: { '': { } }
- }),
+ }
+ });
- _allPackages: _luci2.rpc.declare({
- object: 'luci2.opkg',
- method: 'list',
- params: [ 'offset', 'limit', 'pattern' ],
+ this.system = {
+ getSystemInfo: L.rpc.declare({
+ object: 'system',
+ method: 'info',
expect: { '': { } }
}),
- _installedPackages: _luci2.rpc.declare({
- object: 'luci2.opkg',
- method: 'list_installed',
- params: [ 'offset', 'limit', 'pattern' ],
+ getBoardInfo: L.rpc.declare({
+ object: 'system',
+ method: 'board',
expect: { '': { } }
}),
- _findPackages: _luci2.rpc.declare({
- object: 'luci2.opkg',
- method: 'find',
- params: [ 'offset', 'limit', 'pattern' ],
+ getDiskInfo: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'diskfree',
expect: { '': { } }
}),
- _fetchPackages: function(action, offset, limit, pattern)
+ getInfo: function(cb)
{
- var packages = [ ];
-
- return action(offset, limit, pattern).then(function(list) {
- if (!list.total || !list.packages)
- return { length: 0, total: 0 };
-
- packages.push.apply(packages, list.packages);
- packages.total = list.total;
-
- if (limit <= 0)
- limit = list.total;
-
- if (packages.length >= limit)
- return packages;
-
- _luci2.rpc.batch();
+ L.rpc.batch();
- for (var i = offset + packages.length; i < limit; i += 100)
- action(i, (Math.min(i + 100, limit) % 100) || 100, pattern);
+ this.getSystemInfo();
+ this.getBoardInfo();
+ this.getDiskInfo();
- return _luci2.rpc.flush();
- }).then(function(lists) {
- for (var i = 0; i < lists.length; i++)
- {
- if (!lists[i].total || !lists[i].packages)
- continue;
+ return L.rpc.flush().then(function(info) {
+ var rv = { };
- packages.push.apply(packages, lists[i].packages);
- packages.total = lists[i].total;
- }
+ $.extend(rv, info[0]);
+ $.extend(rv, info[1]);
+ $.extend(rv, info[2]);
- return packages;
+ return rv;
});
},
- listPackages: function(offset, limit, pattern)
- {
- return _luci2.opkg._fetchPackages(_luci2.opkg._allPackages, offset, limit, pattern);
- },
- installedPackages: function(offset, limit, pattern)
- {
- return _luci2.opkg._fetchPackages(_luci2.opkg._installedPackages, offset, limit, pattern);
- },
+ initList: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'init_list',
+ expect: { initscripts: [ ] },
+ filter: function(data) {
+ data.sort(function(a, b) { return (a.start || 0) - (b.start || 0) });
+ return data;
+ }
+ }),
- findPackages: function(offset, limit, pattern)
+ initEnabled: function(init, cb)
{
- return _luci2.opkg._fetchPackages(_luci2.opkg._findPackages, offset, limit, pattern);
+ return this.initList().then(function(list) {
+ for (var i = 0; i < list.length; i++)
+ if (list[i].name == init)
+ return !!list[i].enabled;
+
+ return false;
+ });
},
- installPackage: _luci2.rpc.declare({
- object: 'luci2.opkg',
- method: 'install',
- params: [ 'package' ],
- expect: { '': { } }
+ initRun: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'init_action',
+ params: [ 'name', 'action' ],
+ filter: function(data) {
+ return (data == 0);
+ }
}),
- removePackage: _luci2.rpc.declare({
- object: 'luci2.opkg',
- method: 'remove',
- params: [ 'package' ],
- expect: { '': { } }
- }),
+ initStart: function(init, cb) { return L.system.initRun(init, 'start', cb) },
+ initStop: function(init, cb) { return L.system.initRun(init, 'stop', cb) },
+ initRestart: function(init, cb) { return L.system.initRun(init, 'restart', cb) },
+ initReload: function(init, cb) { return L.system.initRun(init, 'reload', cb) },
+ initEnable: function(init, cb) { return L.system.initRun(init, 'enable', cb) },
+ initDisable: function(init, cb) { return L.system.initRun(init, 'disable', cb) },
- getConfig: _luci2.rpc.declare({
- object: 'luci2.opkg',
- method: 'config_get',
- expect: { config: '' }
- }),
- setConfig: _luci2.rpc.declare({
- object: 'luci2.opkg',
- method: 'config_set',
- params: [ 'data' ]
+ performReboot: L.rpc.declare({
+ object: 'luci2.system',
+ method: 'reboot'
})
};
this.session = {
- login: _luci2.rpc.declare({
+ login: L.rpc.declare({
object: 'session',
method: 'login',
params: [ 'username', 'password' ],
expect: { '': { } }
}),
- access: _luci2.rpc.declare({
+ access: L.rpc.declare({
object: 'session',
method: 'access',
params: [ 'scope', 'object', 'function' ],
isAlive: function()
{
- return _luci2.session.access('ubus', 'session', 'access');
+ return L.session.access('ubus', 'session', 'access');
},
startHeartbeat: function()
{
this._hearbeatInterval = window.setInterval(function() {
- _luci2.session.isAlive().then(function(alive) {
+ L.session.isAlive().then(function(alive) {
if (!alive)
{
- _luci2.session.stopHeartbeat();
- _luci2.ui.login(true);
+ L.session.stopHeartbeat();
+ L.ui.login(true);
}
});
- }, _luci2.globals.timeout * 2);
+ }, L.globals.timeout * 2);
},
stopHeartbeat: function()
window.clearInterval(this._hearbeatInterval);
delete this._hearbeatInterval;
}
+ },
+
+
+ aclCache: { },
+
+ callAccess: L.rpc.declare({
+ object: 'session',
+ method: 'access',
+ expect: { '': { } }
+ }),
+
+ callAccessCallback: function(acls)
+ {
+ L.session.aclCache = acls;
+ },
+
+ updateACLs: function()
+ {
+ return L.session.callAccess()
+ .then(L.session.callAccessCallback);
+ },
+
+ hasACL: function(scope, object, func)
+ {
+ var acls = L.session.aclCache;
+
+ if (typeof(func) == 'undefined')
+ return (acls && acls[scope] && acls[scope][object]);
+
+ if (acls && acls[scope] && acls[scope][object])
+ for (var i = 0; i < acls[scope][object].length; i++)
+ if (acls[scope][object][i] == func)
+ return true;
+
+ return false;
}
};
this.ui = {
+ saveScrollTop: function()
+ {
+ this._scroll_top = $(document).scrollTop();
+ },
+
+ restoreScrollTop: function()
+ {
+ if (typeof(this._scroll_top) == 'undefined')
+ return;
+
+ $(document).scrollTop(this._scroll_top);
+
+ delete this._scroll_top;
+ },
+
loading: function(enable)
{
var win = $(window);
var body = $('body');
- var div = _luci2._modal || (
- _luci2._modal = $('<div />')
- .addClass('cbi-modal-loader')
- .append($('<div />').text(_luci2.tr('Loading data...')))
+
+ var state = L.ui._loading || (L.ui._loading = {
+ modal: $('<div />')
+ .css('z-index', 2000)
+ .addClass('modal fade')
+ .append($('<div />')
+ .addClass('modal-dialog')
+ .append($('<div />')
+ .addClass('modal-content luci2-modal-loader')
+ .append($('<div />')
+ .addClass('modal-body')
+ .text(L.tr('Loading data…')))))
.appendTo(body)
- );
+ .modal({
+ backdrop: 'static',
+ keyboard: false
+ })
+ });
- if (enable)
- {
- body.css('overflow', 'hidden');
- body.css('padding', 0);
- body.css('width', win.width());
- body.css('height', win.height());
- div.css('width', win.width());
- div.css('height', win.height());
- div.show();
- }
- else
- {
- div.hide();
- body.css('overflow', '');
- body.css('padding', '');
- body.css('width', '');
- body.css('height', '');
- }
+ state.modal.modal(enable ? 'show' : 'hide');
},
dialog: function(title, content, options)
{
var win = $(window);
var body = $('body');
- var div = _luci2._dialog || (
- _luci2._dialog = $('<div />')
- .addClass('cbi-modal-dialog')
+
+ var state = L.ui._dialog || (L.ui._dialog = {
+ dialog: $('<div />')
+ .addClass('modal fade')
.append($('<div />')
+ .addClass('modal-dialog')
.append($('<div />')
- .addClass('cbi-modal-dialog-header'))
- .append($('<div />')
- .addClass('cbi-modal-dialog-body'))
- .append($('<div />')
- .addClass('cbi-modal-dialog-footer')
- .append($('<button />')
- .addClass('cbi-button')
- .text(_luci2.tr('Close'))
- .click(function() {
- $('body')
- .css('overflow', '')
- .css('padding', '')
- .css('width', '')
- .css('height', '');
-
- $(this).parent().parent().parent().hide();
- }))))
+ .addClass('modal-content')
+ .append($('<div />')
+ .addClass('modal-header')
+ .append('<h4 />')
+ .addClass('modal-title'))
+ .append($('<div />')
+ .addClass('modal-body'))
+ .append($('<div />')
+ .addClass('modal-footer')
+ .append(L.ui.button(L.tr('Close'), 'primary')
+ .click(function() {
+ $(this).parents('div.modal').modal('hide');
+ })))))
.appendTo(body)
- );
+ });
if (typeof(options) != 'object')
options = { };
if (title === false)
{
- body
- .css('overflow', '')
- .css('padding', '')
- .css('width', '')
- .css('height', '');
-
- _luci2._dialog.hide();
+ state.dialog.modal('hide');
- return;
+ return state.dialog;
}
- var cnt = div.children().children('div.cbi-modal-dialog-body');
- var ftr = div.children().children('div.cbi-modal-dialog-footer');
+ var cnt = state.dialog.children().children().children('div.modal-body');
+ var ftr = state.dialog.children().children().children('div.modal-footer');
- ftr.empty();
+ ftr.empty().show();
if (options.style == 'confirm')
{
- ftr.append($('<button />')
- .addClass('cbi-button')
- .text(_luci2.tr('Ok'))
- .click(options.confirm || function() { _luci2.ui.dialog(false) }));
+ ftr.append(L.ui.button(L.tr('Ok'), 'primary')
+ .click(options.confirm || function() { L.ui.dialog(false) }));
- ftr.append($('<button />')
- .addClass('cbi-button')
- .text(_luci2.tr('Cancel'))
- .click(options.cancel || function() { _luci2.ui.dialog(false) }));
+ ftr.append(L.ui.button(L.tr('Cancel'), 'default')
+ .click(options.cancel || function() { L.ui.dialog(false) }));
}
else if (options.style == 'close')
{
- ftr.append($('<button />')
- .addClass('cbi-button')
- .text(_luci2.tr('Close'))
- .click(options.close || function() { _luci2.ui.dialog(false) }));
+ ftr.append(L.ui.button(L.tr('Close'), 'primary')
+ .click(options.close || function() { L.ui.dialog(false) }));
}
else if (options.style == 'wait')
{
- ftr.append($('<button />')
- .addClass('cbi-button')
- .text(_luci2.tr('Close'))
+ ftr.append(L.ui.button(L.tr('Close'), 'primary')
.attr('disabled', true));
}
- div.find('div.cbi-modal-dialog-header').text(title);
- div.show();
+ if (options.wide)
+ {
+ state.dialog.addClass('wide');
+ }
+ else
+ {
+ state.dialog.removeClass('wide');
+ }
- cnt
- .css('max-height', Math.floor(win.height() * 0.70) + 'px')
- .empty()
- .append(content);
+ state.dialog.find('h4:first').text(title);
+ state.dialog.modal('show');
- div.children()
- .css('margin-top', -Math.floor(div.children().height() / 2) + 'px');
+ cnt.empty().append(content);
- body.css('overflow', 'hidden');
- body.css('padding', 0);
- body.css('width', win.width());
- body.css('height', win.height());
- div.css('width', win.width());
- div.css('height', win.height());
+ return state.dialog;
},
upload: function(title, content, options)
{
- var form = _luci2._upload || (
- _luci2._upload = $('<form />')
+ var state = L.ui._upload || (L.ui._upload = {
+ form: $('<form />')
.attr('method', 'post')
.attr('action', '/cgi-bin/luci-upload')
.attr('enctype', 'multipart/form-data')
.append($('<p />'))
.append($('<input />')
.attr('type', 'hidden')
- .attr('name', 'sessionid')
- .attr('value', _luci2.globals.sid))
+ .attr('name', 'sessionid'))
.append($('<input />')
.attr('type', 'hidden')
- .attr('name', 'filename')
- .attr('value', options.filename))
+ .attr('name', 'filename'))
.append($('<input />')
.attr('type', 'file')
.attr('name', 'filedata')
.addClass('cbi-input-file'))
.append($('<div />')
.css('width', '100%')
- .addClass('progressbar')
- .addClass('intermediate')
+ .addClass('progress progress-striped active')
.append($('<div />')
+ .addClass('progress-bar')
.css('width', '100%')))
.append($('<iframe />')
+ .addClass('pull-right')
.attr('name', 'cbi-fileupload-frame')
.css('width', '1px')
.css('height', '1px')
- .css('visibility', 'hidden'))
- );
+ .css('visibility', 'hidden')),
- var finish = _luci2._upload_finish_cb || (
- _luci2._upload_finish_cb = function(ev) {
+ finish_cb: function(ev) {
$(this).off('load');
var body = (this.contentDocument || this.contentWindow.document).body;
json = $.parseJSON(body.innerHTML);
} catch(e) {
json = {
- message: _luci2.tr('Invalid server response received'),
- error: [ -1, _luci2.tr('Invalid data') ]
+ message: L.tr('Invalid server response received'),
+ error: [ -1, L.tr('Invalid data') ]
};
};
if (json.error)
{
L.ui.dialog(L.tr('File upload'), [
- $('<p />').text(_luci2.tr('The file upload failed with the server response below:')),
+ $('<p />').text(L.tr('The file upload failed with the server response below:')),
$('<pre />').addClass('alert-message').text(json.message || json.error[1]),
- $('<p />').text(_luci2.tr('In case of network problems try uploading the file again.'))
+ $('<p />').text(L.tr('In case of network problems try uploading the file again.'))
], { style: 'close' });
}
- else if (typeof(ev.data.cb) == 'function')
+ else if (typeof(state.success_cb) == 'function')
{
- ev.data.cb(json);
+ state.success_cb(json);
}
- }
- );
+ },
- var confirm = _luci2._upload_confirm_cb || (
- _luci2._upload_confirm_cb = function() {
- var d = _luci2._upload;
- var f = d.find('.cbi-input-file');
- var b = d.find('.progressbar');
- var p = d.find('p');
+ confirm_cb: function() {
+ var f = state.form.find('.cbi-input-file');
+ var b = state.form.find('.progress');
+ var p = state.form.find('p');
if (!f.val())
return;
- d.find('iframe').on('load', { cb: options.success }, finish);
- d.submit();
+ state.form.find('iframe').on('load', state.finish_cb);
+ state.form.submit();
f.hide();
b.show();
- p.text(_luci2.tr('File upload in progress …'));
+ p.text(L.tr('File upload in progress …'));
- _luci2._dialog.find('button').prop('disabled', true);
+ state.form.parent().parent().find('button').prop('disabled', true);
}
- );
+ });
- _luci2._upload.find('.progressbar').hide();
- _luci2._upload.find('.cbi-input-file').val('').show();
- _luci2._upload.find('p').text(content || _luci2.tr('Select the file to upload and press "%s" to proceed.').format(_luci2.tr('Ok')));
+ state.form.find('.progress').hide();
+ state.form.find('.cbi-input-file').val('').show();
+ state.form.find('p').text(content || L.tr('Select the file to upload and press "%s" to proceed.').format(L.tr('Ok')));
- _luci2.ui.dialog(title || _luci2.tr('File upload'), _luci2._upload, {
+ state.form.find('[name=sessionid]').val(L.globals.sid);
+ state.form.find('[name=filename]').val(options.filename);
+
+ state.success_cb = options.success;
+
+ L.ui.dialog(title || L.tr('File upload'), state.form, {
style: 'confirm',
- confirm: confirm
+ confirm: state.confirm_cb
});
},
var images = $();
var interval, timeout;
- _luci2.ui.dialog(
- _luci2.tr('Waiting for device'), [
- $('<p />').text(_luci2.tr('Please stand by while the device is reconfiguring …')),
+ L.ui.dialog(
+ L.tr('Waiting for device'), [
+ $('<p />').text(L.tr('Please stand by while the device is reconfiguring …')),
$('<div />')
.css('width', '100%')
.addClass('progressbar')
for (var i = 0; i < protocols.length; i++)
images = images.add($('<img />').attr('url', protocols[i] + '://' + address + ':' + ports[i]));
- //_luci2.network.getNetworkStatus(function(s) {
+ //L.network.getNetworkStatus(function(s) {
// for (var i = 0; i < protocols.length; i++)
// {
// for (var j = 0; j < s.length; j++)
//}).then(function() {
images.on('load', function() {
var url = this.getAttribute('url');
- _luci2.session.isAlive().then(function(access) {
+ L.session.isAlive().then(function(access) {
if (access)
{
window.clearTimeout(timeout);
window.clearInterval(interval);
- _luci2.ui.dialog(false);
+ L.ui.dialog(false);
images = null;
}
else
interval = window.setInterval(function() {
images.each(function() {
- this.setAttribute('src', this.getAttribute('url') + _luci2.globals.resource + '/icons/loading.gif?r=' + Math.random());
+ this.setAttribute('src', this.getAttribute('url') + L.globals.resource + '/icons/loading.gif?r=' + Math.random());
});
}, 5000);
window.clearInterval(interval);
images.off('load');
- _luci2.ui.dialog(
- _luci2.tr('Device not responding'),
- _luci2.tr('The device was not responding within 180 seconds, you might need to manually reconnect your computer or use SSH to regain access.'),
+ L.ui.dialog(
+ L.tr('Device not responding'),
+ L.tr('The device was not responding within 180 seconds, you might need to manually reconnect your computer or use SSH to regain access.'),
{ style: 'close' }
);
}, 180000);
login: function(invalid)
{
- if (!_luci2._login_deferred || _luci2._login_deferred.state() != 'pending')
- _luci2._login_deferred = $.Deferred();
-
- /* try to find sid from hash */
- var sid = _luci2.getHash('id');
- if (sid && sid.match(/^[a-f0-9]{32}$/))
- {
- _luci2.globals.sid = sid;
- _luci2.session.isAlive().then(function(access) {
- if (access)
- {
- _luci2.session.startHeartbeat();
- _luci2._login_deferred.resolve();
- }
- else
- {
- _luci2.setHash('id', undefined);
- _luci2.ui.login();
- }
- });
-
- return _luci2._login_deferred;
- }
-
- var form = _luci2._login || (
- _luci2._login = $('<div />')
+ var state = L.ui._login || (L.ui._login = {
+ form: $('<form />')
+ .attr('target', '')
+ .attr('method', 'post')
.append($('<p />')
.addClass('alert-message')
- .text(_luci2.tr('Wrong username or password given!')))
+ .text(L.tr('Wrong username or password given!')))
.append($('<p />')
.append($('<label />')
- .text(_luci2.tr('Username'))
+ .text(L.tr('Username'))
.append($('<br />'))
.append($('<input />')
.attr('type', 'text')
.attr('name', 'username')
.attr('value', 'root')
- .addClass('cbi-input-text'))))
+ .addClass('form-control')
+ .keypress(function(ev) {
+ if (ev.which == 10 || ev.which == 13)
+ state.confirm_cb();
+ }))))
.append($('<p />')
.append($('<label />')
- .text(_luci2.tr('Password'))
+ .text(L.tr('Password'))
.append($('<br />'))
.append($('<input />')
.attr('type', 'password')
.attr('name', 'password')
- .addClass('cbi-input-password'))))
+ .addClass('form-control')
+ .keypress(function(ev) {
+ if (ev.which == 10 || ev.which == 13)
+ state.confirm_cb();
+ }))))
.append($('<p />')
- .text(_luci2.tr('Enter your username and password above, then click "%s" to proceed.').format(_luci2.tr('Ok'))))
- );
+ .text(L.tr('Enter your username and password above, then click "%s" to proceed.').format(L.tr('Ok')))),
- var response_cb = _luci2._login_response_cb || (
- _luci2._login_response_cb = function(response) {
+ response_cb: function(response) {
if (!response.ubus_rpc_session)
{
- _luci2.ui.login(true);
+ L.ui.login(true);
}
else
{
- _luci2.globals.sid = response.ubus_rpc_session;
- _luci2.setHash('id', _luci2.globals.sid);
- _luci2.session.startHeartbeat();
- _luci2.ui.dialog(false);
- _luci2._login_deferred.resolve();
+ L.globals.sid = response.ubus_rpc_session;
+ L.setHash('id', L.globals.sid);
+ L.session.startHeartbeat();
+ L.ui.dialog(false);
+ state.deferred.resolve();
}
- }
- );
+ },
- var confirm_cb = _luci2._login_confirm_cb || (
- _luci2._login_confirm_cb = function() {
- var d = _luci2._login;
- var u = d.find('[name=username]').val();
- var p = d.find('[name=password]').val();
+ confirm_cb: function() {
+ var u = state.form.find('[name=username]').val();
+ var p = state.form.find('[name=password]').val();
if (!u)
return;
- _luci2.ui.dialog(
- _luci2.tr('Logging in'), [
- $('<p />').text(_luci2.tr('Log in in progress …')),
+ L.ui.dialog(
+ L.tr('Logging in'), [
+ $('<p />').text(L.tr('Log in in progress …')),
$('<div />')
.css('width', '100%')
.addClass('progressbar')
], { style: 'wait' }
);
- _luci2.globals.sid = '00000000000000000000000000000000';
- _luci2.session.login(u, p).then(response_cb);
+ L.globals.sid = '00000000000000000000000000000000';
+ L.session.login(u, p).then(state.response_cb);
}
- );
+ });
+
+ if (!state.deferred || state.deferred.state() != 'pending')
+ state.deferred = $.Deferred();
+
+ /* try to find sid from hash */
+ var sid = L.getHash('id');
+ if (sid && sid.match(/^[a-f0-9]{32}$/))
+ {
+ L.globals.sid = sid;
+ L.session.isAlive().then(function(access) {
+ if (access)
+ {
+ L.session.startHeartbeat();
+ state.deferred.resolve();
+ }
+ else
+ {
+ L.setHash('id', undefined);
+ L.ui.login();
+ }
+ });
+
+ return state.deferred;
+ }
if (invalid)
- form.find('.alert-message').show();
+ state.form.find('.alert-message').show();
else
- form.find('.alert-message').hide();
+ state.form.find('.alert-message').hide();
- _luci2.ui.dialog(_luci2.tr('Authorization Required'), form, {
+ L.ui.dialog(L.tr('Authorization Required'), state.form, {
style: 'confirm',
- confirm: confirm_cb
+ confirm: state.confirm_cb
});
- return _luci2._login_deferred;
+ state.form.find('[name=password]').focus();
+
+ return state.deferred;
},
+ cryptPassword: L.rpc.declare({
+ object: 'luci2.ui',
+ method: 'crypt',
+ params: [ 'data' ],
+ expect: { crypt: '' }
+ }),
+
- _acl_merge_scope: function(acl_scope, scope)
+ mergeACLScope: function(acl_scope, scope)
{
if ($.isArray(scope))
{
}
},
- _acl_merge_permission: function(acl_perm, perm)
+ mergeACLPermission: function(acl_perm, perm)
{
if ($.isPlainObject(perm))
{
for (var scope_name in perm)
{
var acl_scope = acl_perm[scope_name] || (acl_perm[scope_name] = { });
- this._acl_merge_scope(acl_scope, perm[scope_name]);
+ L.ui.mergeACLScope(acl_scope, perm[scope_name]);
}
}
},
- _acl_merge_group: function(acl_group, group)
+ mergeACLGroup: function(acl_group, group)
{
if ($.isPlainObject(group))
{
if (group.read)
{
var acl_perm = acl_group.read || (acl_group.read = { });
- this._acl_merge_permission(acl_perm, group.read);
+ L.ui.mergeACLPermission(acl_perm, group.read);
}
if (group.write)
{
var acl_perm = acl_group.write || (acl_group.write = { });
- this._acl_merge_permission(acl_perm, group.write);
+ L.ui.mergeACLPermission(acl_perm, group.write);
}
}
},
- _acl_merge_tree: function(acl_tree, tree)
+ callACLsCallback: function(trees)
{
- if ($.isPlainObject(tree))
+ var acl_tree = { };
+
+ for (var i = 0; i < trees.length; i++)
{
- for (var group_name in tree)
+ if (!$.isPlainObject(trees[i]))
+ continue;
+
+ for (var group_name in trees[i])
{
var acl_group = acl_tree[group_name] || (acl_tree[group_name] = { });
- this._acl_merge_group(acl_group, tree[group_name]);
+ L.ui.mergeACLGroup(acl_group, trees[i][group_name]);
}
}
+
+ return acl_tree;
},
- listAvailableACLs: _luci2.rpc.declare({
+ callACLs: L.rpc.declare({
object: 'luci2.ui',
method: 'acls',
- expect: { acls: [ ] },
- filter: function(trees) {
- var acl_tree = { };
- for (var i = 0; i < trees.length; i++)
- _luci2.ui._acl_merge_tree(acl_tree, trees[i]);
- return acl_tree;
- }
+ expect: { acls: [ ] }
}),
- renderMainMenu: _luci2.rpc.declare({
+ getAvailableACLs: function()
+ {
+ return this.callACLs().then(this.callACLsCallback);
+ },
+
+ renderChangeIndicator: function()
+ {
+ return $('<ul />')
+ .addClass('nav navbar-nav navbar-right')
+ .append($('<li />')
+ .append($('<a />')
+ .attr('id', 'changes')
+ .attr('href', '#')
+ .append($('<span />')
+ .addClass('label label-info'))));
+ },
+
+ callMenuCallback: function(entries)
+ {
+ L.globals.mainMenu = new L.ui.menu();
+ L.globals.mainMenu.entries(entries);
+
+ $('#mainmenu')
+ .empty()
+ .append(L.globals.mainMenu.render(0, 1))
+ .append(L.ui.renderChangeIndicator());
+ },
+
+ callMenu: L.rpc.declare({
object: 'luci2.ui',
method: 'menu',
- expect: { menu: { } },
- filter: function(entries) {
- _luci2.globals.mainMenu = new _luci2.ui.menu();
- _luci2.globals.mainMenu.entries(entries);
-
- $('#mainmenu')
- .empty()
- .append(_luci2.globals.mainMenu.render(0, 1));
- }
+ expect: { menu: { } }
}),
+ renderMainMenu: function()
+ {
+ return this.callMenu().then(this.callMenuCallback);
+ },
+
renderViewMenu: function()
{
$('#viewmenu')
.empty()
- .append(_luci2.globals.mainMenu.render(2, 900));
+ .append(L.globals.mainMenu.render(2, 900));
},
- renderView: function(node)
+ renderView: function()
{
- var name = node.view.split(/\//).join('.');
+ var node = arguments[0];
+ var name = node.view.split(/\//).join('.');
+ var cname = L.toClassName(name);
+ var views = L.views || (L.views = { });
+ var args = [ ];
+
+ for (var i = 1; i < arguments.length; i++)
+ args.push(arguments[i]);
- _luci2.ui.renderViewMenu();
+ if (L.globals.currentView)
+ L.globals.currentView.finish();
- if (!_luci2._views)
- _luci2._views = { };
+ L.ui.renderViewMenu();
+ L.setHash('view', node.view);
- _luci2.setHash('view', node.view);
+ if (views[cname] instanceof L.ui.view)
+ {
+ L.globals.currentView = views[cname];
+ return views[cname].render.apply(views[cname], args);
+ }
- if (_luci2._views[name] instanceof _luci2.ui.view)
- return _luci2._views[name].render();
+ var url = L.globals.resource + '/view/' + name + '.js';
- return $.ajax(_luci2.globals.resource + '/view/' + name + '.js', {
+ return $.ajax(url, {
method: 'GET',
cache: true,
dataType: 'text'
}).then(function(data) {
try {
- var viewConstructor = (new Function(['L', '$'], 'return ' + data))(_luci2, $);
+ var viewConstructorSource = (
+ '(function(L, $) { ' +
+ 'return %s' +
+ '})(L, $);\n\n' +
+ '//@ sourceURL=%s'
+ ).format(data, url);
+
+ var viewConstructor = eval(viewConstructorSource);
- _luci2._views[name] = new viewConstructor({
+ views[cname] = new viewConstructor({
name: name,
acls: node.write || { }
});
- return _luci2._views[name].render();
+ L.globals.currentView = views[cname];
+ return views[cname].render.apply(views[cname], args);
+ }
+ catch(e) {
+ alert('Unable to instantiate view "%s": %s'.format(url, e));
+ };
+
+ return $.Deferred().resolve();
+ });
+ },
+
+ changeView: function()
+ {
+ var name = L.getHash('view');
+ var node = L.globals.defaultNode;
+
+ if (name && L.globals.mainMenu)
+ node = L.globals.mainMenu.getNode(name);
+
+ if (node)
+ {
+ L.ui.loading(true);
+ L.ui.renderView(node).then(function() {
+ L.ui.loading(false);
+ });
+ }
+ },
+
+ updateHostname: function()
+ {
+ return L.system.getBoardInfo().then(function(info) {
+ if (info.hostname)
+ $('#hostname').text(info.hostname);
+ });
+ },
+
+ updateChanges: function()
+ {
+ return L.uci.changes().then(function(changes) {
+ var n = 0;
+ var html = '';
+
+ for (var config in changes)
+ {
+ var log = [ ];
+
+ for (var i = 0; i < changes[config].length; i++)
+ {
+ var c = changes[config][i];
+
+ switch (c[0])
+ {
+ case 'order':
+ log.push('uci reorder %s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2]));
+ break;
+
+ case 'remove':
+ if (c.length < 3)
+ log.push('uci delete %s.<del>%s</del>'.format(config, c[1]));
+ else
+ log.push('uci delete %s.%s.<del>%s</del>'.format(config, c[1], c[2]));
+ break;
+
+ case 'rename':
+ if (c.length < 4)
+ log.push('uci rename %s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3]));
+ else
+ log.push('uci rename %s.%s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3], c[4]));
+ break;
+
+ case 'add':
+ log.push('uci add %s <ins>%s</ins> (= <ins><strong>%s</strong></ins>)'.format(config, c[2], c[1]));
+ break;
+
+ case 'list-add':
+ log.push('uci add_list %s.%s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3], c[4]));
+ break;
+
+ case 'list-del':
+ log.push('uci del_list %s.%s.<del>%s=<strong>%s</strong></del>'.format(config, c[1], c[2], c[3], c[4]));
+ break;
+
+ case 'set':
+ if (c.length < 4)
+ log.push('uci set %s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2]));
+ else
+ log.push('uci set %s.%s.<ins>%s=<strong>%s</strong></ins>'.format(config, c[1], c[2], c[3], c[4]));
+ break;
+ }
+ }
+
+ html += '<code>/etc/config/%s</code><pre class="uci-changes">%s</pre>'.format(config, log.join('\n'));
+ n += changes[config].length;
}
- catch(e) { };
- return $.Deferred().resolve();
+ if (n > 0)
+ $('#changes')
+ .click(function(ev) {
+ L.ui.dialog(L.tr('Staged configuration changes'), html, {
+ style: 'confirm',
+ confirm: function() {
+ L.uci.apply().then(
+ function(code) { alert('Success with code ' + code); },
+ function(code) { alert('Error with code ' + code); }
+ );
+ }
+ });
+ ev.preventDefault();
+ })
+ .children('span')
+ .show()
+ .text(L.trcp('Pending configuration changes', '1 change', '%d changes', n).format(n));
+ else
+ $('#changes').children('span').hide();
});
},
init: function()
{
- _luci2.ui.loading(true);
+ L.ui.loading(true);
$.when(
- _luci2.ui.renderMainMenu()
+ L.session.updateACLs(),
+ L.ui.updateHostname(),
+ L.ui.updateChanges(),
+ L.ui.renderMainMenu(),
+ L.NetworkModel.init()
).then(function() {
- _luci2.ui.renderView(_luci2.globals.defaultNode).then(function() {
- _luci2.ui.loading(false);
- })
+ L.ui.renderView(L.globals.defaultNode).then(function() {
+ L.ui.loading(false);
+ });
+
+ $(window).on('hashchange', function() {
+ L.ui.changeView();
+ });
});
+ },
+
+ button: function(label, style, title)
+ {
+ style = style || 'default';
+
+ return $('<button />')
+ .attr('type', 'button')
+ .attr('title', title ? title : '')
+ .addClass('btn btn-' + style)
+ .text(label);
}
};
- var AbstractWidget = Class.extend({
+ this.ui.AbstractWidget = Class.extend({
i18n: function(text) {
return text;
},
- toString: function() {
- var x = document.createElement('div');
- x.appendChild(this.render());
+ label: function() {
+ var key = arguments[0];
+ var args = [ ];
+
+ for (var i = 1; i < arguments.length; i++)
+ args.push(arguments[i]);
+
+ switch (typeof(this.options[key]))
+ {
+ case 'undefined':
+ return '';
+
+ case 'function':
+ return this.options[key].apply(this, args);
+
+ default:
+ return ''.format.apply('' + this.options[key], args);
+ }
+ },
- return x.innerHTML;
+ toString: function() {
+ return $('<div />').append(this.render()).html();
},
insertInto: function(id) {
return $(id).empty().append(this.render());
+ },
+
+ appendTo: function(id) {
+ return $(id).append(this.render());
+ },
+
+ on: function(evname, evfunc)
+ {
+ var evnames = L.toArray(evname);
+
+ if (!this.events)
+ this.events = { };
+
+ for (var i = 0; i < evnames.length; i++)
+ this.events[evnames[i]] = evfunc;
+
+ return this;
+ },
+
+ trigger: function(evname, evdata)
+ {
+ if (this.events)
+ {
+ var evnames = L.toArray(evname);
+
+ for (var i = 0; i < evnames.length; i++)
+ if (this.events[evnames[i]])
+ this.events[evnames[i]].call(this, evdata);
+ }
+
+ return this;
}
});
- this.ui.view = AbstractWidget.extend({
+ this.ui.view = this.ui.AbstractWidget.extend({
_fetch_template: function()
{
- return $.ajax(_luci2.globals.resource + '/template/' + this.options.name + '.htm', {
+ return $.ajax(L.globals.resource + '/template/' + this.options.name + '.htm', {
method: 'GET',
cache: true,
dataType: 'text',
return '';
case ':':
- return _luci2.tr(p2);
+ return L.tr(p2);
case '=':
- return _luci2.globals[p2] || '';
+ return L.globals[p2] || '';
default:
return '(?' + match + ')';
container.append($('<h2 />').append(this.title));
if (this.description)
- container.append($('<div />').addClass('cbi-map-descr').append(this.description));
+ container.append($('<p />').append(this.description));
var self = this;
+ var args = [ ];
+
+ for (var i = 0; i < arguments.length; i++)
+ args.push(arguments[i]);
+
return this._fetch_template().then(function() {
- return _luci2.deferrable(self.execute());
+ return L.deferrable(self.execute.apply(self, args));
});
+ },
+
+ repeat: function(func, interval)
+ {
+ var self = this;
+
+ if (!self._timeouts)
+ self._timeouts = [ ];
+
+ var index = self._timeouts.length;
+
+ if (typeof(interval) != 'number')
+ interval = 5000;
+
+ var setTimer, runTimer;
+
+ setTimer = function() {
+ if (self._timeouts)
+ self._timeouts[index] = window.setTimeout(runTimer, interval);
+ };
+
+ runTimer = function() {
+ L.deferrable(func.call(self)).then(setTimer, setTimer);
+ };
+
+ runTimer();
+ },
+
+ finish: function()
+ {
+ if ($.isArray(this._timeouts))
+ {
+ for (var i = 0; i < this._timeouts.length; i++)
+ window.clearTimeout(this._timeouts[i]);
+
+ delete this._timeouts;
+ }
}
});
- this.ui.menu = AbstractWidget.extend({
+ this.ui.menu = this.ui.AbstractWidget.extend({
init: function() {
this._nodes = { };
},
}
},
- _indexcmp: function(a, b)
+ sortNodesCallback: function(a, b)
{
var x = a.index || 0;
var y = b.index || 0;
for (var child in (node.childs || { }))
nodes.push(node.childs[child]);
- nodes.sort(this._indexcmp);
+ nodes.sort(this.sortNodesCallback);
for (var i = 0; i < nodes.length; i++)
{
var child = this.firstChildView(nodes[i]);
if (child)
{
- $.extend(node, child);
+ for (var key in child)
+ if (!node.hasOwnProperty(key) && child.hasOwnProperty(key))
+ node[key] = child[key];
+
return node;
}
}
return undefined;
},
- _onclick: function(ev)
+ handleClick: function(ev)
{
- _luci2.ui.loading(true);
- _luci2.ui.renderView(ev.data).then(function() {
- _luci2.ui.loading(false);
- });
+ L.setHash('view', ev.data);
ev.preventDefault();
this.blur();
},
- _render: function(childs, level, min, max)
+ renderNodes: function(childs, level, min, max)
{
var nodes = [ ];
for (var node in childs)
nodes.push(childs[node]);
}
- nodes.sort(this._indexcmp);
+ nodes.sort(this.sortNodesCallback);
var list = $('<ul />');
if (level == 0)
- list.addClass('nav');
+ list.addClass('nav').addClass('navbar-nav');
else if (level == 1)
- list.addClass('dropdown-menu');
+ list.addClass('dropdown-menu').addClass('navbar-inverse');
for (var i = 0; i < nodes.length; i++)
{
- if (!_luci2.globals.defaultNode)
+ if (!L.globals.defaultNode)
{
- var v = _luci2.getHash('view');
+ var v = L.getHash('view');
if (!v || v == nodes[i].view)
- _luci2.globals.defaultNode = nodes[i];
+ L.globals.defaultNode = nodes[i];
}
var item = $('<li />')
.append($('<a />')
.attr('href', '#')
- .text(_luci2.tr(nodes[i].title))
- .click(nodes[i], this._onclick))
+ .text(L.tr(nodes[i].title)))
.appendTo(list);
if (nodes[i].childs && level < max)
{
item.addClass('dropdown');
- item.find('a').addClass('menu');
- item.append(this._render(nodes[i].childs, level + 1));
+
+ item.find('a')
+ .addClass('dropdown-toggle')
+ .attr('data-toggle', 'dropdown')
+ .append('<b class="caret"></b>');
+
+ item.append(this.renderNodes(nodes[i].childs, level + 1));
+ }
+ else
+ {
+ item.find('a').click(nodes[i].view, this.handleClick);
}
}
render: function(min, max)
{
- var top = min ? this.getNode(_luci2.globals.defaultNode.view, min) : this._nodes;
- return this._render(top.childs, 0, min, max);
+ var top = min ? this.getNode(L.globals.defaultNode.view, min) : this._nodes;
+ return this.renderNodes(top.childs, 0, min, max);
},
getNode: function(path, max)
}
});
- this.ui.table = AbstractWidget.extend({
+ this.ui.table = this.ui.AbstractWidget.extend({
init: function()
{
this._rows = [ ];
}
var table = document.createElement('table');
- table.className = 'cbi-section-table';
+ table.className = 'table table-condensed table-hover';
var has_caption = false;
var has_description = false;
}
});
- this.ui.progress = AbstractWidget.extend({
+ this.ui.progress = this.ui.AbstractWidget.extend({
render: function()
{
var vn = parseInt(this.options.value) || 0;
var mn = parseInt(this.options.max) || 100;
var pc = Math.floor((100 / mn) * vn);
- var bar = document.createElement('div');
- bar.className = 'progressbar';
-
- bar.appendChild(document.createElement('div'));
- bar.lastChild.appendChild(document.createElement('div'));
- bar.lastChild.style.width = pc + '%';
+ var text;
if (typeof(this.options.format) == 'string')
- $(bar.lastChild.lastChild).append(this.options.format.format(this.options.value, this.options.max, pc));
+ text = this.options.format.format(this.options.value, this.options.max, pc);
else if (typeof(this.options.format) == 'function')
- $(bar.lastChild.lastChild).append(this.options.format(pc));
+ text = this.options.format(pc);
else
- $(bar.lastChild.lastChild).append('%.2f%%'.format(pc));
+ text = '%.2f%%'.format(pc);
- return bar;
+ return $('<div />')
+ .addClass('progress')
+ .append($('<div />')
+ .addClass('progress-bar')
+ .addClass('progress-bar-info')
+ .css('width', pc + '%'))
+ .append($('<small />')
+ .text(text));
}
});
- this.ui.devicebadge = AbstractWidget.extend({
+ this.ui.devicebadge = this.ui.AbstractWidget.extend({
render: function()
{
- var dev = this.options.l3_device || this.options.device || '?';
+ var l2dev = this.options.l2_device || this.options.device;
+ var l3dev = this.options.l3_device;
+ var dev = l3dev || l2dev || '?';
var span = document.createElement('span');
- span.className = 'ifacebadge';
+ span.className = 'badge';
if (typeof(this.options.signal) == 'number' ||
typeof(this.options.noise) == 'number')
}
span.appendChild(document.createElement('img'));
- span.lastChild.src = _luci2.globals.resource + '/icons/signal-' + r + '.png';
+ span.lastChild.src = L.globals.resource + '/icons/signal-' + r + '.png';
if (r == 'none')
- span.title = _luci2.tr('No signal');
+ span.title = L.tr('No signal');
else
span.title = '%s: %d %s / %s: %d %s'.format(
- _luci2.tr('Signal'), this.options.signal, _luci2.tr('dBm'),
- _luci2.tr('Noise'), this.options.noise, _luci2.tr('dBm')
+ L.tr('Signal'), this.options.signal, L.tr('dBm'),
+ L.tr('Noise'), this.options.noise, L.tr('dBm')
);
}
else
{
var type = 'ethernet';
- var desc = _luci2.tr('Ethernet device');
+ var desc = L.tr('Ethernet device');
- if (this.options.l3_device != this.options.device)
+ if (l3dev != l2dev)
{
type = 'tunnel';
- desc = _luci2.tr('Tunnel interface');
+ desc = L.tr('Tunnel interface');
}
else if (dev.indexOf('br-') == 0)
{
type = 'bridge';
- desc = _luci2.tr('Bridge');
+ desc = L.tr('Bridge');
}
else if (dev.indexOf('.') > 0)
{
type = 'vlan';
- desc = _luci2.tr('VLAN interface');
+ desc = L.tr('VLAN interface');
}
else if (dev.indexOf('wlan') == 0 ||
dev.indexOf('ath') == 0 ||
dev.indexOf('wl') == 0)
{
type = 'wifi';
- desc = _luci2.tr('Wireless Network');
+ desc = L.tr('Wireless Network');
}
span.appendChild(document.createElement('img'));
- span.lastChild.src = _luci2.globals.resource + '/icons/' + type + (this.options.up ? '' : '_disabled') + '.png';
+ span.lastChild.src = L.globals.resource + '/icons/' + type + (this.options.up ? '' : '_disabled') + '.png';
span.title = desc;
}
validation: {
i18n: function(msg)
{
- _luci2.cbi.validation.message = _luci2.tr(msg);
+ L.cbi.validation.message = L.tr(msg);
},
compile: function(code)
var pos = 0;
var esc = false;
var depth = 0;
- var types = _luci2.cbi.validation.types;
+ var types = L.cbi.validation.types;
var stack = [ ];
code += ',';
else if (typeof types[label] == 'function')
{
stack.push(types[label]);
- stack.push(null);
+ stack.push([ ]);
}
else
{
throw "Syntax error, argument list follows non-function";
stack[stack.length-1] =
- arguments.callee(code.substring(pos, i));
+ L.cbi.validation.compile(code.substring(pos, i));
pos = i+1;
}
'ipaddr': function()
{
- if (validation.types['ip4addr'].apply(this) ||
- validation.types['ip6addr'].apply(this))
+ if (L.parseIPv4(this) || L.parseIPv6(this))
return true;
validation.i18n('Must be a valid IP address');
'ip4addr': function()
{
- if (this.match(/^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})(\/(\S+))?$/))
- {
- if ((RegExp.$1 >= 0) && (RegExp.$1 <= 255) &&
- (RegExp.$2 >= 0) && (RegExp.$2 <= 255) &&
- (RegExp.$3 >= 0) && (RegExp.$3 <= 255) &&
- (RegExp.$4 >= 0) && (RegExp.$4 <= 255) &&
- ((RegExp.$6.indexOf('.') < 0)
- ? ((RegExp.$6 >= 0) && (RegExp.$6 <= 32))
- : (validation.types['ip4addr'].apply(RegExp.$6))))
- return true;
- }
+ if (L.parseIPv4(this))
+ return true;
validation.i18n('Must be a valid IPv4 address');
return false;
'ip6addr': function()
{
- if (this.match(/^([a-fA-F0-9:.]+)(\/(\d+))?$/))
- {
- if (!RegExp.$2 || ((RegExp.$3 >= 0) && (RegExp.$3 <= 128)))
- {
- var addr = RegExp.$1;
+ if (L.parseIPv6(this))
+ return true;
- if (addr == '::')
- {
- return true;
- }
+ validation.i18n('Must be a valid IPv6 address');
+ return false;
+ },
- if (addr.indexOf('.') > 0)
- {
- var off = addr.lastIndexOf(':');
+ 'netmask4': function()
+ {
+ if (L.isNetmask(L.parseIPv4(this)))
+ return true;
- if (!(off && validation.types['ip4addr'].apply(addr.substr(off+1))))
- {
- validation.i18n('Must be a valid IPv6 address');
- return false;
- }
+ validation.i18n('Must be a valid IPv4 netmask');
+ return false;
+ },
- addr = addr.substr(0, off) + ':0:0';
- }
+ 'netmask6': function()
+ {
+ if (L.isNetmask(L.parseIPv6(this)))
+ return true;
- if (addr.indexOf('::') >= 0)
- {
- var colons = 0;
- var fill = '0';
+ validation.i18n('Must be a valid IPv6 netmask6');
+ return false;
+ },
- for (var i = 1; i < (addr.length-1); i++)
- if (addr.charAt(i) == ':')
- colons++;
+ 'cidr4': function()
+ {
+ if (this.match(/^([0-9.]+)\/(\d{1,2})$/))
+ if (RegExp.$2 <= 32 && L.parseIPv4(RegExp.$1))
+ return true;
- if (colons > 7)
- {
- validation.i18n('Must be a valid IPv6 address');
- return false;
- }
+ validation.i18n('Must be a valid IPv4 prefix');
+ return false;
+ },
- for (var i = 0; i < (7 - colons); i++)
- fill += ':0';
+ 'cidr6': function()
+ {
+ if (this.match(/^([a-fA-F0-9:.]+)\/(\d{1,3})$/))
+ if (RegExp.$2 <= 128 && L.parseIPv6(RegExp.$1))
+ return true;
- if (addr.match(/^(.*?)::(.*?)$/))
- addr = (RegExp.$1 ? RegExp.$1 + ':' : '') + fill +
- (RegExp.$2 ? ':' + RegExp.$2 : '');
- }
+ validation.i18n('Must be a valid IPv6 prefix');
+ return false;
+ },
- if (addr.match(/^(?:[a-fA-F0-9]{1,4}:){7}[a-fA-F0-9]{1,4}$/) != null)
- return true;
+ 'ipmask4': function()
+ {
+ if (this.match(/^([0-9.]+)\/([0-9.]+)$/))
+ {
+ var addr = RegExp.$1, mask = RegExp.$2;
+ if (L.parseIPv4(addr) && L.isNetmask(L.parseIPv4(mask)))
+ return true;
+ }
- validation.i18n('Must be a valid IPv6 address');
- return false;
- }
+ validation.i18n('Must be a valid IPv4 address/netmask pair');
+ return false;
+ },
+
+ 'ipmask6': function()
+ {
+ if (this.match(/^([a-fA-F0-9:.]+)\/([a-fA-F0-9:.]+)$/))
+ {
+ var addr = RegExp.$1, mask = RegExp.$2;
+ if (L.parseIPv6(addr) && L.isNetmask(L.parseIPv6(mask)))
+ return true;
}
+ validation.i18n('Must be a valid IPv6 address/netmask pair');
return false;
},
msgs.push(validation.message.format.apply(validation.message, arguments[i+1]));
}
- validation.message = msgs.join( _luci2.tr(' - or - '));
+ validation.message = msgs.join( L.tr(' - or - '));
return false;
},
};
- var AbstractValue = AbstractWidget.extend({
+ this.cbi.AbstractValue = this.ui.AbstractWidget.extend({
init: function(name, options)
{
this.name = name;
this.dependencies = [ ];
this.rdependency = { };
- this.options = _luci2.defaults(options, {
+ this.options = L.defaults(options, {
placeholder: '',
datatype: 'string',
optional: false,
id: function(sid)
{
- return this.section.id('field', sid || '__unknown__', this.name);
+ return this.ownerSection.id('field', sid || '__unknown__', this.name);
},
- render: function(sid)
+ render: function(sid, condensed)
{
var i = this.instance[sid] = { };
- i.top = $('<div />').addClass('cbi-value');
+ i.top = $('<div />')
+ .addClass('luci2-field');
- if (typeof(this.options.caption) == 'string')
- $('<label />')
- .addClass('cbi-value-title')
- .attr('for', this.id(sid))
- .text(this.options.caption)
- .appendTo(i.top);
+ if (!condensed)
+ {
+ i.top.addClass('form-group');
+
+ if (typeof(this.options.caption) == 'string')
+ $('<label />')
+ .addClass('col-lg-2 control-label')
+ .attr('for', this.id(sid))
+ .text(this.options.caption)
+ .appendTo(i.top);
+ }
- i.widget = $('<div />').addClass('cbi-value-field').append(this.widget(sid)).appendTo(i.top);
- i.error = $('<div />').addClass('cbi-value-error').appendTo(i.top);
+ i.error = $('<div />')
+ .hide()
+ .addClass('luci2-field-error label label-danger');
+
+ i.widget = $('<div />')
+ .addClass('luci2-field-widget')
+ .append(this.widget(sid))
+ .append(i.error)
+ .appendTo(i.top);
+
+ if (!condensed)
+ {
+ i.widget.addClass('col-lg-5');
- if (typeof(this.options.description) == 'string')
$('<div />')
- .addClass('cbi-value-description')
- .text(this.options.description)
+ .addClass('col-lg-5')
+ .text((typeof(this.options.description) == 'string') ? this.options.description : '')
.appendTo(i.top);
+ }
return i.top;
},
+ active: function(sid)
+ {
+ return (this.instance[sid] && !this.instance[sid].disabled);
+ },
+
ucipath: function(sid)
{
return {
- config: (this.options.uci_package || this.map.uci_package),
+ config: (this.options.uci_package || this.ownerMap.uci_package),
section: (this.options.uci_section || sid),
option: (this.options.uci_option || this.name)
};
ucivalue: function(sid)
{
var uci = this.ucipath(sid);
- var val = this.map.get(uci.config, uci.section, uci.option);
+ var val = this.ownerMap.get(uci.config, uci.section, uci.option);
if (typeof(val) == 'undefined')
return this.options.initial;
return this.choices[i][1];
}
else if (v === true)
- return _luci2.tr('yes');
+ return L.tr('yes');
else if (v === false)
- return _luci2.tr('no');
+ return L.tr('no');
else if ($.isArray(v))
return v.join(', ');
if (typeof(a) != typeof(b))
return true;
- if (typeof(a) == 'object')
+ if ($.isArray(a))
{
if (a.length != b.length)
return true;
return false;
}
+ else if ($.isPlainObject(a))
+ {
+ for (var k in a)
+ if (!(k in b))
+ return true;
+
+ for (var k in b)
+ if (!(k in a) || a[k] !== b[k])
+ return true;
+
+ return false;
+ }
return (a != b);
},
if (this.instance[sid].disabled)
{
if (!this.options.keep)
- return this.map.set(uci.config, uci.section, uci.option, undefined);
+ return this.ownerMap.set(uci.config, uci.section, uci.option, undefined);
return false;
}
var val = this.formvalue(sid);
if (chg)
- this.map.set(uci.config, uci.section, uci.option, val);
+ this.ownerMap.set(uci.config, uci.section, uci.option, val);
return chg;
},
- validator: function(sid, elem, multi)
+ findSectionID: function($elem)
+ {
+ return this.ownerSection.findParentSectionIDs($elem)[0];
+ },
+
+ setError: function($elem, msg, msgargs)
+ {
+ var $field = $elem.parents('.luci2-field:first');
+ var $error = $field.find('.luci2-field-error:first');
+
+ if (typeof(msg) == 'string' && msg.length > 0)
+ {
+ $field.addClass('luci2-form-error');
+ $elem.parent().addClass('has-error');
+
+ $error.text(msg.format.apply(msg, msgargs)).show();
+ $field.trigger('validate');
+
+ return false;
+ }
+ else
+ {
+ $elem.parent().removeClass('has-error');
+
+ var $other_errors = $field.find('.has-error');
+ if ($other_errors.length == 0)
+ {
+ $field.removeClass('luci2-form-error');
+ $error.text('').hide();
+ $field.trigger('validate');
+
+ return true;
+ }
+
+ return false;
+ }
+ },
+
+ handleValidate: function(ev)
+ {
+ var $elem = $(this);
+
+ var d = ev.data;
+ var rv = true;
+ var val = $elem.val();
+ var vstack = d.vstack;
+
+ if (vstack && typeof(vstack[0]) == 'function')
+ {
+ delete validation.message;
+
+ if ((val.length == 0 && !d.opt))
+ {
+ rv = d.self.setError($elem, L.tr('Field must not be empty'));
+ }
+ else if (val.length > 0 && !vstack[0].apply(val, vstack[1]))
+ {
+ rv = d.self.setError($elem, validation.message, vstack[1]);
+ }
+ else
+ {
+ rv = d.self.setError($elem);
+ }
+ }
+
+ if (rv)
+ {
+ var sid = d.self.findSectionID($elem);
+
+ for (var field in d.self.rdependency)
+ {
+ d.self.rdependency[field].toggle(sid);
+ d.self.rdependency[field].validate(sid);
+ }
+
+ d.self.ownerSection.tabtoggle(sid);
+ }
+
+ return rv;
+ },
+
+ attachEvents: function(sid, elem)
{
+ var evdata = {
+ self: this,
+ opt: this.options.optional
+ };
+
+ if (this.events)
+ for (var evname in this.events)
+ elem.on(evname, evdata, this.events[evname]);
+
if (typeof(this.options.datatype) == 'undefined' && $.isEmptyObject(this.rdependency))
return elem;
if (typeof(this.options.datatype) == 'string')
{
try {
- vstack = _luci2.cbi.validation.compile(this.options.datatype);
+ evdata.vstack = L.cbi.validation.compile(this.options.datatype);
} catch(e) { };
}
else if (typeof(this.options.datatype) == 'function')
{
var vfunc = this.options.datatype;
- vstack = [ function(elem) {
+ evdata.vstack = [ function(elem) {
var rv = vfunc(this, elem);
if (rv !== true)
validation.message = rv;
}, [ elem ] ];
}
- var evdata = {
- self: this,
- sid: sid,
- elem: elem,
- multi: multi,
- inst: this.instance[sid],
- opt: this.options.optional
- };
-
- var validator = function(ev)
- {
- var d = ev.data;
- var rv = true;
- var val = d.elem.val();
-
- if (vstack && typeof(vstack[0]) == 'function')
- {
- delete validation.message;
-
- if ((val.length == 0 && !d.opt))
- {
- d.elem.addClass('error');
- d.inst.top.addClass('error');
- d.inst.error.text(_luci2.tr('Field must not be empty'));
- rv = false;
- }
- else if (val.length > 0 && !vstack[0].apply(val, vstack[1]))
- {
- d.elem.addClass('error');
- d.inst.top.addClass('error');
- d.inst.error.text(validation.message.format.apply(validation.message, vstack[1]));
- rv = false;
- }
- else
- {
- d.elem.removeClass('error');
-
- if (d.multi && d.inst.widget.find('input.error, select.error').length > 0)
- {
- rv = false;
- }
- else
- {
- d.inst.top.removeClass('error');
- d.inst.error.text('');
- }
- }
- }
-
- if (rv)
- {
- for (var field in d.self.rdependency)
- d.self.rdependency[field].toggle(d.sid);
- }
-
- return rv;
- };
-
if (elem.prop('tagName') == 'SELECT')
{
- elem.change(evdata, validator);
+ elem.change(evdata, this.handleValidate);
}
else if (elem.prop('tagName') == 'INPUT' && elem.attr('type') == 'checkbox')
{
- elem.click(evdata, validator);
- elem.blur(evdata, validator);
+ elem.click(evdata, this.handleValidate);
+ elem.blur(evdata, this.handleValidate);
}
else
{
- elem.keyup(evdata, validator);
- elem.blur(evdata, validator);
+ elem.keyup(evdata, this.handleValidate);
+ elem.blur(evdata, this.handleValidate);
}
- elem.attr('cbi-validate', true).on('validate', evdata, validator);
+ elem.addClass('luci2-field-validate')
+ .on('validate', evdata, this.handleValidate);
return elem;
},
{
var i = this.instance[sid];
- i.widget.find('[cbi-validate]').trigger('validate');
+ i.widget.find('.luci2-field-validate').trigger('validate');
return (i.disabled || i.error.text() == '');
},
- depends: function(d, v)
+ depends: function(d, v, add)
{
var dep;
{
if (typeof(d[i]) == 'string')
dep[d[i]] = true;
- else if (d[i] instanceof AbstractValue)
+ else if (d[i] instanceof L.cbi.AbstractValue)
dep[d[i].name] = true;
}
}
- else if (d instanceof AbstractValue)
+ else if (d instanceof L.cbi.AbstractValue)
{
dep = { };
dep[d.name] = (typeof(v) == 'undefined') ? true : v;
for (var field in dep)
{
- var f = this.section.fields[field];
+ var f = this.ownerSection.fields[field];
if (f)
f.rdependency[this.name] = this;
else
if ($.isEmptyObject(dep))
return this;
- this.dependencies.push(dep);
+ if (!add || !this.dependencies.length)
+ this.dependencies.push(dep);
+ else
+ for (var i = 0; i < this.dependencies.length; i++)
+ $.extend(this.dependencies[i], dep);
return this;
},
for (var field in d[n])
{
- var val = this.section.fields[field].formvalue(sid);
+ var val = this.ownerSection.fields[field].formvalue(sid);
var cmp = d[n][field];
if (typeof(cmp) == 'boolean')
break;
}
}
- else if (typeof(cmp) == 'string')
+ else if (typeof(cmp) == 'string' || typeof(cmp) == 'number')
{
if (val != cmp)
{
if (i.disabled)
{
i.disabled = false;
+ i.top.removeClass('luci2-field-disabled');
i.top.fadeIn();
}
{
i.disabled = true;
i.top.is(':visible') ? i.top.fadeOut() : i.top.hide();
+ i.top.addClass('luci2-field-disabled');
}
return false;
}
});
- this.cbi.CheckboxValue = AbstractValue.extend({
+ this.cbi.CheckboxValue = this.cbi.AbstractValue.extend({
widget: function(sid)
{
var o = this.options;
.attr('type', 'checkbox')
.prop('checked', this.ucivalue(sid));
- return this.validator(sid, i);
+ return $('<div />')
+ .addClass('checkbox')
+ .append(this.attachEvents(sid, i));
},
ucivalue: function(sid)
if (this.instance[sid].disabled)
{
if (!this.options.keep)
- return this.map.set(uci.config, uci.section, uci.option, undefined);
+ return this.ownerMap.set(uci.config, uci.section, uci.option, undefined);
return false;
}
if (chg)
{
- val = val ? this.options.enabled : this.options.disabled;
-
if (this.options.optional && val == this.options.initial)
- this.map.set(uci.config, uci.section, uci.option, undefined);
+ this.ownerMap.set(uci.config, uci.section, uci.option, undefined);
else
- this.map.set(uci.config, uci.section, uci.option, val);
+ this.ownerMap.set(uci.config, uci.section, uci.option, val ? this.options.enabled : this.options.disabled);
}
return chg;
}
});
- this.cbi.InputValue = AbstractValue.extend({
+ this.cbi.InputValue = this.cbi.AbstractValue.extend({
widget: function(sid)
{
var i = $('<input />')
+ .addClass('form-control')
.attr('id', this.id(sid))
.attr('type', 'text')
.attr('placeholder', this.options.placeholder)
.val(this.ucivalue(sid));
- return this.validator(sid, i);
+ return this.attachEvents(sid, i);
}
});
- this.cbi.PasswordValue = AbstractValue.extend({
+ this.cbi.PasswordValue = this.cbi.AbstractValue.extend({
widget: function(sid)
{
var i = $('<input />')
+ .addClass('form-control')
.attr('id', this.id(sid))
.attr('type', 'password')
.attr('placeholder', this.options.placeholder)
.val(this.ucivalue(sid));
- var t = $('<img />')
- .attr('src', _luci2.globals.resource + '/icons/cbi/reload.gif')
- .attr('title', _luci2.tr('Reveal or hide password'))
- .addClass('cbi-button')
- .click(function(ev) {
- var i = $(this).prev();
- var t = i.attr('type');
- i.attr('type', (t == 'password') ? 'text' : 'password');
- i = t = null;
- });
+ var t = $('<span />')
+ .addClass('input-group-btn')
+ .append(L.ui.button(L.tr('Reveal'), 'default')
+ .click(function(ev) {
+ var b = $(this);
+ var i = b.parent().prev();
+ var t = i.attr('type');
+ b.text(t == 'password' ? L.tr('Hide') : L.tr('Reveal'));
+ i.attr('type', (t == 'password') ? 'text' : 'password');
+ b = i = t = null;
+ }));
- this.validator(sid, i);
+ this.attachEvents(sid, i);
return $('<div />')
- .addClass('cbi-input-password')
+ .addClass('input-group')
.append(i)
.append(t);
}
});
- this.cbi.ListValue = AbstractValue.extend({
+ this.cbi.ListValue = this.cbi.AbstractValue.extend({
widget: function(sid)
{
- var s = $('<select />');
+ var s = $('<select />')
+ .addClass('form-control');
- if (this.options.optional)
+ if (this.options.optional && !this.has_empty)
$('<option />')
.attr('value', '')
- .text(_luci2.tr('-- Please choose --'))
+ .text(L.tr('-- Please choose --'))
.appendTo(s);
if (this.choices)
s.attr('id', this.id(sid)).val(this.ucivalue(sid));
- return this.validator(sid, s);
+ return this.attachEvents(sid, s);
},
value: function(k, v)
if (!this.choices)
this.choices = [ ];
+ if (k == '')
+ this.has_empty = true;
+
this.choices.push([k, v || k]);
return this;
}
for (var i = 0; i < this.choices.length; i++)
{
$('<label />')
+ .addClass('checkbox')
.append($('<input />')
- .addClass('cbi-input-checkbox')
.attr('type', 'checkbox')
.attr('value', this.choices[i][0])
.prop('checked', s[this.choices[i][0]]))
.append(this.choices[i][1])
.appendTo(t);
-
- $('<br />')
- .appendTo(t);
}
return t;
}
});
- this.cbi.ComboBox = AbstractValue.extend({
+ this.cbi.ComboBox = this.cbi.AbstractValue.extend({
_change: function(ev)
{
var s = ev.target;
{
ev.data.select.hide();
ev.data.input.show().focus();
-
- var v = ev.data.input.val();
- ev.data.input.val(' ');
- ev.data.input.val(v);
+ ev.data.input.val('');
}
else if (self.options.optional && s.selectedIndex == 0)
{
{
ev.data.input.val(ev.data.select.val());
}
+
+ ev.stopPropagation();
},
_blur: function(ev)
ev.data.select.empty();
- if (self.options.optional)
+ if (self.options.optional && !self.has_empty)
$('<option />')
.attr('value', '')
- .text(_luci2.tr('-- please choose --'))
+ .text(L.tr('-- please choose --'))
.appendTo(ev.data.select);
if (self.choices)
$('<option />')
.attr('value', ' ')
- .text(_luci2.tr('-- custom --'))
+ .text(L.tr('-- custom --'))
.appendTo(ev.data.select);
ev.data.input.hide();
- ev.data.select.val(val).show().focus();
+ ev.data.select.val(val).show().blur();
},
_enter: function(ev)
.attr('id', this.id(sid));
var t = $('<input />')
+ .addClass('form-control')
.attr('type', 'text')
.hide()
.appendTo(d);
var s = $('<select />')
+ .addClass('form-control')
.appendTo(d);
var evdata = {
self: this,
- input: this.validator(sid, t),
- select: this.validator(sid, s)
+ input: t,
+ select: s
};
s.change(evdata, this._change);
t.val(this.ucivalue(sid));
t.blur();
+ this.attachEvents(sid, t);
+ this.attachEvents(sid, s);
+
return d;
},
if (!this.choices)
this.choices = [ ];
+ if (k == '')
+ this.has_empty = true;
+
this.choices.push([k, v || k]);
return this;
},
var v = s.values || [ ];
delete s.values;
- $(s.parent).children('input').each(function(i) {
+ $(s.parent).children('div.input-group').children('input').each(function(i) {
if (i != del)
v.push(this.value || '');
});
sid: s.sid,
self: s.self,
parent: s.parent,
- index: i
+ index: i,
+ remove: ((i+1) < v.length)
};
+ var btn;
+ if (evdata.remove)
+ btn = L.ui.button('–', 'danger').click(evdata, this._btnclick);
+ else
+ btn = L.ui.button('+', 'success').click(evdata, this._btnclick);
+
if (this.choices)
{
var txt = $('<input />')
+ .addClass('form-control')
.attr('type', 'text')
- .hide()
- .appendTo(s.parent);
+ .hide();
var sel = $('<select />')
+ .addClass('form-control');
+
+ $('<div />')
+ .addClass('input-group')
+ .append(txt)
+ .append(sel)
+ .append($('<span />')
+ .addClass('input-group-btn')
+ .append(btn))
.appendTo(s.parent);
- evdata.input = this.validator(s.sid, txt, true);
- evdata.select = this.validator(s.sid, sel, true);
+ evdata.input = this.attachEvents(s.sid, txt);
+ evdata.select = this.attachEvents(s.sid, sel);
sel.change(evdata, this._change);
txt.blur(evdata, this._blur);
.attr('type', 'text')
.attr('index', i)
.attr('placeholder', (i == 0) ? this.options.placeholder : '')
- .addClass('cbi-input-text')
+ .addClass('form-control')
.keydown(evdata, this._keydown)
.keypress(evdata, this._keypress)
.val(v[i]);
- f.appendTo(s.parent);
+ $('<div />')
+ .addClass('input-group')
+ .append(f)
+ .append($('<span />')
+ .addClass('input-group-btn')
+ .append(btn))
+ .appendTo(s.parent);
if (i == focus)
{
f.val(val);
}
- evdata.input = this.validator(s.sid, f, true);
-
- f = null;
- }
-
- $('<img />')
- .attr('src', _luci2.globals.resource + ((i+1) < v.length ? '/icons/cbi/remove.gif' : '/icons/cbi/add.gif'))
- .attr('title', (i+1) < v.length ? _luci2.tr('Remove entry') : _luci2.tr('Add entry'))
- .addClass('cbi-button')
- .click(evdata, this._btnclick)
- .appendTo(s.parent);
+ evdata.input = this.attachEvents(s.sid, f);
- $('<br />')
- .appendTo(s.parent);
+ f = null;
+ }
evdata = null;
}
/* arrow up */
case 38:
- var prev = input.prevAll('input:first');
+ var prev = input.parent().prevAll('div.input-group:first').children('input');
if (prev.is(':visible'))
prev.focus();
else
/* arrow down */
case 40:
- var next = input.nextAll('input:first');
+ var next = input.parent().nextAll('div.input-group:first').children('input');
if (next.is(':visible'))
next.focus();
else
{
if (!this.getAttribute('disabled'))
{
- if (ev.target.src.indexOf('remove') > -1)
+ if (ev.data.remove)
{
var index = ev.data.index;
ev.data.self._redraw(-index, -1, index, ev.data);
formvalue: function(sid)
{
var rv = [ ];
- var fields = $('#' + this.id(sid) + ' > input');
+ var fields = $('#' + this.id(sid) + ' input');
for (var i = 0; i < fields.length; i++)
if (typeof(fields[i].value) == 'string' && fields[i].value.length)
}
});
- this.cbi.DummyValue = AbstractValue.extend({
+ this.cbi.DummyValue = this.cbi.AbstractValue.extend({
widget: function(sid)
{
return $('<div />')
- .addClass('cbi-value-dummy')
+ .addClass('form-control-static')
.attr('id', this.id(sid))
- .html(this.ucivalue(sid));
+ .html(this.ucivalue(sid) || this.label('placeholder'));
},
formvalue: function(sid)
}
});
- this.cbi.NetworkList = AbstractValue.extend({
- load: function(sid)
+ this.cbi.ButtonValue = this.cbi.AbstractValue.extend({
+ widget: function(sid)
{
- var self = this;
+ this.options.optional = true;
- if (!self.interfaces)
- {
- self.interfaces = [ ];
- return _luci2.network.getNetworkStatus().then(function(ifaces) {
- self.interfaces = ifaces;
- self = null;
- });
- }
+ var btn = $('<button />')
+ .addClass('btn btn-default')
+ .attr('id', this.id(sid))
+ .attr('type', 'button')
+ .text(this.label('text'));
- return undefined;
+ return this.attachEvents(sid, btn);
+ }
+ });
+
+ this.cbi.NetworkList = this.cbi.AbstractValue.extend({
+ load: function(sid)
+ {
+ return L.NetworkModel.init();
},
_device_icon: function(dev)
{
- var type = 'ethernet';
- var desc = _luci2.tr('Ethernet device');
-
- if (dev.type == 'IP tunnel')
- {
- type = 'tunnel';
- desc = _luci2.tr('Tunnel interface');
- }
- else if (dev['bridge-members'])
- {
- type = 'bridge';
- desc = _luci2.tr('Bridge');
- }
- else if (dev.wireless)
- {
- type = 'wifi';
- desc = _luci2.tr('Wireless Network');
- }
- else if (dev.device.indexOf('.') > 0)
- {
- type = 'vlan';
- desc = _luci2.tr('VLAN interface');
- }
-
return $('<img />')
- .attr('src', _luci2.globals.resource + '/icons/' + type + (dev.up ? '' : '_disabled') + '.png')
- .attr('title', '%s (%s)'.format(desc, dev.device));
+ .attr('src', dev.icon())
+ .attr('title', '%s (%s)'.format(dev.description(), dev.name() || '?'));
},
widget: function(sid)
var id = this.id(sid);
var ul = $('<ul />')
.attr('id', id)
- .addClass('cbi-input-networks');
+ .addClass('list-unstyled');
var itype = this.options.multiple ? 'checkbox' : 'radio';
var value = this.ucivalue(sid);
for (var i = 0; i < value.length; i++)
check[value[i]] = true;
- if (this.interfaces)
+ var interfaces = L.NetworkModel.getInterfaces();
+
+ for (var i = 0; i < interfaces.length; i++)
{
- for (var i = 0; i < this.interfaces.length; i++)
- {
- var iface = this.interfaces[i];
- var badge = $('<span />')
- .addClass('ifacebadge')
- .text('%s: '.format(iface['interface']));
-
- if (iface.device && iface.device.subdevices)
- for (var j = 0; j < iface.device.subdevices.length; j++)
- badge.append(this._device_icon(iface.device.subdevices[j]));
- else if (iface.device)
- badge.append(this._device_icon(iface.device));
- else
- badge.append($('<em />').text(_luci2.tr('(No devices attached)')));
+ var iface = interfaces[i];
- $('<li />')
- .append($('<label />')
- .append($('<input />')
- .attr('name', itype + id)
- .attr('type', itype)
- .attr('value', iface['interface'])
- .prop('checked', !!check[iface['interface']])
- .addClass('cbi-input-' + itype))
- .append(badge))
- .appendTo(ul);
- }
+ $('<li />')
+ .append($('<label />')
+ .addClass(itype + ' inline')
+ .append(this.attachEvents(sid, $('<input />')
+ .attr('name', itype + id)
+ .attr('type', itype)
+ .attr('value', iface.name())
+ .prop('checked', !!check[iface.name()])))
+ .append(iface.renderBadge()))
+ .appendTo(ul);
}
if (!this.options.multiple)
{
$('<li />')
.append($('<label />')
- .append($('<input />')
+ .addClass(itype + ' inline text-muted')
+ .append(this.attachEvents(sid, $('<input />')
.attr('name', itype + id)
.attr('type', itype)
.attr('value', '')
- .prop('checked', !value)
- .addClass('cbi-input-' + itype))
- .append(_luci2.tr('unspecified')))
+ .prop('checked', $.isEmptyObject(check))))
+ .append(L.tr('unspecified')))
.appendTo(ul);
}
}
});
+ this.cbi.DeviceList = this.cbi.NetworkList.extend({
+ handleFocus: function(ev)
+ {
+ var self = ev.data.self;
+ var input = $(this);
+
+ input.parent().prev().prop('checked', true);
+ },
+
+ handleBlur: function(ev)
+ {
+ ev.which = 10;
+ ev.data.self.handleKeydown.call(this, ev);
+ },
+
+ handleKeydown: function(ev)
+ {
+ if (ev.which != 10 && ev.which != 13)
+ return;
+
+ var sid = ev.data.sid;
+ var self = ev.data.self;
+ var input = $(this);
+ var ifnames = L.toArray(input.val());
+
+ if (!ifnames.length)
+ return;
+
+ L.NetworkModel.createDevice(ifnames[0]);
+
+ self._redraw(sid, $('#' + self.id(sid)), ifnames[0]);
+ },
+
+ load: function(sid)
+ {
+ return L.NetworkModel.init();
+ },
+
+ _redraw: function(sid, ul, sel)
+ {
+ var id = ul.attr('id');
+ var devs = L.NetworkModel.getDevices();
+ var iface = L.NetworkModel.getInterface(sid);
+ var itype = this.options.multiple ? 'checkbox' : 'radio';
+ var check = { };
+
+ if (!sel)
+ {
+ for (var i = 0; i < devs.length; i++)
+ if (devs[i].isInNetwork(iface))
+ check[devs[i].name()] = true;
+ }
+ else
+ {
+ if (this.options.multiple)
+ check = L.toObject(this.formvalue(sid));
+
+ check[sel] = true;
+ }
+
+ ul.empty();
+
+ for (var i = 0; i < devs.length; i++)
+ {
+ var dev = devs[i];
+
+ if (dev.isBridge() && this.options.bridges === false)
+ continue;
+
+ if (!dev.isBridgeable() && this.options.multiple)
+ continue;
+
+ var badge = $('<span />')
+ .addClass('badge')
+ .append($('<img />').attr('src', dev.icon()))
+ .append(' %s: %s'.format(dev.name(), dev.description()));
+
+ //var ifcs = dev.getInterfaces();
+ //if (ifcs.length)
+ //{
+ // for (var j = 0; j < ifcs.length; j++)
+ // badge.append((j ? ', ' : ' (') + ifcs[j].name());
+ //
+ // badge.append(')');
+ //}
+
+ $('<li />')
+ .append($('<label />')
+ .addClass(itype + ' inline')
+ .append($('<input />')
+ .attr('name', itype + id)
+ .attr('type', itype)
+ .attr('value', dev.name())
+ .prop('checked', !!check[dev.name()]))
+ .append(badge))
+ .appendTo(ul);
+ }
+
+
+ $('<li />')
+ .append($('<label />')
+ .attr('for', 'custom' + id)
+ .addClass(itype + ' inline')
+ .append($('<input />')
+ .attr('name', itype + id)
+ .attr('type', itype)
+ .attr('value', ''))
+ .append($('<span />')
+ .addClass('badge')
+ .append($('<input />')
+ .attr('id', 'custom' + id)
+ .attr('type', 'text')
+ .attr('placeholder', L.tr('Custom device …'))
+ .on('focus', { self: this, sid: sid }, this.handleFocus)
+ .on('blur', { self: this, sid: sid }, this.handleBlur)
+ .on('keydown', { self: this, sid: sid }, this.handleKeydown))))
+ .appendTo(ul);
+
+ if (!this.options.multiple)
+ {
+ $('<li />')
+ .append($('<label />')
+ .addClass(itype + ' inline text-muted')
+ .append($('<input />')
+ .attr('name', itype + id)
+ .attr('type', itype)
+ .attr('value', '')
+ .prop('checked', $.isEmptyObject(check)))
+ .append(L.tr('unspecified')))
+ .appendTo(ul);
+ }
+ },
+
+ widget: function(sid)
+ {
+ var id = this.id(sid);
+ var ul = $('<ul />')
+ .attr('id', id)
+ .addClass('list-unstyled');
+
+ this._redraw(sid, ul);
+
+ return ul;
+ },
+
+ save: function(sid)
+ {
+ if (this.instance[sid].disabled)
+ return;
+
+ var ifnames = this.formvalue(sid);
+ //if (!ifnames)
+ // return;
+
+ var iface = L.NetworkModel.getInterface(sid);
+ if (!iface)
+ return;
+
+ iface.setDevices($.isArray(ifnames) ? ifnames : [ ifnames ]);
+ }
+ });
+
- var AbstractSection = AbstractWidget.extend({
+ this.cbi.AbstractSection = this.ui.AbstractWidget.extend({
id: function()
{
- var s = [ arguments[0], this.map.uci_package, this.uci_type ];
+ var s = [ arguments[0], this.ownerMap.uci_package, this.uci_type ];
- for (var i = 1; i < arguments.length; i++)
+ for (var i = 1; i < arguments.length && typeof(arguments[i]) == 'string'; i++)
s.push(arguments[i].replace(/\./g, '_'));
return s.join('_');
var w = widget ? new widget(name, options) : null;
- if (!(w instanceof AbstractValue))
+ if (!(w instanceof L.cbi.AbstractValue))
throw 'Widget must be an instance of AbstractValue';
- w.section = this;
- w.map = this.map;
+ w.ownerSection = this;
+ w.ownerMap = this.ownerMap;
this.fields[name] = w;
tab.fields.push(w);
return w;
},
- ucipackages: function(pkg)
+ tabtoggle: function(sid)
{
for (var i = 0; i < this.tabs.length; i++)
- for (var j = 0; j < this.tabs[i].fields.length; j++)
- if (this.tabs[i].fields[j].options.uci_package)
- pkg[this.tabs[i].fields[j].options.uci_package] = true;
+ {
+ var tab = this.tabs[i];
+ var elem = $('#' + this.id('nodetab', sid, tab.id));
+ var empty = true;
+
+ for (var j = 0; j < tab.fields.length; j++)
+ {
+ if (tab.fields[j].active(sid))
+ {
+ empty = false;
+ break;
+ }
+ }
+
+ if (empty && elem.is(':visible'))
+ elem.fadeOut();
+ else if (!empty)
+ elem.fadeIn();
+ }
},
- formvalue: function()
+ validate: function(parent_sid)
{
- var rv = { };
+ var s = this.getUCISections(parent_sid);
+ var n = 0;
- this.sections(function(s) {
- var sid = s['.name'];
- var sv = rv[sid] || (rv[sid] = { });
+ for (var i = 0; i < s.length; i++)
+ {
+ var $item = $('#' + this.id('sectionitem', s[i]['.name']));
+
+ $item.find('.luci2-field-validate').trigger('validate');
+ n += $item.find('.luci2-field.luci2-form-error').not('.luci2-field-disabled').length;
+ }
+
+ return (n == 0);
+ },
+
+ load: function(parent_sid)
+ {
+ var deferreds = [ ];
+
+ var s = this.getUCISections(parent_sid);
+ for (var i = 0; i < s.length; i++)
+ {
+ for (var f in this.fields)
+ {
+ if (typeof(this.fields[f].load) != 'function')
+ continue;
+
+ var rv = this.fields[f].load(s[i]['.name']);
+ if (L.isDeferred(rv))
+ deferreds.push(rv);
+ }
+
+ for (var j = 0; j < this.subsections.length; j++)
+ {
+ var rv = this.subsections[j].load(s[i]['.name']);
+ deferreds.push.apply(deferreds, rv);
+ }
+ }
- for (var i = 0; i < this.tabs.length; i++)
- for (var j = 0; j < this.tabs[i].fields.length; j++)
+ return deferreds;
+ },
+
+ save: function(parent_sid)
+ {
+ var deferreds = [ ];
+ var s = this.getUCISections(parent_sid);
+
+ for (i = 0; i < s.length; i++)
+ {
+ if (!this.options.readonly)
+ {
+ for (var f in this.fields)
{
- var val = this.tabs[i].fields[j].formvalue(sid);
- sv[this.tabs[i].fields[j].name] = val;
+ if (typeof(this.fields[f].save) != 'function')
+ continue;
+
+ var rv = this.fields[f].save(s[i]['.name']);
+ if (L.isDeferred(rv))
+ deferreds.push(rv);
}
- });
+ }
- return rv;
+ for (var j = 0; j < this.subsections.length; j++)
+ {
+ var rv = this.subsections[j].save(s[i]['.name']);
+ deferreds.push.apply(deferreds, rv);
+ }
+ }
+
+ return deferreds;
},
- validate: function(sid)
+ teaser: function(sid)
{
- var rv = true;
+ var tf = this.teaser_fields;
- if (!sid)
+ if (!tf)
{
- var as = this.sections();
- for (var i = 0; i < as.length; i++)
- if (!this.validate(as[i]['.name']))
- rv = false;
- return rv;
+ tf = this.teaser_fields = [ ];
+
+ if ($.isArray(this.options.teasers))
+ {
+ for (var i = 0; i < this.options.teasers.length; i++)
+ {
+ var f = this.options.teasers[i];
+ if (f instanceof L.cbi.AbstractValue)
+ tf.push(f);
+ else if (typeof(f) == 'string' && this.fields[f] instanceof L.cbi.AbstractValue)
+ tf.push(this.fields[f]);
+ }
+ }
+ else
+ {
+ for (var i = 0; tf.length <= 5 && i < this.tabs.length; i++)
+ for (var j = 0; tf.length <= 5 && j < this.tabs[i].fields.length; j++)
+ tf.push(this.tabs[i].fields[j]);
+ }
}
- var inst = this.instance[sid];
- var sv = rv[sid] || (rv[sid] = { });
+ var t = '';
- var invals = 0;
- var legend = $('#' + this.id('sort', sid)).find('legend:first');
+ for (var i = 0; i < tf.length; i++)
+ {
+ if (tf[i].instance[sid] && tf[i].instance[sid].disabled)
+ continue;
- legend.children('span').detach();
+ var n = tf[i].options.caption || tf[i].name;
+ var v = tf[i].textvalue(sid);
- for (var i = 0; i < this.tabs.length; i++)
- {
- var inval = 0;
- var tab = $('#' + this.id('tabhead', sid, this.tabs[i].id));
+ if (typeof(v) == 'undefined')
+ continue;
+
+ t = t + '%s%s: <strong>%s</strong>'.format(t ? ' | ' : '', n, v);
+ }
+
+ return t;
+ },
- tab.children('span').detach();
+ findAdditionalUCIPackages: function()
+ {
+ var packages = [ ];
+ for (var i = 0; i < this.tabs.length; i++)
for (var j = 0; j < this.tabs[i].fields.length; j++)
- if (!this.tabs[i].fields[j].validate(sid))
- inval++;
+ if (this.tabs[i].fields[j].options.uci_package)
+ packages.push(this.tabs[i].fields[j].options.uci_package);
- if (inval > 0)
- {
- $('<span />')
- .addClass('badge')
- .attr('title', _luci2.tr('%d Errors'.format(inval)))
- .text(inval)
- .appendTo(tab);
+ return packages;
+ },
- invals += inval;
- tab = null;
- rv = false;
- }
- }
+ findParentSectionIDs: function($elem)
+ {
+ var rv = [ ];
+ var $parents = $elem.parents('.luci2-section-item');
- if (invals > 0)
- $('<span />')
- .addClass('badge')
- .attr('title', _luci2.tr('%d Errors'.format(invals)))
- .text(invals)
- .appendTo(legend);
+ for (var i = 0; i < $parents.length; i++)
+ rv.push($parents[i].getAttribute('data-luci2-sid'));
return rv;
}
});
- this.cbi.TypedSection = AbstractSection.extend({
+ this.cbi.TypedSection = this.cbi.AbstractSection.extend({
init: function(uci_type, options)
{
this.uci_type = uci_type;
this.options = options;
this.tabs = [ ];
this.fields = { };
- this.active_panel = 0;
+ this.subsections = [ ];
+ this.active_panel = { };
this.active_tab = { };
+
+ this.instance = { };
},
- filter: function(section)
+ filter: function(section, parent_sid)
{
return true;
},
- sections: function(cb)
+ sort: function(section1, section2)
+ {
+ return 0;
+ },
+
+ subsection: function(widget, uci_type, options)
+ {
+ var w = widget ? new widget(uci_type, options) : null;
+
+ if (!(w instanceof L.cbi.AbstractSection))
+ throw 'Widget must be an instance of AbstractSection';
+
+ w.ownerSection = this;
+ w.ownerMap = this.ownerMap;
+ w.index = this.subsections.length;
+
+ this.subsections.push(w);
+ return w;
+ },
+
+ getUCISections: function(parent_sid)
{
- var s1 = this.map.ucisections(this.map.uci_package);
+ var s1 = L.uci.sections(this.ownerMap.uci_package);
var s2 = [ ];
for (var i = 0; i < s1.length; i++)
if (s1[i]['.type'] == this.uci_type)
- if (this.filter(s1[i]))
+ if (this.filter(s1[i], parent_sid))
s2.push(s1[i]);
- if (typeof(cb) == 'function')
- for (var i = 0; i < s2.length; i++)
- cb.apply(this, [ s2[i] ]);
+ s2.sort(this.sort);
return s2;
},
- add: function(name)
+ add: function(name, parent_sid)
{
- this.map.add(this.map.uci_package, this.uci_type, name);
+ return this.ownerMap.add(this.ownerMap.uci_package, this.uci_type, name);
},
- remove: function(sid)
+ remove: function(sid, parent_sid)
{
- this.map.remove(this.map.uci_package, sid);
+ return this.ownerMap.remove(this.ownerMap.uci_package, sid);
},
- _add: function(ev)
+ handleAdd: function(ev)
{
var addb = $(this);
var name = undefined;
var self = ev.data.self;
+ var sid = self.findParentSectionIDs(addb)[0];
if (addb.prev().prop('nodeName') == 'INPUT')
name = addb.prev().val();
if (addb.prop('disabled') || name === '')
return;
- self.active_panel = -1;
- self.map.save();
- self.add(name);
- self.map.redraw();
- },
+ L.ui.saveScrollTop();
+
+ self.setPanelIndex(sid, -1);
+ self.ownerMap.save();
+
+ ev.data.sid = self.add(name, sid);
+ ev.data.type = self.uci_type;
+ ev.data.name = name;
+
+ self.trigger('add', ev);
+
+ self.ownerMap.redraw();
+
+ L.ui.restoreScrollTop();
+ },
+
+ handleRemove: function(ev)
+ {
+ var self = ev.data.self;
+ var sids = self.findParentSectionIDs($(this));
+
+ if (sids.length)
+ {
+ L.ui.saveScrollTop();
+
+ ev.sid = sids[0];
+ ev.parent_sid = sids[1];
- _remove: function(ev)
- {
- var self = ev.data.self;
- var sid = ev.data.sid;
+ self.trigger('remove', ev);
+
+ self.ownerMap.save();
+ self.remove(ev.sid, ev.parent_sid);
+ self.ownerMap.redraw();
- self.map.save();
- self.remove(sid);
- self.map.redraw();
+ L.ui.restoreScrollTop();
+ }
ev.stopPropagation();
},
- _sid: function(ev)
+ handleSID: function(ev)
{
var self = ev.data.self;
var text = $(this);
var addb = text.next();
var errt = addb.next();
var name = text.val();
- var used = false;
if (!/^[a-zA-Z0-9_]*$/.test(name))
{
- errt.text(_luci2.tr('Invalid section name')).show();
+ errt.text(L.tr('Invalid section name')).show();
text.addClass('error');
addb.prop('disabled', true);
return false;
}
- for (var sid in self.map.uci.values[self.map.uci_package])
- if (sid == name)
- {
- used = true;
- break;
- }
-
- for (var sid in self.map.uci.creates[self.map.uci_package])
- if (sid == name)
- {
- used = true;
- break;
- }
-
- if (used)
+ if (L.uci.get(self.ownerMap.uci_package, name))
{
- errt.text(_luci2.tr('Name already used')).show();
+ errt.text(L.tr('Name already used')).show();
text.addClass('error');
addb.prop('disabled', true);
return false;
return true;
},
- teaser: function(sid)
+ handleTab: function(ev)
{
- var tf = this.teaser_fields;
+ var self = ev.data.self;
+ var $tab = $(this);
+ var sid = self.findParentSectionIDs($tab)[0];
- if (!tf)
- {
- tf = this.teaser_fields = [ ];
+ self.active_tab[sid] = $tab.parent().index();
+ },
- if ($.isArray(this.options.teasers))
- {
- for (var i = 0; i < this.options.teasers.length; i++)
- {
- var f = this.options.teasers[i];
- if (f instanceof AbstractValue)
- tf.push(f);
- else if (typeof(f) == 'string' && this.fields[f] instanceof AbstractValue)
- tf.push(this.fields[f]);
- }
- }
- else
- {
- for (var i = 0; tf.length <= 5 && i < this.tabs.length; i++)
- for (var j = 0; tf.length <= 5 && j < this.tabs[i].fields.length; j++)
- tf.push(this.tabs[i].fields[j]);
- }
- }
+ handleTabValidate: function(ev)
+ {
+ var $pane = $(ev.delegateTarget);
+ var $badge = $pane.parent()
+ .children('.nav-tabs')
+ .children('li')
+ .eq($pane.index() - 1) // item #1 is the <ul>
+ .find('.badge:first');
- var t = '';
+ var err_count = $pane.find('.luci2-field.luci2-form-error').not('.luci2-field-disabled').length;
+ if (err_count > 0)
+ $badge
+ .text(err_count)
+ .attr('title', L.trp('1 Error', '%d Errors', err_count).format(err_count))
+ .show();
+ else
+ $badge.hide();
+ },
- for (var i = 0; i < tf.length; i++)
- {
- if (tf[i].instance[sid] && tf[i].instance[sid].disabled)
- continue;
+ handlePanelValidate: function(ev)
+ {
+ var $elem = $(this);
+ var $badge = $elem
+ .prevAll('.luci2-section-header:first')
+ .children('.luci2-section-teaser')
+ .find('.badge:first');
- var n = tf[i].options.caption || tf[i].name;
- var v = tf[i].textvalue(sid);
+ var err_count = $elem.find('.luci2-field.luci2-form-error').not('.luci2-field-disabled').length;
+ if (err_count > 0)
+ $badge
+ .text(err_count)
+ .attr('title', L.trp('1 Error', '%d Errors', err_count).format(err_count))
+ .show();
+ else
+ $badge.hide();
+ },
- if (typeof(v) == 'undefined')
- continue;
+ handlePanelCollapse: function(ev)
+ {
+ var self = ev.data.self;
- t = t + '%s%s: <strong>%s</strong>'.format(t ? ' | ' : '', n, v);
+ var $items = $(ev.delegateTarget).children('.luci2-section-item');
+
+ var $this_panel = $(ev.target);
+ var $this_teaser = $this_panel.prevAll('.luci2-section-header:first').children('.luci2-section-teaser');
+
+ var $prev_panel = $items.children('.luci2-section-panel.in');
+ var $prev_teaser = $prev_panel.prevAll('.luci2-section-header:first').children('.luci2-section-teaser');
+
+ var sids = self.findParentSectionIDs($prev_panel);
+
+ self.setPanelIndex(sids[1], $this_panel.parent().index());
+
+ $prev_panel
+ .removeClass('in')
+ .addClass('collapse');
+
+ $prev_teaser
+ .show()
+ .children('span:last')
+ .empty()
+ .append(self.teaser(sids[0]));
+
+ $this_teaser
+ .hide();
+
+ ev.stopPropagation();
+ },
+
+ handleSort: function(ev)
+ {
+ var self = ev.data.self;
+
+ var $item = $(this).parents('.luci2-section-item:first');
+ var $next = ev.data.up ? $item.prev() : $item.next();
+
+ if ($item.length && $next.length)
+ {
+ var cur_sid = $item.attr('data-luci2-sid');
+ var new_sid = $next.attr('data-luci2-sid');
+
+ L.uci.swap(self.ownerMap.uci_package, cur_sid, new_sid);
+
+ self.ownerMap.save();
+ self.ownerMap.redraw();
}
- return t;
+ ev.stopPropagation();
+ },
+
+ getPanelIndex: function(parent_sid)
+ {
+ return (this.active_panel[parent_sid || '__top__'] || 0);
+ },
+
+ setPanelIndex: function(parent_sid, new_index)
+ {
+ if (typeof(new_index) == 'number')
+ this.active_panel[parent_sid || '__top__'] = new_index;
},
- _render_add: function()
+ renderAdd: function()
{
- var text = _luci2.tr('Add section');
- var ttip = _luci2.tr('Create new section...');
+ if (!this.options.addremove)
+ return null;
+
+ var text = L.tr('Add section');
+ var ttip = L.tr('Create new section...');
if ($.isArray(this.options.add_caption))
text = this.options.add_caption[0], ttip = this.options.add_caption[1];
else if (typeof(this.options.add_caption) == 'string')
text = this.options.add_caption, ttip = '';
- var add = $('<div />').addClass('cbi-section-add');
+ var add = $('<div />');
if (this.options.anonymous === false)
{
.addClass('cbi-input-text')
.attr('type', 'text')
.attr('placeholder', ttip)
- .blur({ self: this }, this._sid)
- .keyup({ self: this }, this._sid)
+ .blur({ self: this }, this.handleSID)
+ .keyup({ self: this }, this.handleSID)
.appendTo(add);
$('<img />')
- .attr('src', _luci2.globals.resource + '/icons/cbi/add.gif')
+ .attr('src', L.globals.resource + '/icons/cbi/add.gif')
.attr('title', text)
.addClass('cbi-button')
- .click({ self: this }, this._add)
+ .click({ self: this }, this.handleAdd)
.appendTo(add);
$('<div />')
}
else
{
- $('<input />')
- .attr('type', 'button')
- .addClass('cbi-button')
- .addClass('cbi-button-add')
- .val(text).attr('title', ttip)
- .click({ self: this }, this._add)
- .appendTo(add)
+ L.ui.button(text, 'success', ttip)
+ .click({ self: this }, this.handleAdd)
+ .appendTo(add);
}
return add;
},
- _render_remove: function(sid)
+ renderRemove: function(index)
{
- var text = _luci2.tr('Remove');
- var ttip = _luci2.tr('Remove this section');
+ if (!this.options.addremove)
+ return null;
+
+ var text = L.tr('Remove');
+ var ttip = L.tr('Remove this section');
if ($.isArray(this.options.remove_caption))
text = this.options.remove_caption[0], ttip = this.options.remove_caption[1];
else if (typeof(this.options.remove_caption) == 'string')
text = this.options.remove_caption, ttip = '';
- return $('<input />')
- .attr('type', 'button')
- .addClass('cbi-button')
- .addClass('cbi-button-remove')
- .val(text).attr('title', ttip)
- .click({ self: this, sid: sid }, this._remove);
+ return L.ui.button(text, 'danger', ttip)
+ .click({ self: this, index: index }, this.handleRemove);
},
- _render_caption: function(sid)
+ renderSort: function(index)
{
- if (typeof(this.options.caption) == 'string')
- {
- return $('<legend />')
- .text(this.options.caption.format(sid));
- }
- else if (typeof(this.options.caption) == 'function')
- {
- return $('<legend />')
- .text(this.options.caption.call(this, sid));
- }
+ if (!this.options.sortable)
+ return null;
- return '';
+ var b1 = L.ui.button('↑', 'info', L.tr('Move up'))
+ .click({ self: this, index: index, up: true }, this.handleSort);
+
+ var b2 = L.ui.button('↓', 'info', L.tr('Move down'))
+ .click({ self: this, index: index, up: false }, this.handleSort);
+
+ return b1.add(b2);
},
- render: function()
+ renderCaption: function()
+ {
+ return $('<h3 />')
+ .addClass('panel-title')
+ .append(this.label('caption') || this.uci_type);
+ },
+
+ renderDescription: function()
{
- var allsections = $();
- var panel_index = 0;
+ var text = this.label('description');
- this.instance = { };
+ if (text)
+ return $('<div />')
+ .addClass('luci2-section-description')
+ .text(text);
- var s = this.sections();
+ return null;
+ },
- if (s.length == 0)
+ renderTeaser: function(sid, index)
+ {
+ if (this.options.collabsible || this.ownerMap.options.collabsible)
{
- var fieldset = $('<fieldset />')
- .addClass('cbi-section');
+ return $('<div />')
+ .attr('id', this.id('teaser', sid))
+ .addClass('luci2-section-teaser well well-sm')
+ .append($('<span />')
+ .addClass('badge'))
+ .append($('<span />'));
+ }
- var head = $('<div />')
- .addClass('cbi-section-head')
- .appendTo(fieldset);
+ return null;
+ },
- head.append(this._render_caption(undefined));
+ renderHead: function(condensed)
+ {
+ if (condensed)
+ return null;
- if (typeof(this.options.description) == 'string')
- {
- $('<div />')
- .addClass('cbi-section-descr')
- .text(this.options.description)
- .appendTo(head);
- }
+ return $('<div />')
+ .addClass('panel-heading')
+ .append(this.renderCaption())
+ .append(this.renderDescription());
+ },
- allsections = allsections.add(fieldset);
- }
+ renderTabDescription: function(sid, index, tab_index)
+ {
+ var tab = this.tabs[tab_index];
- for (var i = 0; i < s.length; i++)
+ if (typeof(tab.description) == 'string')
{
- var sid = s[i]['.name'];
- var inst = this.instance[sid] = { tabs: [ ] };
+ return $('<div />')
+ .addClass('cbi-tab-descr')
+ .text(tab.description);
+ }
- var fieldset = $('<fieldset />')
- .attr('id', this.id('sort', sid))
- .addClass('cbi-section');
+ return null;
+ },
- var head = $('<div />')
- .addClass('cbi-section-head')
- .attr('cbi-section-num', this.index)
- .attr('cbi-section-id', sid);
+ renderTabHead: function(sid, index, tab_index)
+ {
+ var tab = this.tabs[tab_index];
+ var cur = this.active_tab[sid] || 0;
- head.append(this._render_caption(sid));
+ var tabh = $('<li />')
+ .append($('<a />')
+ .attr('id', this.id('nodetab', sid, tab.id))
+ .attr('href', '#' + this.id('node', sid, tab.id))
+ .attr('data-toggle', 'tab')
+ .text((tab.caption ? tab.caption.format(tab.id) : tab.id) + ' ')
+ .append($('<span />')
+ .addClass('badge'))
+ .on('shown.bs.tab', { self: this, sid: sid }, this.handleTab));
- if (typeof(this.options.description) == 'string')
- {
- $('<div />')
- .addClass('cbi-section-descr')
- .text(this.options.description)
- .appendTo(head);
- }
+ if (cur == tab_index)
+ tabh.addClass('active');
- var teaser;
- if ((s.length > 1 && this.options.collabsible) || this.map.options.collabsible)
- teaser = $('<div />')
- .addClass('cbi-section-teaser')
- .appendTo(head);
+ if (!tab.fields.length)
+ tabh.hide();
- if (this.options.addremove)
- $('<div />')
- .addClass('cbi-section-remove')
- .addClass('right')
- .append(this._render_remove(sid))
- .appendTo(head);
+ return tabh;
+ },
- var body = $('<div />')
- .attr('index', panel_index++);
+ renderTabBody: function(sid, index, tab_index)
+ {
+ var tab = this.tabs[tab_index];
+ var cur = this.active_tab[sid] || 0;
- var fields = $('<fieldset />')
- .addClass('cbi-section-node');
+ var tabb = $('<div />')
+ .addClass('tab-pane')
+ .attr('id', this.id('node', sid, tab.id))
+ .append(this.renderTabDescription(sid, index, tab_index))
+ .on('validate', this.handleTabValidate);
- if (this.tabs.length > 1)
- {
- var menu = $('<ul />')
- .addClass('cbi-tabmenu');
+ if (cur == tab_index)
+ tabb.addClass('active');
- for (var j = 0; j < this.tabs.length; j++)
- {
- var tabid = this.id('tab', sid, this.tabs[j].id);
- var theadid = this.id('tabhead', sid, this.tabs[j].id);
+ for (var i = 0; i < tab.fields.length; i++)
+ tabb.append(tab.fields[i].render(sid));
- var tabc = $('<div />')
- .addClass('cbi-tabcontainer')
- .attr('id', tabid)
- .attr('index', j);
+ return tabb;
+ },
- if (typeof(this.tabs[j].description) == 'string')
- {
- $('<div />')
- .addClass('cbi-tab-descr')
- .text(this.tabs[j].description)
- .appendTo(tabc);
- }
+ renderPanelHead: function(sid, index, parent_sid)
+ {
+ var head = $('<div />')
+ .addClass('luci2-section-header')
+ .append(this.renderTeaser(sid, index))
+ .append($('<div />')
+ .addClass('btn-group')
+ .append(this.renderSort(index))
+ .append(this.renderRemove(index)));
- for (var k = 0; k < this.tabs[j].fields.length; k++)
- this.tabs[j].fields[k].render(sid).appendTo(tabc);
+ if (this.options.collabsible)
+ {
+ head.attr('data-toggle', 'collapse')
+ .attr('data-parent', this.id('sectiongroup', parent_sid))
+ .attr('data-target', '#' + this.id('panel', sid));
+ }
- tabc.appendTo(fields);
- tabc = null;
+ return head;
+ },
- $('<li />').attr('id', theadid).append(
- $('<a />')
- .text(this.tabs[j].caption.format(this.tabs[j].id))
- .attr('href', '#' + tabid)
- ).appendTo(menu);
- }
+ renderPanelBody: function(sid, index, parent_sid)
+ {
+ var body = $('<div />')
+ .attr('id', this.id('panel', sid))
+ .addClass('luci2-section-panel')
+ .on('validate', this.handlePanelValidate);
- menu.appendTo(body);
- menu = null;
+ if (this.options.collabsible || this.ownerMap.options.collabsible)
+ {
+ body.addClass('panel-collapse collapse');
- fields.appendTo(body);
- fields = null;
+ if (index == this.getPanelIndex(parent_sid))
+ body.addClass('in');
+ }
- var t = body.tabs({ active: this.active_tab[sid] });
+ var tab_heads = $('<ul />')
+ .addClass('nav nav-tabs');
- t.on('tabsactivate', { self: this, sid: sid }, function(ev, ui) {
- var d = ev.data;
- d.self.validate();
- d.self.active_tab[d.sid] = parseInt(ui.newPanel.attr('index'));
- });
- }
- else
- {
- for (var j = 0; j < this.tabs[0].fields.length; j++)
- this.tabs[0].fields[j].render(sid).appendTo(fields);
+ var tab_bodies = $('<div />')
+ .addClass('form-horizontal tab-content')
+ .append(tab_heads);
- fields.appendTo(body);
- fields = null;
- }
+ for (var j = 0; j < this.tabs.length; j++)
+ {
+ tab_heads.append(this.renderTabHead(sid, index, j));
+ tab_bodies.append(this.renderTabBody(sid, index, j));
+ }
- head.appendTo(fieldset);
- head = null;
+ body.append(tab_bodies);
- body.appendTo(fieldset);
- body = null;
+ if (this.tabs.length <= 1)
+ tab_heads.hide();
- allsections = allsections.add(fieldset);
- fieldset = null;
+ for (var i = 0; i < this.subsections.length; i++)
+ body.append(this.subsections[i].render(false, sid));
- //this.validate(sid);
- //
- //if (teaser)
- // teaser.append(this.teaser(sid));
- }
+ return body;
+ },
- if (this.options.collabsible && s.length > 1)
- {
- var a = $('<div />').append(allsections).accordion({
- header: '> fieldset > div.cbi-section-head',
- heightStyle: 'content',
- active: this.active_panel
- });
+ renderBody: function(condensed, parent_sid)
+ {
+ var s = this.getUCISections(parent_sid);
+ var n = this.getPanelIndex(parent_sid);
- a.on('accordionbeforeactivate', { self: this }, function(ev, ui) {
- var h = ui.oldHeader;
- var s = ev.data.self;
- var i = h.attr('cbi-section-id');
+ if (n < 0)
+ this.setPanelIndex(parent_sid, n + s.length);
+ else if (n >= s.length)
+ this.setPanelIndex(parent_sid, s.length - 1);
- h.children('.cbi-section-teaser').empty().append(s.teaser(i));
- s.validate();
- });
+ var body = $('<ul />')
+ .addClass('luci2-section-group list-group');
- a.on('accordionactivate', { self: this }, function(ev, ui) {
- ev.data.self.active_panel = parseInt(ui.newPanel.attr('index'));
- });
+ if (this.options.collabsible)
+ {
+ body.attr('id', this.id('sectiongroup', parent_sid))
+ .on('show.bs.collapse', { self: this }, this.handlePanelCollapse);
+ }
- if (this.options.sortable)
- {
- var s = a.sortable({
- axis: 'y',
- handle: 'div.cbi-section-head'
- });
+ if (s.length == 0)
+ {
+ body.append($('<li />')
+ .addClass('list-group-item text-muted')
+ .text(this.label('placeholder') || L.tr('There are no entries defined yet.')))
+ }
- s.on('sortupdate', { self: this, ids: s.sortable('toArray') }, function(ev, ui) {
- var sections = [ ];
- for (var i = 0; i < ev.data.ids.length; i++)
- sections.push(ev.data.ids[i].substring(ev.data.ids[i].lastIndexOf('.') + 1));
- _luci2.uci.order(ev.data.self.map.uci_package, sections);
- });
+ for (var i = 0; i < s.length; i++)
+ {
+ var sid = s[i]['.name'];
+ var inst = this.instance[sid] = { tabs: [ ] };
- s.on('sortstop', function(ev, ui) {
- ui.item.children('div.cbi-section-head').triggerHandler('focusout');
- });
- }
+ body.append($('<li />')
+ .addClass('luci2-section-item list-group-item')
+ .attr('id', this.id('sectionitem', sid))
+ .attr('data-luci2-sid', sid)
+ .append(this.renderPanelHead(sid, i, parent_sid))
+ .append(this.renderPanelBody(sid, i, parent_sid)));
+ }
- if (this.options.addremove)
- this._render_add().appendTo(a);
+ return body;
+ },
- return a;
- }
+ render: function(condensed, parent_sid)
+ {
+ this.instance = { };
+
+ var panel = $('<div />')
+ .addClass('panel panel-default')
+ .append(this.renderHead(condensed))
+ .append(this.renderBody(condensed, parent_sid));
if (this.options.addremove)
- allsections = allsections.add(this._render_add());
+ panel.append($('<div />')
+ .addClass('panel-footer')
+ .append(this.renderAdd()));
- return allsections;
+ return panel;
},
- finish: function()
+ finish: function(parent_sid)
{
- var s = this.sections();
+ var s = this.getUCISections(parent_sid);
for (var i = 0; i < s.length; i++)
{
var sid = s[i]['.name'];
- this.validate(sid);
+ if (i != this.getPanelIndex(parent_sid))
+ $('#' + this.id('teaser', sid)).children('span:last')
+ .append(this.teaser(sid));
+ else
+ $('#' + this.id('teaser', sid))
+ .hide();
- $('#' + this.id('sort', sid))
- .children('.cbi-section-head')
- .children('.cbi-section-teaser')
- .append(this.teaser(sid));
+ for (var j = 0; j < this.subsections.length; j++)
+ this.subsections[j].finish(sid);
}
}
});
this.cbi.TableSection = this.cbi.TypedSection.extend({
- render: function()
+ renderTableHead: function()
{
- var allsections = $();
- var panel_index = 0;
-
- this.instance = { };
-
- var s = this.sections();
-
- var fieldset = $('<fieldset />')
- .addClass('cbi-section');
-
- fieldset.append(this._render_caption(sid));
-
- if (typeof(this.options.description) == 'string')
- {
- $('<div />')
- .addClass('cbi-section-descr')
- .text(this.options.description)
- .appendTo(fieldset);
- }
-
- var fields = $('<div />')
- .addClass('cbi-section-node')
- .appendTo(fieldset);
-
- var table = $('<table />')
- .addClass('cbi-section-table')
- .appendTo(fields);
-
var thead = $('<thead />')
- .append($('<tr />').addClass('cbi-section-table-titles'))
- .appendTo(table);
+ .append($('<tr />')
+ .addClass('cbi-section-table-titles'));
for (var j = 0; j < this.tabs[0].fields.length; j++)
- $('<th />')
+ thead.children().append($('<th />')
.addClass('cbi-section-table-cell')
.css('width', this.tabs[0].fields[j].options.width || '')
- .append(this.tabs[0].fields[j].options.caption)
- .appendTo(thead.children());
+ .append(this.tabs[0].fields[j].label('caption')));
- if (this.options.sortable)
- $('<th />').addClass('cbi-section-table-cell').text(' ').appendTo(thead.children());
+ if (this.options.addremove !== false || this.options.sortable)
+ thead.children().append($('<th />')
+ .addClass('cbi-section-table-cell')
+ .text(' '));
- if (this.options.addremove !== false)
- $('<th />').addClass('cbi-section-table-cell').text(' ').appendTo(thead.children());
+ return thead;
+ },
- var tbody = $('<tbody />')
- .appendTo(table);
+ renderTableRow: function(sid, index)
+ {
+ var row = $('<tr />')
+ .addClass('luci2-section-item')
+ .attr('id', this.id('sectionitem', sid))
+ .attr('data-luci2-sid', sid);
- if (s.length == 0)
+ for (var j = 0; j < this.tabs[0].fields.length; j++)
{
- $('<tr />')
- .addClass('cbi-section-table-row')
- .append(
- $('<td />')
- .addClass('cbi-section-table-cell')
- .addClass('cbi-section-table-placeholder')
- .attr('colspan', thead.children().children().length)
- .text(this.options.placeholder || _luci2.tr('This section contains no values yet')))
- .appendTo(tbody);
+ row.append($('<td />')
+ .css('width', this.tabs[0].fields[j].options.width || '')
+ .append(this.tabs[0].fields[j].render(sid, true)));
}
- for (var i = 0; i < s.length; i++)
+ if (this.options.addremove !== false || this.options.sortable)
{
- var sid = s[i]['.name'];
- var inst = this.instance[sid] = { tabs: [ ] };
+ row.append($('<td />')
+ .css('width', '1%')
+ .addClass('text-right')
+ .append($('<div />')
+ .addClass('btn-group')
+ .append(this.renderSort(index))
+ .append(this.renderRemove(index))));
+ }
- var row = $('<tr />')
- .addClass('cbi-section-table-row')
- .appendTo(tbody);
+ return row;
+ },
- for (var j = 0; j < this.tabs[0].fields.length; j++)
- {
- $('<td />')
- .addClass('cbi-section-table-cell')
- .css('width', this.tabs[0].fields[j].options.width || '')
- .append(this.tabs[0].fields[j].render(sid, true))
- .appendTo(row);
- }
+ renderTableBody: function(parent_sid)
+ {
+ var s = this.getUCISections(parent_sid);
- if (this.options.sortable)
- {
- $('<td />')
- .addClass('cbi-section-table-cell')
- .addClass('cbi-section-table-sort')
- .append($('<img />').attr('src', _luci2.globals.resource + '/icons/cbi/up.gif').attr('title', _luci2.tr('Drag to sort')))
- .append($('<br />'))
- .append($('<img />').attr('src', _luci2.globals.resource + '/icons/cbi/down.gif').attr('title', _luci2.tr('Drag to sort')))
- .appendTo(row);
- }
+ var tbody = $('<tbody />');
- if (this.options.addremove !== false)
- {
- $('<td />')
- .addClass('cbi-section-table-cell')
- .append(this._render_remove(sid))
- .appendTo(row);
- }
+ if (s.length == 0)
+ {
+ var cols = this.tabs[0].fields.length;
- this.validate(sid);
+ if (this.options.addremove !== false || this.options.sortable)
+ cols++;
- row = null;
+ tbody.append($('<tr />')
+ .append($('<td />')
+ .addClass('text-muted')
+ .attr('colspan', cols)
+ .text(this.label('placeholder') || L.tr('There are no entries defined yet.'))));
}
- if (this.options.sortable)
+ for (var i = 0; i < s.length; i++)
{
- var s = tbody.sortable({
- handle: 'td.cbi-section-table-sort'
- });
-
- s.on('sortupdate', { self: this, ids: s.sortable('toArray') }, function(ev, ui) {
- var sections = [ ];
- for (var i = 0; i < ev.data.ids.length; i++)
- sections.push(ev.data.ids[i].substring(ev.data.ids[i].lastIndexOf('.') + 1));
- _luci2.uci.order(ev.data.self.map.uci_package, sections);
- });
+ var sid = s[i]['.name'];
+ var inst = this.instance[sid] = { tabs: [ ] };
- s.on('sortstop', function(ev, ui) {
- ui.item.children('div.cbi-section-head').triggerHandler('focusout');
- });
+ tbody.append(this.renderTableRow(sid, i));
}
- if (this.options.addremove)
- this._render_add().appendTo(fieldset);
-
- fields = table = thead = tbody = null;
+ return tbody;
+ },
- return fieldset;
+ renderBody: function(condensed, parent_sid)
+ {
+ return $('<table />')
+ .addClass('table table-condensed table-hover')
+ .append(this.renderTableHead())
+ .append(this.renderTableBody(parent_sid));
}
});
this.cbi.NamedSection = this.cbi.TypedSection.extend({
- sections: function(cb)
+ getUCISections: function(cb)
{
var sa = [ ];
- var pkg = this.map.uci.values[this.map.uci_package];
+ var sl = L.uci.sections(this.ownerMap.uci_package);
- for (var s in pkg)
- if (pkg[s]['.name'] == this.uci_type)
+ for (var i = 0; i < sl.length; i++)
+ if (sl[i]['.name'] == this.uci_type)
{
- sa.push(pkg[s]);
+ sa.push(sl[i]);
break;
}
if (typeof(cb) == 'function' && sa.length > 0)
- cb.apply(this, [ sa[0] ]);
+ cb.call(this, sa[0]);
return sa;
}
});
+ this.cbi.SingleSection = this.cbi.NamedSection.extend({
+ render: function()
+ {
+ this.instance = { };
+ this.instance[this.uci_type] = { tabs: [ ] };
+
+ return $('<div />')
+ .addClass('luci2-section-item')
+ .attr('id', this.id('sectionitem', this.uci_type))
+ .attr('data-luci2-sid', this.uci_type)
+ .append(this.renderPanelBody(this.uci_type, 0));
+ }
+ });
+
this.cbi.DummySection = this.cbi.TypedSection.extend({
- sections: function(cb)
+ getUCISections: function(cb)
{
if (typeof(cb) == 'function')
cb.apply(this, [ { '.name': this.uci_type } ]);
}
});
- this.cbi.Map = AbstractWidget.extend({
+ this.cbi.Map = this.ui.AbstractWidget.extend({
init: function(uci_package, options)
{
var self = this;
this.uci_package = uci_package;
this.sections = [ ];
- this.options = _luci2.defaults(options, {
+ this.options = L.defaults(options, {
save: function() { },
- prepare: function() {
- return _luci2.uci.writable(function(writable) {
- self.options.readonly = !writable;
- });
- }
+ prepare: function() { }
});
},
+ loadCallback: function()
+ {
+ var deferreds = [ L.deferrable(this.options.prepare()) ];
+
+ for (var i = 0; i < this.sections.length; i++)
+ {
+ var rv = this.sections[i].load();
+ deferreds.push.apply(deferreds, rv);
+ }
+
+ return $.when.apply($, deferreds);
+ },
+
load: function()
{
- this.uci = {
- newid: 0,
- values: { },
- creates: { },
- changes: { },
- deletes: { }
- };
+ var self = this;
+ var packages = [ this.uci_package ];
- this.active_panel = 0;
+ for (var i = 0; i < this.sections.length; i++)
+ packages.push.apply(packages, this.sections[i].findAdditionalUCIPackages());
+
+ for (var i = 0; i < packages.length; i++)
+ if (!L.uci.writable(packages[i]))
+ {
+ this.options.readonly = true;
+ break;
+ }
+
+ return L.uci.load(packages).then(function() {
+ return self.loadCallback();
+ });
+ },
- var packages = { };
+ handleTab: function(ev)
+ {
+ ev.data.self.active_tab = $(ev.target).parent().index();
+ },
- for (var i = 0; i < this.sections.length; i++)
- this.sections[i].ucipackages(packages);
+ handleApply: function(ev)
+ {
+ var self = ev.data.self;
- packages[this.uci_package] = true;
+ self.trigger('apply', ev);
+ },
- var load_cb = this._load_cb || (this._load_cb = $.proxy(function(packages) {
- for (var i = 0; i < packages.length; i++)
- {
- this.uci.values[packages[i]['.package']] = packages[i];
- delete packages[i]['.package'];
- }
+ handleSave: function(ev)
+ {
+ var self = ev.data.self;
- var deferreds = [ _luci2.deferrable(this.options.prepare()) ];
+ self.send().then(function() {
+ self.trigger('save', ev);
+ });
+ },
- for (var i = 0; i < this.sections.length; i++)
- {
- for (var f in this.sections[i].fields)
- {
- if (typeof(this.sections[i].fields[f].load) != 'function')
- continue;
+ handleReset: function(ev)
+ {
+ var self = ev.data.self;
- var s = this.sections[i].sections();
- for (var j = 0; j < s.length; j++)
- {
- var rv = this.sections[i].fields[f].load(s[j]['.name']);
- if (_luci2.isDeferred(rv))
- deferreds.push(rv);
- }
- }
- }
+ self.trigger('reset', ev);
+ self.reset();
+ },
- return $.when.apply($, deferreds);
- }, this));
+ renderTabHead: function(tab_index)
+ {
+ var section = this.sections[tab_index];
+ var cur = this.active_tab || 0;
- _luci2.rpc.batch();
+ var tabh = $('<li />')
+ .append($('<a />')
+ .attr('id', section.id('sectiontab'))
+ .attr('href', '#' + section.id('section'))
+ .attr('data-toggle', 'tab')
+ .text(section.label('caption') + ' ')
+ .append($('<span />')
+ .addClass('badge'))
+ .on('shown.bs.tab', { self: this }, this.handleTab));
- for (var pkg in packages)
- _luci2.uci.get_all(pkg);
+ if (cur == tab_index)
+ tabh.addClass('active');
- return _luci2.rpc.flush().then(load_cb);
+ return tabh;
},
- render: function()
+ renderTabBody: function(tab_index)
{
- var map = $('<div />').addClass('cbi-map');
+ var section = this.sections[tab_index];
+ var desc = section.label('description');
+ var cur = this.active_tab || 0;
- if (typeof(this.options.caption) == 'string')
- $('<h2 />').text(this.options.caption).appendTo(map);
+ var tabb = $('<div />')
+ .addClass('tab-pane')
+ .attr('id', section.id('section'));
- if (typeof(this.options.description) == 'string')
- $('<div />').addClass('cbi-map-descr').text(this.options.description).appendTo(map);
+ if (cur == tab_index)
+ tabb.addClass('active');
- var sections = $('<div />').appendTo(map);
+ if (desc)
+ tabb.append($('<p />')
+ .text(desc));
- for (var i = 0; i < this.sections.length; i++)
- {
- var s = this.sections[i].render();
+ var s = section.render(this.options.tabbed);
- if (this.options.readonly || this.sections[i].options.readonly)
- s.find('input, select, button, img.cbi-button').attr('disabled', true);
+ if (this.options.readonly || section.options.readonly)
+ s.find('input, select, button, img.cbi-button').attr('disabled', true);
- s.appendTo(sections);
+ tabb.append(s);
- if (this.sections[i].options.active)
- this.active_panel = i;
- }
+ return tabb;
+ },
- if (this.options.collabsible)
- {
- var a = sections.accordion({
- header: '> fieldset > div.cbi-section-head',
- heightStyle: 'content',
- active: this.active_panel
- });
+ renderBody: function()
+ {
+ var tabs = $('<ul />')
+ .addClass('nav nav-tabs');
- a.on('accordionbeforeactivate', { self: this }, function(ev, ui) {
- var h = ui.oldHeader;
- var s = ev.data.self.sections[parseInt(h.attr('cbi-section-num'))];
- var i = h.attr('cbi-section-id');
+ var body = $('<div />')
+ .append(tabs);
- h.children('.cbi-section-teaser').empty().append(s.teaser(i));
+ for (var i = 0; i < this.sections.length; i++)
+ {
+ tabs.append(this.renderTabHead(i));
+ body.append(this.renderTabBody(i));
+ }
- for (var i = 0; i < ev.data.self.sections.length; i++)
- ev.data.self.sections[i].validate();
- });
+ if (this.options.tabbed)
+ body.addClass('tab-content');
+ else
+ tabs.hide();
- a.on('accordionactivate', { self: this }, function(ev, ui) {
- ev.data.self.active_panel = parseInt(ui.newPanel.attr('index'));
- });
- }
+ return body;
+ },
- if (this.options.pageaction !== false)
- {
- var a = $('<div />')
- .addClass('cbi-page-actions')
- .appendTo(map);
+ renderFooter: function()
+ {
+ var evdata = {
+ self: this
+ };
- $('<input />')
- .addClass('cbi-button').addClass('cbi-button-apply')
- .attr('type', 'button')
- .val(_luci2.tr('Save & Apply'))
- .appendTo(a);
+ return $('<div />')
+ .addClass('panel panel-default panel-body text-right')
+ .append($('<div />')
+ .addClass('btn-group')
+ .append(L.ui.button(L.tr('Save & Apply'), 'primary')
+ .click(evdata, this.handleApply))
+ .append(L.ui.button(L.tr('Save'), 'default')
+ .click(evdata, this.handleSave))
+ .append(L.ui.button(L.tr('Reset'), 'default')
+ .click(evdata, this.handleReset)));
+ },
- $('<input />')
- .addClass('cbi-button').addClass('cbi-button-save')
- .attr('type', 'button')
- .val(_luci2.tr('Save'))
- .click({ self: this }, function(ev) { ev.data.self.send(); })
- .appendTo(a);
+ render: function()
+ {
+ var map = $('<form />');
- $('<input />')
- .addClass('cbi-button').addClass('cbi-button-reset')
- .attr('type', 'button')
- .val(_luci2.tr('Reset'))
- .click({ self: this }, function(ev) { ev.data.self.insertInto(ev.data.self.target); })
- .appendTo(a);
+ if (typeof(this.options.caption) == 'string')
+ map.append($('<h2 />')
+ .text(this.options.caption));
- a = null;
- }
+ if (typeof(this.options.description) == 'string')
+ map.append($('<p />')
+ .text(this.options.description));
- var top = $('<form />').append(map);
+ map.append(this.renderBody());
- map = null;
+ if (this.options.pageaction !== false)
+ map.append(this.renderFooter());
- return top;
+ return map;
},
finish: function()
{
var w = widget ? new widget(uci_type, options) : null;
- if (!(w instanceof AbstractSection))
+ if (!(w instanceof L.cbi.AbstractSection))
throw 'Widget must be an instance of AbstractSection';
- w.map = this;
+ w.ownerMap = this;
w.index = this.sections.length;
this.sections.push(w);
return w;
},
- formvalue: function()
- {
- var rv = { };
-
- for (var i = 0; i < this.sections.length; i++)
- {
- var sids = this.sections[i].formvalue();
- for (var sid in sids)
- {
- var s = rv[sid] || (rv[sid] = { });
- $.extend(s, sids[sid]);
- }
- }
-
- return rv;
- },
-
add: function(conf, type, name)
{
- var c = this.uci.creates;
- var s = '.new.%d'.format(this.uci.newid++);
-
- if (!c[conf])
- c[conf] = { };
-
- c[conf][s] = {
- '.type': type,
- '.name': s,
- '.create': name,
- '.anonymous': !name
- };
-
- return s;
+ return L.uci.add(conf, type, name);
},
remove: function(conf, sid)
{
- var n = this.uci.creates;
- var c = this.uci.changes;
- var d = this.uci.deletes;
-
- /* requested deletion of a just created section */
- if (sid.indexOf('.new.') == 0)
- {
- if (n[conf])
- delete n[conf][sid];
- }
- else
- {
- if (c[conf])
- delete c[conf][sid];
-
- if (!d[conf])
- d[conf] = { };
-
- d[conf][sid] = true;
- }
- },
-
- ucisections: function(conf, cb)
- {
- var sa = [ ];
- var pkg = this.uci.values[conf];
- var crt = this.uci.creates[conf];
- var del = this.uci.deletes[conf];
-
- if (!pkg)
- return sa;
-
- for (var s in pkg)
- if (!del || del[s] !== true)
- sa.push(pkg[s]);
-
- sa.sort(function(a, b) { return a['.index'] - b['.index'] });
-
- if (crt)
- for (var s in crt)
- sa.push(crt[s]);
-
- if (typeof(cb) == 'function')
- for (var i = 0; i < sa.length; i++)
- cb.apply(this, [ sa[i] ]);
-
- return sa;
+ return L.uci.remove(conf, sid);
},
get: function(conf, sid, opt)
{
- var v = this.uci.values;
- var n = this.uci.creates;
- var c = this.uci.changes;
- var d = this.uci.deletes;
-
- /* requested option in a just created section */
- if (sid.indexOf('.new.') == 0)
- {
- if (!n[conf])
- return undefined;
-
- if (typeof(opt) == 'undefined')
- return (n[conf][sid] || { });
-
- return n[conf][sid][opt];
- }
-
- /* requested an option value */
- if (typeof(opt) != 'undefined')
- {
- /* check whether option was deleted */
- if (d[conf] && d[conf][sid])
- {
- if (d[conf][sid] === true)
- return undefined;
-
- for (var i = 0; i < d[conf][sid].length; i++)
- if (d[conf][sid][i] == opt)
- return undefined;
- }
-
- /* check whether option was changed */
- if (c[conf] && c[conf][sid] && typeof(c[conf][sid][opt]) != 'undefined')
- return c[conf][sid][opt];
-
- /* return base value */
- if (v[conf] && v[conf][sid])
- return v[conf][sid][opt];
-
- return undefined;
- }
-
- /* requested an entire section */
- if (v[conf])
- return (v[conf][sid] || { });
-
- return undefined;
+ return L.uci.get(conf, sid, opt);
},
set: function(conf, sid, opt, val)
{
- var n = this.uci.creates;
- var c = this.uci.changes;
- var d = this.uci.deletes;
-
- if (sid.indexOf('.new.') == 0)
- {
- if (n[conf] && n[conf][sid])
- {
- if (typeof(val) != 'undefined')
- n[conf][sid][opt] = val;
- else
- delete n[conf][sid][opt];
- }
- }
- else if (typeof(val) != 'undefined')
- {
- if (!c[conf])
- c[conf] = { };
-
- if (!c[conf][sid])
- c[conf][sid] = { };
-
- c[conf][sid][opt] = val;
- }
- else
- {
- if (!d[conf])
- d[conf] = { };
-
- if (!d[conf][sid])
- d[conf][sid] = [ ];
-
- d[conf][sid].push(opt);
- }
+ return L.uci.set(conf, sid, opt, val);
},
validate: function()
var rv = true;
for (var i = 0; i < this.sections.length; i++)
+ {
if (!this.sections[i].validate())
rv = false;
+ }
return rv;
},
save: function()
{
- if (this.options.readonly)
- return _luci2.deferrable();
-
- var deferreds = [ _luci2.deferrable(this.options.save()) ];
+ var self = this;
- for (var i = 0; i < this.sections.length; i++)
- {
- if (this.sections[i].options.readonly)
- continue;
+ if (self.options.readonly)
+ return L.deferrable();
- for (var f in this.sections[i].fields)
- {
- if (typeof(this.sections[i].fields[f].save) != 'function')
- continue;
+ var deferreds = [ ];
- var s = this.sections[i].sections();
- for (var j = 0; j < s.length; j++)
- {
- var rv = this.sections[i].fields[f].save(s[j]['.name']);
- if (_luci2.isDeferred(rv))
- deferreds.push(rv);
- }
- }
+ for (var i = 0; i < self.sections.length; i++)
+ {
+ var rv = self.sections[i].save();
+ deferreds.push.apply(deferreds, rv);
}
- return $.when.apply($, deferreds);
+ return $.when.apply($, deferreds).then(function() {
+ return L.deferrable(self.options.save());
+ });
},
send: function()
{
if (!this.validate())
- return _luci2.deferrable();
-
- var send_cb = this._send_cb || (this._send_cb = $.proxy(function() {
- _luci2.rpc.batch();
-
- if (this.uci.creates)
- for (var c in this.uci.creates)
- for (var s in this.uci.creates[c])
- {
- var r = {
- config: c,
- values: { }
- };
-
- for (var k in this.uci.creates[c][s])
- {
- if (k == '.type')
- r.type = this.uci.creates[c][s][k];
- else if (k == '.create')
- r.name = this.uci.creates[c][s][k];
- else if (k.charAt(0) != '.')
- r.values[k] = this.uci.creates[c][s][k];
- }
-
- _luci2.uci.add(r.config, r.type, r.name, r.values);
- }
-
- if (this.uci.changes)
- for (var c in this.uci.changes)
- for (var s in this.uci.changes[c])
- _luci2.uci.set(c, s, this.uci.changes[c][s]);
-
- if (this.uci.deletes)
- for (var c in this.uci.deletes)
- for (var s in this.uci.deletes[c])
- {
- var o = this.uci.deletes[c][s];
- _luci2.uci['delete'](c, s, (o === true) ? undefined : o);
- }
-
- return _luci2.rpc.flush();
- }, this));
+ return L.deferrable();
var self = this;
- _luci2.ui.loading(true);
+ L.ui.saveScrollTop();
+ L.ui.loading(true);
- return this.save().then(send_cb).then(function() {
+ return this.save().then(function() {
+ return L.uci.save();
+ }).then(function() {
+ return L.ui.updateChanges();
+ }).then(function() {
return self.load();
}).then(function() {
self.redraw();
self = null;
- _luci2.ui.loading(false);
+ L.ui.loading(false);
+ L.ui.restoreScrollTop();
});
},
- dialog: function(id)
+ revert: function()
{
- var d = $('<div />');
- var p = $('<p />');
+ var packages = [ this.uci_package ];
+
+ for (var i = 0; i < this.sections.length; i++)
+ packages.push.apply(packages, this.sections[i].findAdditionalUCIPackages());
- $('<img />')
- .attr('src', _luci2.globals.resource + '/icons/loading.gif')
- .css('vertical-align', 'middle')
- .css('padding-right', '10px')
- .appendTo(p);
+ L.uci.unload(packages);
+ },
- p.append(_luci2.tr('Loading data...'));
+ reset: function()
+ {
+ var self = this;
- p.appendTo(d);
- d.appendTo(id);
+ self.revert();
- return d.dialog({
- modal: true,
- draggable: false,
- resizable: false,
- height: 90,
- open: function() {
- $(this).parent().children('.ui-dialog-titlebar').hide();
- }
- });
+ return self.insertInto(self.target);
},
insertInto: function(id)
var self = this;
self.target = $(id);
- _luci2.ui.loading(true);
+ L.ui.loading(true);
self.target.hide();
return self.load().then(function() {
self.finish();
self.target.show();
self = null;
- _luci2.ui.loading(false);
+ L.ui.loading(false);
+ });
+ }
+ });
+
+ this.cbi.Modal = this.cbi.Map.extend({
+ handleApply: function(ev)
+ {
+ var self = ev.data.self;
+
+ self.trigger('apply', ev);
+ },
+
+ handleSave: function(ev)
+ {
+ var self = ev.data.self;
+
+ self.send().then(function() {
+ self.trigger('save', ev);
+ self.close();
+ });
+ },
+
+ handleReset: function(ev)
+ {
+ var self = ev.data.self;
+
+ self.trigger('close', ev);
+ self.revert();
+ self.close();
+ },
+
+ renderFooter: function()
+ {
+ var evdata = {
+ self: this
+ };
+
+ return $('<div />')
+ .addClass('btn-group')
+ .append(L.ui.button(L.tr('Save & Apply'), 'primary')
+ .click(evdata, this.handleApply))
+ .append(L.ui.button(L.tr('Save'), 'default')
+ .click(evdata, this.handleSave))
+ .append(L.ui.button(L.tr('Cancel'), 'default')
+ .click(evdata, this.handleReset));
+ },
+
+ render: function()
+ {
+ var modal = L.ui.dialog(this.label('caption'), null, { wide: true });
+ var map = $('<form />');
+
+ var desc = this.label('description');
+ if (desc)
+ map.append($('<p />').text(desc));
+
+ map.append(this.renderBody());
+
+ modal.find('.modal-body').append(map);
+ modal.find('.modal-footer').append(this.renderFooter());
+
+ return modal;
+ },
+
+ redraw: function()
+ {
+ this.render();
+ this.finish();
+ },
+
+ show: function()
+ {
+ var self = this;
+
+ L.ui.loading(true);
+
+ return self.load().then(function() {
+ self.render();
+ self.finish();
+
+ L.ui.loading(false);
});
+ },
+
+ close: function()
+ {
+ L.ui.dialog(false);
}
});
};