From 28f805b2e5f95e04515bd7d045a103cc3a366c2c Mon Sep 17 00:00:00 2001 From: Duncan Hill Date: Mon, 4 Dec 2023 21:12:28 +0000 Subject: 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 --- .../luci-static/resources/view/example/form.js | 83 +++++++----- .../luci-static/resources/view/example/htmlview.js | 60 +++++---- .../view/example/rpc-jsonmap-tablesection.js | 109 +++++++++++++++ .../view/example/rpc-jsonmap-typedsection.js | 109 +++++++++++++++ .../luci-static/resources/view/example/rpc.js | 150 +++++++++++++++++++++ 5 files changed, 456 insertions(+), 55 deletions(-) create mode 100644 applications/luci-app-example/htdocs/luci-static/resources/view/example/rpc-jsonmap-tablesection.js create mode 100644 applications/luci-app-example/htdocs/luci-static/resources/view/example/rpc-jsonmap-typedsection.js create mode 100644 applications/luci-app-example/htdocs/luci-static/resources/view/example/rpc.js (limited to 'applications/luci-app-example/htdocs/luci-static/resources/view/example') 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 -- cgit v1.2.3