summaryrefslogtreecommitdiffhomepage
path: root/applications/luci-app-example/htdocs/luci-static/resources
diff options
context:
space:
mode:
authorDuncan Hill <github.com@cricalix.net>2023-12-04 21:12:28 +0000
committerGitHub <noreply@github.com>2023-12-04 22:12:28 +0100
commit28f805b2e5f95e04515bd7d045a103cc3a366c2c (patch)
tree4c52498b38b97efdc3660c404a0341027d391518 /applications/luci-app-example/htdocs/luci-static/resources
parenta5786b5f2f68596b2ea8441fc8ad8ad7cf2ce2a9 (diff)
luci-app-example: Update with more documentation, more examples (#6503)
* luci-app-example: Update with more documentation, examples * Update translations file * Move more YAML support to .md file, improve README * luci-app-example: Update with more documentation, examples * luci-app-example: Fix missed call to load_sample_yaml * Format with tabs by using jsbeautify
Diffstat (limited to 'applications/luci-app-example/htdocs/luci-static/resources')
-rw-r--r--applications/luci-app-example/htdocs/luci-static/resources/view/example/form.js83
-rw-r--r--applications/luci-app-example/htdocs/luci-static/resources/view/example/htmlview.js60
-rw-r--r--applications/luci-app-example/htdocs/luci-static/resources/view/example/rpc-jsonmap-tablesection.js109
-rw-r--r--applications/luci-app-example/htdocs/luci-static/resources/view/example/rpc-jsonmap-typedsection.js109
-rw-r--r--applications/luci-app-example/htdocs/luci-static/resources/view/example/rpc.js150
5 files changed, 456 insertions, 55 deletions
diff --git a/applications/luci-app-example/htdocs/luci-static/resources/view/example/form.js b/applications/luci-app-example/htdocs/luci-static/resources/view/example/form.js
index 75fa3c3079..976df08088 100644
--- a/applications/luci-app-example/htdocs/luci-static/resources/view/example/form.js
+++ b/applications/luci-app-example/htdocs/luci-static/resources/view/example/form.js
@@ -2,35 +2,58 @@
'require view';
'require form';
+// Project code format is tabs, not spaces
return view.extend({
- render: function() {
- var m, s, o;
-
- m = new form.Map('example', _('Example Form'),
- _('Example Form Configuration.'));
-
- s = m.section(form.TypedSection, 'first', _('first section'));
- s.anonymous = true;
-
- s.option(form.Value, 'first_option', _('First Option'),
- _('Input for the first option'));
-
- s = m.section(form.TypedSection, 'second', _('second section'));
- s.anonymous = true;
-
- o = s.option(form.Flag, 'flag', _('Flag Option'),
- _('A boolean option'));
- o.default = '1';
- o.rmempty = false;
-
- o = s.option(form.ListValue, 'select', _('Select Option'),
- _('A select option'));
- o.placeholder = 'placeholder';
- o.value('key1', 'value1');
- o.value('key2', 'value2');
- o.rmempty = false;
- o.editable = true;
-
- return m.render();
- },
+ render: function() {
+ var m, s, o;
+
+ /*
+ The first argument to form.Map() maps to the configuration file available
+ via uci at /etc/config/. In this case, 'example' maps to /etc/config/example.
+
+ If the file is completely empty, the form sections will indicate that the
+ section contains no values yet. As such, your package installation (LuCI app
+ or software that the app configures) should lay down a basic configuration
+ file with all the needed sections.
+
+ The relevant ACL path for reading a configuration with UCI this way is
+ read > uci > ["example"]
+
+ The relevant ACL path for writing back the configuration is
+ write > uci > ["example"]
+ */
+ m = new form.Map('example', _('Example Form'),
+ _('Example Form Configuration.'));
+
+ s = m.section(form.TypedSection, 'first', _('first section'));
+ s.anonymous = true;
+
+ s.option(form.Value, 'first_option', _('First Option'),
+ _('Input for the first option'));
+
+ s = m.section(form.TypedSection, 'second', _('second section'));
+ s.anonymous = true;
+
+ o = s.option(form.Flag, 'flag', _('Flag Option'),
+ _('A boolean option'));
+ o.default = '1';
+ o.rmempty = false;
+
+ o = s.option(form.ListValue, 'select', _('Select Option'),
+ _('A select option'));
+ o.placeholder = 'placeholder';
+ o.value('key1', 'value1');
+ o.value('key2', 'value2');
+ o.rmempty = false;
+ o.editable = true;
+
+ s = m.section(form.TypedSection, 'third', _('third section'));
+ s.anonymous = true;
+ o = s.option(form.Value, 'password_option', _('Password Option'),
+ _('Input for a password (storage on disk is not encrypted)'));
+ o.password = true;
+ o = s.option(form.DynamicList, 'list_option', _('Dynamic list option'));
+
+ return m.render();
+ },
});
diff --git a/applications/luci-app-example/htdocs/luci-static/resources/view/example/htmlview.js b/applications/luci-app-example/htdocs/luci-static/resources/view/example/htmlview.js
index feae899a17..7dd8a4137b 100644
--- a/applications/luci-app-example/htdocs/luci-static/resources/view/example/htmlview.js
+++ b/applications/luci-app-example/htdocs/luci-static/resources/view/example/htmlview.js
@@ -3,28 +3,38 @@
'require view';
return view.extend({
- handleSaveApply: null,
- handleSave: null,
- handleReset: null,
- load: function() {
- return Promise.all([
- uci.load('example')
- ]);
- },
- render: function(data) {
- var body = E([
- E('h2', _('Example HTML Page'))
- ]);
- var sections = uci.sections('example');
- var listContainer = E('div');
- var list = E('ul');
- list.appendChild(E('li', { 'class': 'css-class' }, ['First Option in first section: ', E('em', {}, [sections[0].first_option])]));
- list.appendChild(E('li', { 'class': 'css-class' }, ['Flag in second section: ', E('em', {}, [sections[1].flag])]));
- list.appendChild(E('li', { 'class': 'css-class' }, ['Select in second section: ', E('em', {}, [sections[1].select])]));
- listContainer.appendChild(list);
- body.appendChild(listContainer);
- console.log(sections);
- return body;
- }
- });
-
+ handleSaveApply: null,
+ handleSave: null,
+ handleReset: null,
+ load: function () {
+ return Promise.all([
+ // The relevant ACL path for reading a configuration with UCI this way is
+ // read > uci > ["example"]
+ uci.load('example')
+ ]);
+ },
+ render: function (data) {
+ var body = E([
+ E('h2', _('Example HTML Page'))
+ ]);
+ var sections = uci.sections('example');
+ var listContainer = E('div');
+ var list = E('ul');
+ // Note that this is pretty error-prone, because sections might be missing
+ // etcetera.
+ list.appendChild(E('li', {
+ 'class': 'css-class'
+ }, ['First Option in first section: ', E('em', {}, [sections[0]
+ .first_option])]));
+ list.appendChild(E('li', {
+ 'class': 'css-class'
+ }, ['Flag in second section: ', E('em', {}, [sections[1].flag])]));
+ list.appendChild(E('li', {
+ 'class': 'css-class'
+ }, ['Select in second section: ', E('em', {}, [sections[1].select])]));
+ listContainer.appendChild(list);
+ body.appendChild(listContainer);
+ console.log(sections);
+ return body;
+ }
+}); \ No newline at end of file
diff --git a/applications/luci-app-example/htdocs/luci-static/resources/view/example/rpc-jsonmap-tablesection.js b/applications/luci-app-example/htdocs/luci-static/resources/view/example/rpc-jsonmap-tablesection.js
new file mode 100644
index 0000000000..4a009782e4
--- /dev/null
+++ b/applications/luci-app-example/htdocs/luci-static/resources/view/example/rpc-jsonmap-tablesection.js
@@ -0,0 +1,109 @@
+'use strict';
+'require form';
+'require rpc';
+'require view';
+
+/*
+Declare the RPC calls that are needed. The object value maps to the name
+listed by the shell command
+
+$ ubus list
+
+Custom scripts can be placed in /usr/libexec/rpcd, and must emit JSON. The name of the file
+in that directory will be the value for the object key in the declared map.
+
+Permissions to make these calls must be granted in /usr/share/rpcd/acl.d
+via a file named the same as the application package name (luci-app-example)
+*/
+var load_sample2 = rpc.declare({
+ object: 'luci.example',
+ method: 'get_sample2'
+});
+
+function capitalize(message) {
+ var arr = message.split(" ");
+ for (var i = 0; i < arr.length; i++) {
+ arr[i] = arr[i].charAt(0).toUpperCase() + arr[i].slice(1);
+ }
+ return arr.join(" ");
+}
+
+return view.extend({
+ generic_failure: function (message) {
+ // Map an error message into a div for rendering
+ return E('div', {
+ 'class': 'error'
+ }, [_('RPC call failure: '), message])
+ },
+ render_sample2_using_jsonmap: function (sample) {
+ console.log('render_sample2_using_jsonmap()');
+ console.log(sample);
+
+ // Handle errors as before
+ if (sample.error) {
+ return this.generic_failure(sample.error)
+ }
+
+ // Variables you'll usually see declared in LuCI JS apps; forM, Section, Option
+ var m, s, o;
+
+ /*
+ LuCI has the concept of a JSONMap. This will map a structured object to
+ a configuration form. Normally you'd use this with a UCI-powered result,
+ since saving via an RPC call is going to be more difficult than using the
+ built-in UCI/ubus libraries.
+
+ https://openwrt.github.io/luci/jsapi/LuCI.form.JSONMap.html
+
+ Execute ubus call luci.example get_sample2 to see the JSON being used.
+ */
+ m = new form.JSONMap(sample, _('JSONMap TableSection Sample'), _(
+ 'See browser console for raw data'));
+ // Make the form read-only; this only matters if the apply/save/reset handlers
+ // are not replaced with null to disable them.
+ m.readonly = true;
+ // Set up for a tabbed display
+ m.tabbed = false;
+
+ const option_names = Object.keys(sample);
+ for (var i = option_names.length - 1; i >= 0; i--) {
+ var option_name = option_names[i];
+ var display_name = option_name.replace("_", " ");
+ s = m.section(form.TableSection, option_name, capitalize(display_name), _(
+ 'Description for this table section'))
+ o = s.option(form.Value, 'name', _('Option name'));
+ o = s.option(form.Value, 'value', _('Option value'));
+ o = s.option(form.DynamicList, 'parakeets', 'Parakeets');
+ }
+ return m;
+ },
+ /*
+ load() is called on first page load, and the results of each promise are
+ placed in an array in the call order. This array is passed to the render()
+ function as the first argument.
+ */
+ load: function () {
+ return Promise.all([
+ load_sample2(),
+ ]);
+ },
+ // render() is called by the LuCI framework to do any data manipulation, and the
+ // return is used to modify the DOM that the browser shows.
+ render: function (data) {
+ // data[0] will be the result from load_sample2
+ var sample2 = data[0] || {};
+ return this.render_sample2_using_jsonmap(sample2).render();
+ },
+ /*
+ Since this is a view-only screen, the handlers are disabled
+ Normally, when using something like Map or JSONMap, you would
+ not null out these handlers, so that the form can be saved/applied.
+
+ With a RPC data source, you would need to define custom handlers
+ that verify the changes, and make RPC calls to a backend that can
+ process the request.
+ */
+ handleSave: null,
+ handleSaveApply: null,
+ handleReset: null
+}) \ No newline at end of file
diff --git a/applications/luci-app-example/htdocs/luci-static/resources/view/example/rpc-jsonmap-typedsection.js b/applications/luci-app-example/htdocs/luci-static/resources/view/example/rpc-jsonmap-typedsection.js
new file mode 100644
index 0000000000..58958054ac
--- /dev/null
+++ b/applications/luci-app-example/htdocs/luci-static/resources/view/example/rpc-jsonmap-typedsection.js
@@ -0,0 +1,109 @@
+'use strict';
+'require form';
+'require rpc';
+'require view';
+
+/*
+Declare the RPC calls that are needed. The object value maps to the name
+listed by the shell command
+
+$ ubus list
+
+Custom scripts can be placed in /usr/libexec/rpcd, and must emit JSON. The name of the file
+in that directory will be the value for the object key in the declared map.
+
+Permissions to make these calls must be granted in /usr/share/rpcd/acl.d
+via a file named the same as the application package name (luci-app-example)
+*/
+var load_sample2 = rpc.declare({
+ object: 'luci.example',
+ method: 'get_sample2'
+});
+
+function capitalize(message) {
+ var arr = message.split(" ");
+ for (var i = 0; i < arr.length; i++) {
+ arr[i] = arr[i].charAt(0).toUpperCase() + arr[i].slice(1);
+ }
+ return arr.join(" ");
+}
+
+return view.extend({
+ generic_failure: function (message) {
+ // Map an error message into a div for rendering
+ return E('div', {
+ 'class': 'error'
+ }, [_('RPC call failure: '), message])
+ },
+ render_sample2_using_jsonmap: function (sample) {
+ console.log('render_sample2_using_jsonmap()');
+ console.log(sample);
+
+ // Handle errors as before
+ if (sample.error) {
+ return this.generic_failure(sample.error)
+ }
+
+ // Variables you'll usually see declared in LuCI JS apps; forM, Section, Option
+ var m, s, o;
+
+ /*
+ LuCI has the concept of a JSONMap. This will map a structured object to
+ a configuration form. Normally you'd use this with a UCI-powered result,
+ since saving via an RPC call is going to be more difficult than using the
+ built-in UCI/ubus libraries.
+
+ https://openwrt.github.io/luci/jsapi/LuCI.form.JSONMap.html
+
+ Execute ubus call luci.example get_sample2 to see the JSON being used.
+ */
+ m = new form.JSONMap(sample, _('JSONMap TypedSection Sample'), _(
+ 'See browser console for raw data'));
+ // Make the form read-only; this only matters if the apply/save/reset handlers
+ // are not replaced with null to disable them.
+ m.readonly = true;
+ // Set up for a tabbed display
+ m.tabbed = true;
+
+ const option_names = Object.keys(sample);
+ for (var i = option_names.length - 1; i >= 0; i--) {
+ var option_name = option_names[i];
+ var display_name = option_name.replace("_", " ");
+ s = m.section(form.TypedSection, option_name, capitalize(display_name), _(
+ 'Description for this typed section'))
+ o = s.option(form.Value, 'name', _('Option name'));
+ o = s.option(form.Value, 'value', _('Option value'));
+ o = s.option(form.DynamicList, 'parakeets', 'Parakeets');
+ }
+ return m;
+ },
+ /*
+ load() is called on first page load, and the results of each promise are
+ placed in an array in the call order. This array is passed to the render()
+ function as the first argument.
+ */
+ load: function () {
+ return Promise.all([
+ load_sample2(),
+ ]);
+ },
+ // render() is called by the LuCI framework to do any data manipulation, and the
+ // return is used to modify the DOM that the browser shows.
+ render: function (data) {
+ // data[0] will be the result from load_sample2
+ var sample2 = data[0] || {};
+ return this.render_sample2_using_jsonmap(sample2).render();
+ },
+ /*
+ Since this is a view-only screen, the handlers are disabled
+ Normally, when using something like Map or JSONMap, you would
+ not null out these handlers, so that the form can be saved/applied.
+
+ With a RPC data source, you would need to define custom handlers
+ that verify the changes, and make RPC calls to a backend that can
+ process the request.
+ */
+ handleSave: null,
+ handleSaveApply: null,
+ handleReset: null
+}) \ No newline at end of file
diff --git a/applications/luci-app-example/htdocs/luci-static/resources/view/example/rpc.js b/applications/luci-app-example/htdocs/luci-static/resources/view/example/rpc.js
new file mode 100644
index 0000000000..cf5885b5a2
--- /dev/null
+++ b/applications/luci-app-example/htdocs/luci-static/resources/view/example/rpc.js
@@ -0,0 +1,150 @@
+'use strict';
+'require form';
+'require rpc';
+'require view';
+
+/*
+Declare the RPC calls that are needed. The object value maps to the name
+listed by the shell command
+
+$ ubus list
+
+Custom scripts can be placed in /usr/libexec/rpcd, and must emit JSON. The name of the file
+in that directory will be the value for the object key in the declared map.
+
+Permissions to make these calls must be granted in /usr/share/rpcd/acl.d
+via a file named the same as the application package name (luci-app-example)
+*/
+var load_sample1 = rpc.declare({
+ object: 'luci.example',
+ method: 'get_sample1'
+});
+// Out of the box, this one will be blocked by the framework because there is
+// no ACL granting permission.
+var load_sample3 = rpc.declare({
+ object: 'luci.example',
+ method: 'get_sample3'
+});
+
+
+return view.extend({
+ generic_failure: function (message) {
+ // Map an error message into a div for rendering
+ return E('div', {
+ 'class': 'error'
+ }, [_('RPC call failure: '), message])
+ },
+ render_sample1_using_array: function (sample) {
+ console.log('render_sample1_using_array()');
+ console.log(sample);
+ /*
+ Some simple error handling. If the RPC APIs return a JSON structure of the
+ form {"error": "Some error message"} when there's a failure, then the UI
+ can check for the presence of the error attribute, and render a failure
+ widget instead of breaking completely.
+ */
+ if (sample.error) {
+ return this.generic_failure(sample.error)
+ }
+
+ /*
+ Approach 1 for mapping JSON data to a simple table for display. The listing looks
+ a bit like key/value pairs, but is actually just an array. The loop logic later
+ on must iterate by 2 to get the labels.
+ */
+ const fields = [
+ _('Cats'), sample.num_cats,
+ _('Dogs'), sample.num_dogs,
+ _('Parakeets'), sample.num_parakeets,
+ _('Should be "Not found"'), sample.not_found,
+ _('Is this real?'), sample.is_this_real ? _('Yes') : _('No'),
+ ];
+
+ /*
+ Declare a table element using an automatically available function - E(). E()
+ produces a DOM node, where the first argument is the type of node to produce,
+ the second argument is an object of attributes for that node, and the third
+ argument is an array of child nodes (which can also be E() calls).
+ */
+ var table = E('table', {
+ 'class': 'table',
+ 'id': 'approach-1'
+ });
+
+ // Loop over the array, starting from index 0. Every even-indexed second element is
+ // the label (left column) and the odd-indexed elements are the value (right column)
+ for (var i = 0; i < fields.length; i += 2) {
+ table.appendChild(
+ E('tr', {
+ 'class': 'tr'
+ }, [
+ E('td', {
+ 'class': 'td left',
+ 'width': '33%'
+ }, [fields[i]]),
+ E('td', {
+ 'class': 'td left'
+ }, [(fields[i + 1] != null) ? fields[i + 1] : _('Not found')])
+ ]));
+ }
+ return table;
+ },
+
+ /*
+ load() is called on first page load, and the results of each promise are
+ placed in an array in the call order. This array is passed to the render()
+ function as the first argument.
+ */
+ load: function () {
+ return Promise.all([
+ load_sample1()
+ ]);
+ },
+ // render() is called by the LuCI framework to do any data manipulation, and the
+ // return is used to modify the DOM that the browser shows.
+ render: function (data) {
+ // data[0] will be the result from load_sample1
+ var sample1 = data[0] || {};
+ // data[1] will be the result from load_sample_yaml
+ var sample_yaml = data[1] || {};
+
+ // Render the tables as individual sections.
+ return E('div', {}, [
+ E('div', {
+ 'class': 'cbi-section warning'
+ }, _('See browser console for raw data')),
+ E('div', {
+ 'class': 'cbi-map',
+ 'id': 'map'
+ }, [
+ E('div', {
+ 'class': 'cbi-section',
+ 'id': 'cbi-sample-js'
+ }, [
+ E('div', {
+ 'class': 'left'
+ }, [
+ // _() notation on strings is used for translation detection
+ E('h3', _('Sample JS via RPC')),
+ E('div', {}), _(
+ "JSON converted to table via array building and loop"
+ ),
+ this.render_sample1_using_array(sample1)
+ ]),
+ ]),
+ ]),
+ ]);
+ },
+ /*
+ Since this is a view-only screen, the handlers are disabled
+ Normally, when using something like Map or JSONMap, you would
+ not null out these handlers, so that the form can be saved/applied.
+
+ With a RPC data source, you would need to define custom handlers
+ that verify the changes, and make RPC calls to a backend that can
+ process the request.
+ */
+ handleSave: null,
+ handleSaveApply: null,
+ handleReset: null
+}) \ No newline at end of file