summaryrefslogtreecommitdiffhomepage
path: root/modules/luci-base/htdocs/luci-static/resources
diff options
context:
space:
mode:
Diffstat (limited to 'modules/luci-base/htdocs/luci-static/resources')
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/form.js5
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/fs.js295
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/luci.js21
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/tools/widgets.js51
-rw-r--r--modules/luci-base/htdocs/luci-static/resources/ui.js79
5 files changed, 418 insertions, 33 deletions
diff --git a/modules/luci-base/htdocs/luci-static/resources/form.js b/modules/luci-base/htdocs/luci-static/resources/form.js
index 46f1ff4825..f4cce17fe1 100644
--- a/modules/luci-base/htdocs/luci-static/resources/form.js
+++ b/modules/luci-base/htdocs/luci-static/resources/form.js
@@ -834,7 +834,7 @@ var CBIAbstractValue = CBINode.extend({
return Promise.resolve(this.write(section_id, fval));
}
else {
- if (this.rmempty || this.optional) {
+ if (!active || this.rmempty || this.optional) {
return Promise.resolve(this.remove(section_id));
}
else if (!isEqual(cval, fval)) {
@@ -1031,6 +1031,9 @@ var CBITableSection = CBITypedSection.extend({
for (var i = 0; i < nodes.length; i++) {
var sectionname = this.titleFn('sectiontitle', cfgsections[i]);
+ if (sectionname == null)
+ sectionname = cfgsections[i];
+
var trEl = E('div', {
'id': 'cbi-%s-%s'.format(config_name, cfgsections[i]),
'class': 'tr cbi-section-table-row',
diff --git a/modules/luci-base/htdocs/luci-static/resources/fs.js b/modules/luci-base/htdocs/luci-static/resources/fs.js
new file mode 100644
index 0000000000..6eb0390909
--- /dev/null
+++ b/modules/luci-base/htdocs/luci-static/resources/fs.js
@@ -0,0 +1,295 @@
+'use strict';
+'require rpc';
+
+/**
+ * @typedef {Object} FileStatEntry
+ * @memberof LuCI.fs
+
+ * @property {string} name - Name of the directory entry
+ * @property {string} type - Type of the entry, one of `block`, `char`, `directory`, `fifo`, `symlink`, `file`, `socket` or `unknown`
+ * @property {number} size - Size in bytes
+ * @property {number} mode - Access permissions
+ * @property {number} atime - Last access time in seconds since epoch
+ * @property {number} mtime - Last modification time in seconds since epoch
+ * @property {number} ctime - Last change time in seconds since epoch
+ * @property {number} inode - Inode number
+ * @property {number} uid - Numeric owner id
+ * @property {number} gid - Numeric group id
+ */
+
+/**
+ * @typedef {Object} FileExecResult
+ * @memberof LuCI.fs
+ *
+ * @property {number} code - The exit code of the invoked command
+ * @property {string} [stdout] - The stdout produced by the command, if any
+ * @property {string} [stderr] - The stderr produced by the command, if any
+ */
+
+var callFileList, callFileStat, callFileRead, callFileWrite, callFileRemove,
+ callFileExec, callFileMD5;
+
+callFileList = rpc.declare({
+ object: 'file',
+ method: 'list',
+ params: [ 'path' ]
+});
+
+callFileStat = rpc.declare({
+ object: 'file',
+ method: 'stat',
+ params: [ 'path' ]
+});
+
+callFileRead = rpc.declare({
+ object: 'file',
+ method: 'read',
+ params: [ 'path' ]
+});
+
+callFileWrite = rpc.declare({
+ object: 'file',
+ method: 'write',
+ params: [ 'path', 'data' ]
+});
+
+callFileRemove = rpc.declare({
+ object: 'file',
+ method: 'remove',
+ params: [ 'path' ]
+});
+
+callFileExec = rpc.declare({
+ object: 'file',
+ method: 'exec',
+ params: [ 'command', 'params', 'env' ]
+});
+
+callFileMD5 = rpc.declare({
+ object: 'file',
+ method: 'md5',
+ params: [ 'path' ]
+});
+
+var rpcErrors = [
+ null,
+ 'InvalidCommandError',
+ 'InvalidArgumentError',
+ 'MethodNotFoundError',
+ 'NotFoundError',
+ 'NoDataError',
+ 'PermissionError',
+ 'TimeoutError',
+ 'UnsupportedError'
+];
+
+function handleRpcReply(expect, rc) {
+ if (typeof(rc) == 'number' && rc != 0) {
+ var e = new Error(rpc.getStatusText(rc)); e.name = rpcErrors[rc] || 'Error';
+ throw e;
+ }
+
+ if (expect) {
+ var type = Object.prototype.toString;
+
+ for (var key in expect) {
+ if (rc != null && key != '')
+ rc = rc[key];
+
+ if (rc == null || type.call(rc) != type.call(expect[key])) {
+ var e = new Error(_('Unexpected reply data format')); e.name = 'TypeError';
+ throw e;
+ }
+
+ break;
+ }
+ }
+
+ return rc;
+}
+
+/**
+ * @class fs
+ * @memberof LuCI
+ * @hideconstructor
+ * @classdesc
+ *
+ * Provides high level utilities to wrap file system related RPC calls.
+ * To import the class in views, use `'require fs'`, to import it in
+ * external JavaScript, use `L.require("fs").then(...)`.
+ */
+var FileSystem = L.Class.extend(/** @lends LuCI.fs.prototype */ {
+ /**
+ * Obtains a listing of the specified directory.
+ *
+ * @param {string} path
+ * The directory path to list.
+ *
+ * @returns {Promise<LuCI.fs.FileStatEntry[]>}
+ * Returns a promise resolving to an array of stat detail objects or
+ * rejecting with an error stating the failure reason.
+ */
+ list: function(path) {
+ return callFileList(path).then(handleRpcReply.bind(this, { entries: [] }));
+ },
+
+ /**
+ * Return file stat information on the specified path.
+ *
+ * @param {string} path
+ * The filesystem path to stat.
+ *
+ * @returns {Promise<LuCI.fs.FileStatEntry>}
+ * Returns a promise resolving to a stat detail object or
+ * rejecting with an error stating the failure reason.
+ */
+ stat: function(path) {
+ return callFileStat(path).then(handleRpcReply.bind(this, { '': {} }));
+ },
+
+ /**
+ * Read the contents of the given file and return them.
+ * Note: this function is unsuitable for obtaining binary data.
+ *
+ * @param {string} path
+ * The file path to read.
+ *
+ * @returns {Promise<string>}
+ * Returns a promise resolving to a string containing the file contents or
+ * rejecting with an error stating the failure reason.
+ */
+ read: function(path) {
+ return callFileRead(path).then(handleRpcReply.bind(this, { data: '' }));
+ },
+
+ /**
+ * Write the given data to the specified file path.
+ * If the specified file path does not exist, it will be created, given
+ * sufficient permissions.
+ *
+ * Note: `data` will be converted to a string using `String(data)` or to
+ * `''` when it is `null`.
+ *
+ * @param {string} path
+ * The file path to write to.
+ *
+ * @param {*} [data]
+ * The file data to write. If it is null, it will be set to an empty
+ * string.
+ *
+ * @returns {Promise<number>}
+ * Returns a promise resolving to `0` or rejecting with an error stating
+ * the failure reason.
+ */
+ write: function(path, data) {
+ data = (data != null) ? String(data) : '';
+ return callFileWrite(path, data).then(handleRpcReply.bind(this, { '': 0 }));
+ },
+
+ /**
+ * Unlink the given file.
+ *
+ * @param {string}
+ * The file path to remove.
+ *
+ * @returns {Promise<number>}
+ * Returns a promise resolving to `0` or rejecting with an error stating
+ * the failure reason.
+ */
+ remove: function(path) {
+ return callFileRemove(path).then(handleRpcReply.bind(this, { '': 0 }));
+ },
+
+ /**
+ * Execute the specified command, optionally passing params and
+ * environment variables.
+ *
+ * Note: The `command` must be either the path to an executable,
+ * or a basename without arguments in which case it will be searched
+ * in $PATH. If specified, the values given in `params` will be passed
+ * as arguments to the command.
+ *
+ * The key/value pairs in the optional `env` table are translated to
+ * `setenv()` calls prior to running the command.
+ *
+ * @param {string} command
+ * The command to invoke.
+ *
+ * @param {string[]} [params]
+ * The arguments to pass to the command.
+ *
+ * @param {Object.<string, string>} [env]
+ * Environment variables to set.
+ *
+ * @returns {Promise<LuCI.fs.FileExecResult>}
+ * Returns a promise resolving to an object describing the execution
+ * results or rejecting with an error stating the failure reason.
+ */
+ exec: function(command, params, env) {
+ if (!Array.isArray(params))
+ params = null;
+
+ if (!L.isObject(env))
+ env = null;
+
+ return callFileExec(command, params, env).then(handleRpcReply.bind(this, { '': {} }));
+ },
+
+ /**
+ * Read the contents of the given file, trim leading and trailing white
+ * space and return the trimmed result. In case of errors, return an empty
+ * string instead.
+ *
+ * Note: this function is useful to read single-value files in `/sys`
+ * or `/proc`.
+ *
+ * This function is guaranteed to not reject its promises, on failure,
+ * an empty string will be returned.
+ *
+ * @param {string} path
+ * The file path to read.
+ *
+ * @returns {Promise<string>}
+ * Returns a promise resolving to the file contents or the empty string
+ * on failure.
+ */
+ trimmed: function(path) {
+ return L.resolveDefault(this.read(path), '').then(function(s) {
+ return s.trim();
+ });
+ },
+
+ /**
+ * Read the contents of the given file, split it into lines, trim
+ * leading and trailing white space of each line and return the
+ * resulting array.
+ *
+ * This function is guaranteed to not reject its promises, on failure,
+ * an empty array will be returned.
+ *
+ * @param {string} path
+ * The file path to read.
+ *
+ * @returns {Promise<string[]>}
+ * Returns a promise resolving to an array containing the stripped lines
+ * of the given file or `[]` on failure.
+ */
+ lines: function(path) {
+ return L.resolveDefault(this.read(path), '').then(function(s) {
+ var lines = [];
+
+ s = s.trim();
+
+ if (s != '') {
+ var l = s.split(/\n/);
+
+ for (var i = 0; i < l.length; i++)
+ lines.push(l[i].trim());
+ }
+
+ return lines;
+ });
+ }
+});
+
+return FileSystem;
diff --git a/modules/luci-base/htdocs/luci-static/resources/luci.js b/modules/luci-base/htdocs/luci-static/resources/luci.js
index cad7208532..3f4707d4e5 100644
--- a/modules/luci-base/htdocs/luci-static/resources/luci.js
+++ b/modules/luci-base/htdocs/luci-static/resources/luci.js
@@ -1852,6 +1852,27 @@
return s.split(/\s+/);
},
+ /**
+ * Returns a promise resolving with either the given value or or with
+ * the given default in case the input value is a rejecting promise.
+ *
+ * @instance
+ * @memberof LuCI
+ *
+ * @param {*} value
+ * The value to resolve the promise with.
+ *
+ * @param {*} defvalue
+ * The default value to resolve the promise with in case the given
+ * input value is a rejecting promise.
+ *
+ * @returns {Promise<*>}
+ * Returns a new promise resolving either to the given input value or
+ * to the given default value on error.
+ */
+ resolveDefault: function(value, defvalue) {
+ return Promise.resolve(value).catch(function() { return defvalue });
+ },
/**
* The request callback function is invoked whenever an HTTP
diff --git a/modules/luci-base/htdocs/luci-static/resources/tools/widgets.js b/modules/luci-base/htdocs/luci-static/resources/tools/widgets.js
index 1667fa6707..9cc3e26ed2 100644
--- a/modules/luci-base/htdocs/luci-static/resources/tools/widgets.js
+++ b/modules/luci-base/htdocs/luci-static/resources/tools/widgets.js
@@ -3,6 +3,19 @@
'require form';
'require network';
'require firewall';
+'require fs';
+
+function getUsers() {
+ return fs.lines('/etc/passwd').then(function(lines) {
+ return lines.map(function(line) { return line.split(/:/)[0] });
+ });
+}
+
+function getGroups() {
+ return fs.lines('/etc/group').then(function(lines) {
+ return lines.map(function(line) { return line.split(/:/)[0] });
+ });
+}
var CBIZoneSelect = form.ListValue.extend({
__name__: 'CBI.ZoneSelect',
@@ -559,10 +572,48 @@ var CBIDeviceSelect = form.ListValue.extend({
},
});
+var CBIUserSelect = form.ListValue.extend({
+ __name__: 'CBI.UserSelect',
+
+ load: function(section_id) {
+ return getUsers().then(L.bind(function(users) {
+ for (var i = 0; i < users.length; i++) {
+ this.value(users[i]);
+ }
+
+ return this.super('load', section_id);
+ }, this));
+ },
+
+ filter: function(section_id, value) {
+ return true;
+ },
+});
+
+var CBIGroupSelect = form.ListValue.extend({
+ __name__: 'CBI.GroupSelect',
+
+ load: function(section_id) {
+ return getGroups().then(L.bind(function(groups) {
+ for (var i = 0; i < groups.length; i++) {
+ this.value(groups[i]);
+ }
+
+ return this.super('load', section_id);
+ }, this));
+ },
+
+ filter: function(section_id, value) {
+ return true;
+ },
+});
+
return L.Class.extend({
ZoneSelect: CBIZoneSelect,
ZoneForwards: CBIZoneForwards,
NetworkSelect: CBINetworkSelect,
DeviceSelect: CBIDeviceSelect,
+ UserSelect: CBIUserSelect,
+ GroupSelect: CBIGroupSelect,
});
diff --git a/modules/luci-base/htdocs/luci-static/resources/ui.js b/modules/luci-base/htdocs/luci-static/resources/ui.js
index 99e1548a43..caae812812 100644
--- a/modules/luci-base/htdocs/luci-static/resources/ui.js
+++ b/modules/luci-base/htdocs/luci-static/resources/ui.js
@@ -2,6 +2,7 @@
'require rpc';
'require uci';
'require validation';
+'require fs';
var modalDiv = null,
tooltipDiv = null,
@@ -1470,26 +1471,6 @@ var UIFileUpload = UIElement.extend({
}, options);
},
- callFileStat: rpc.declare({
- 'object': 'file',
- 'method': 'stat',
- 'params': [ 'path' ],
- 'expect': { '': {} }
- }),
-
- callFileList: rpc.declare({
- 'object': 'file',
- 'method': 'list',
- 'params': [ 'path' ],
- 'expect': { 'entries': [] }
- }),
-
- callFileRemove: rpc.declare({
- 'object': 'file',
- 'method': 'remove',
- 'params': [ 'path' ]
- }),
-
bind: function(browserEl) {
this.node = browserEl;
@@ -1502,7 +1483,7 @@ var UIFileUpload = UIElement.extend({
},
render: function() {
- return Promise.resolve(this.value != null ? this.callFileStat(this.value) : null).then(L.bind(function(stat) {
+ return L.resolveDefault(this.value != null ? fs.stat(this.value) : null).then(L.bind(function(stat) {
var label;
if (L.isObject(stat) && stat.type != 'directory')
@@ -1647,15 +1628,11 @@ var UIFileUpload = UIElement.extend({
hidden.value = '';
}
- return this.callFileRemove(path).then(L.bind(function(parent, ev, rc) {
- if (rc == 0)
- return this.handleSelect(parent, null, ev);
- else if (rc == 6)
- alert(_('Delete permission denied'));
- else
- alert(_('Delete request failed: %d %s').format(rc, rpc.getStatusText(rc)));
-
- }, this, parent, ev));
+ return fs.remove(path).then(L.bind(function(parent, ev) {
+ return this.handleSelect(parent, null, ev);
+ }, this, parent, ev)).catch(function(err) {
+ alert(_('Delete request failed: %s').format(err.message));
+ });
}
},
@@ -1817,7 +1794,7 @@ var UIFileUpload = UIElement.extend({
if (fileStat == null) {
L.dom.content(ul, E('em', { 'class': 'spinning' }, _('Loading directory contents…')));
- this.callFileList(path).then(L.bind(this.renderListing, this, browser, path));
+ L.resolveDefault(fs.list(path), []).then(L.bind(this.renderListing, this, browser, path));
}
else {
var button = this.node.firstElementChild,
@@ -1849,7 +1826,7 @@ var UIFileUpload = UIElement.extend({
ev.preventDefault();
- return this.callFileList(path).then(L.bind(function(button, browser, path, list) {
+ return L.resolveDefault(fs.list(path), []).then(L.bind(function(button, browser, path, list) {
document.querySelectorAll('.cbi-filebrowser.open').forEach(function(browserEl) {
L.dom.findClassInstance(browserEl).handleCancel(ev);
});
@@ -2237,6 +2214,44 @@ return L.Class.extend({
}
}),
+ /* Reconnect handling */
+ pingDevice: function(proto, ipaddr) {
+ var target = '%s://%s%s?%s'.format(proto || 'http', ipaddr || window.location.host, L.resource('icons/loading.gif'), Math.random());
+
+ return new Promise(function(resolveFn, rejectFn) {
+ var img = new Image();
+
+ img.onload = resolveFn;
+ img.onerror = rejectFn;
+
+ window.setTimeout(rejectFn, 1000);
+
+ img.src = target;
+ });
+ },
+
+ awaitReconnect: function(/* ... */) {
+ var ipaddrs = arguments.length ? arguments : [ window.location.host ];
+
+ window.setTimeout(L.bind(function() {
+ L.Poll.add(L.bind(function() {
+ var tasks = [], reachable = false;
+
+ for (var i = 0; i < 2; i++)
+ for (var j = 0; j < ipaddrs.length; j++)
+ tasks.push(this.pingDevice(i ? 'https' : 'http', ipaddrs[j])
+ .then(function(ev) { reachable = ev.target.src.replace(/^(https?:\/\/[^\/]+).*$/, '$1/') }, function() {}));
+
+ return Promise.all(tasks).then(function() {
+ if (reachable) {
+ L.Poll.stop();
+ window.location = reachable;
+ }
+ });
+ }, this));
+ }, this), 5000);
+ },
+
/* UCI Changes */
changes: L.Class.singleton({
init: function() {