Skip to content

Commit

Permalink
luci-app-example: Update with more documentation, more examples (#6503)
Browse files Browse the repository at this point in the history
* 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

(cherry picked from commit 28f805b)
  • Loading branch information
cricalix authored and systemcrash committed Dec 4, 2023
1 parent cf8f08e commit d993287
Show file tree
Hide file tree
Showing 15 changed files with 1,210 additions and 86 deletions.
47 changes: 47 additions & 0 deletions applications/luci-app-example/BUILDING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Building a LuCI package

Essentially, you follow the [build system](https://openwrt.org/docs/guide-developer/toolchain/use-buildsystem) instructions to fetch the OpenWrt repository, update the `feeds.conf.default` to point `luci` at a local directory, build out the full toolchain, and then follow the instructions for a [single package](https://openwrt.org/docs/guide-developer/toolchain/single.package) to build the `.opkg` file for the example app.

Wiki documentation overrides this file.

## Setup

* Create a working directory, like `~/src`
* Clone the OpenWrt repository into `~/src/openwrt`
* Clone the LuCI repository into `~/src/luci`

From here on you'll be working in `~/src/openwrt`

## Remapping LuCI source to local disk

* Edit `~/src/openwrt/feeds.conf.default` and comment out the `src-git luci` entry
* Add a `src-link luci` entry pointing to your luci checkout - for example `src-link luci /home/myuser/src/luci`
* Use the `scripts/feeds` tool per the [documentation](https://openwrt.org/docs/guide-developer/toolchain/use-buildsystem#updating_feeds) to update and install all feeds; you should see the local directory get used for luci

If you're doing a whole new application, instead of editing this one, you can use the `src-link custom` example instead as a basis, leaving `src-git luci` alone.

## Selecting the app

* Run `make menuconfig`
* Change the Target system to match your test environment (x86 for QEMU for instance)
* Select the LuCI option
* Select the Applications option
* Navigate the list to find `luci-app-example`
* Press `m` to make the selection be `<M>` - modular build
* Choose Exit all the way back out, and save the configuration

## Toolchain build

Even though you're only building a simple JS + Lua package, you'll need the whole toolchain. Though the command says "install", nothing is actually installed outside of the working directory (`~/src/openwrt` in this case).

* Run `make tools/install`
* Run `make toolchain/install`

## Package build

This will trigger the build of all the dependencies, such as **ubus**, **libjson-c**, **rpcd** etcetera.

* Run `make package/luci-app-example/compile`

The IPK file will be produced in `bin/packages/<architecture>/luci/`. This file can be copied to your test environment (QEMU, real hardware etcetera), and installed with `opkg`.

1 change: 1 addition & 0 deletions applications/luci-app-example/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ include $(TOPDIR)/rules.mk

LUCI_TITLE:=LuCI example app for js based luci
LUCI_DEPENDS:=+luci-base
LUCI_PKGARCH:=all

include ../../luci.mk

Expand Down
87 changes: 76 additions & 11 deletions applications/luci-app-example/README.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,90 @@
# Example app for js based Luci

This app is meant to be a kind of template, example or starting point for developing new luci apps.
This app is meant to be a starting point for developing new LuCI apps using the modern JavaScript client-rendered approach (versus the older Lua server-side render approach).

It provides two pages in the admin backend:
* [htmlview.js](./htdocs/luci-static/resources/view/example/htmlview.js) is based on a view with a form and makes use of internal models.
* [form.js](./htdocs/luci-static/resources/view/example/form.js) uses the `E()` method to create more flexible pages.
# Installation

The view based page is used to modify the example configuration.
In all cases, you'll want to log out of the web interface and back in to force a cache refresh after installing the new package.

The html view page just shows the configured values.
## From git

The configuration is stored in `/etc/config/example`.
The file must exist and created on device boot by UCI defaults script in `/root/etc/uci-defaults/80_example`.
More details about the UCI defaults https://openwrt.org/docs/guide-developer/uci-defaults
To install the luci-app-example to your OpenWrt instance (assuming your OpenWRT instance is on 192.168.1.1):

To install the luci-app-example to your OpenWrt instance use:
```
scp -r root/* [email protected]:/
scp -r htdocs/* [email protected]:/www/
# execute the UCI defaults script to create the /etc/config/example
ssh [email protected] "sh /etc/uci-defaults/80_example"
```

Then you need to re-login to LUCI and you'll see a new Example item in main menu.
## From packages

Install the app on your OpenWrt installation. This can be an actual router/device, or something like a QEMU virtual machine.

`opkg install luci-app-example`

Visit the web UI for the device/virtual machine where the package was installed, log in to OpenWrt, and **Example** should be present in the navigation menu.

# Application structure

See `structure.md` for details on how to lay out a LuCI application.

# Code format

The LuCI Javascript code should be indented with tabs. js-beautify/jsbeautifier can help with this; the examples in this application were formatted with

`js-beautify -t -a -j -w 110 -r <filename>`

# Editing the code

You can either do direct editing on the device/virtual machine, or use something like sshfs to have remote access from your development computer.

By default, the code is minified by the build process, which makes editing it non-trivial. You can either change the build process, or just copy the file content from the git repository and replace the content on disk.

Javascript code can be found on the device/virtual machine in `/www/luci-static/resources/view/example/`.

## [form.js](./htdocs/luci-static/resources/view/example/form.js)

This is a JS view that uses the **form.Map** approach to providing a form that can change the configuration. It relies on UCI access, and the relevant ACL declarations are in `root/usr/share/rpcd/acl.d/luci-app-example.json`.

The declarations are `luci-app-example > read > uci` and `luci-app-example > write > uci`. Note that for both permissions, the node name "example" is provided as a list argument to the interface type (**uci**); this maps to `/etc/config/example`.

Since form.Map and form.JSONMap create Promises, you cannot embed them inside a `E()`-built structure.

## [htmlview.js](./htdocs/luci-static/resources/view/example/htmlview.js)

This is a read-only view that uses `E()` to create DOM nodes.

Data is fetched via the function defined in `load()` - these loads are done as **Promises**, with the promise results stored in an array. Multiple load functions results are available in the array, and can be accessed via a single argument passed to the `render()` function.

This code relies on the same ACL grants as form.js.

The signature for `E()` is `E(node_type, {node attributes}, [child nodes])`.

## [rpc.js](./htdocs/luci-static/resources/view/example/rpc.js)

The RPC JS page is read-only, and demonstrates using RPC calls to get data. It also demonstrates using the JSONMap form object for mapping a configuration to a form, but makes the form read-only for display purposes.

The configuration is stored in `/etc/config/example`. The file must exist and created on device boot by UCI defaults script in `/root/etc/uci-defaults/80_example`. The [developer guide](https://openwrt.org/docs/guide-developer/uci-defaults) has more details about UCI defaults.

The RPCd script is stored as `/usr/libexec/rpcd/luci.example`, and can be called via ubus.

It relies on RPC access, and the relevant ACL declarations are in `root/usr/share/rpcd/acl.d/luci-app-example.json`.

The declaration is `luci-app-example > read > ubus > luci.example`; the list of names under this key is the list of APIs that can be called.

# ACLs

A small note on ACLs. They are global for the entire web UI - the declaration of **luci-app-example** in a file called `acl.d/luci-app-example` is just a naming convention; nothing enforces that only the code in **luci-app-example** is mutating `/etc/config/example`. Once the ACL is defined to allow reads/writes to a UCI node, any code running from the web UI can make changes to that node.

# YAML

You may wish to work with YAML data. See [YAML.md](YAML.md) for details on how to integrate YAML read support.

# Translations

For a real world application (or changes to this example one that you wish to submit upstream), translations should be kept up to date.

To rebuild the translations file, from the root of the repository execute `./build/i18n-scan.pl applications/luci-app-example > applications/luci-app-example/po/templates/example.pot`

If the scan command fails with an error about being unable to open/find `msguniq`, install the GNU `gettext` package for your operating system.
151 changes: 151 additions & 0 deletions applications/luci-app-example/YAML.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Processing YAML in Lua

You may need to deal with YAML data in your Lua code.

## root/usr/libexec/rpcd/luci.example
These are the changes you would need in the `usr/libexec/rpcd/luci.example` file.

First, declare that you want YAML libraries:

```
-- If you need to process YAML, opkg install lyaml
local lyaml = require "lyaml"
```

Then, declare a function to handle the YAML data, and a helper to read the file

```
local function readfile(path)
local s = fs.readfile(path)
return s and (s:gsub("^%s+", ""):gsub("%s+$", ""))
end
local function reading_from_yaml()
-- Use the locally declared readfile() function to read in the
-- sample YAML file that ships with this package.
local example_config = readfile("/etc/luci.example.yaml")
-- Map that string to a Lua table via lyaml's load() method
local example_table = lyaml.load(example_config)
-- Convert the table to JSON
local example_json = jsonc.stringify(example_table)
-- Pass the JSON back
return example_json
end
```

Declare the method in the `methods` table

```
-- Converts the AGH YAML configuration into JSON for consumption by
-- the LuCI app.
get_yaml_file_sample = {
-- A special key of 'call' points to a function definition for execution.
call = function()
local r = {}
r.result = reading_from_yaml()
-- The 'call' handler will refer to '.code', but also defaults if not found.
r.code = 0
-- Return the table object; the call handler will access the attributes
-- of the table.
return r
end
},
```

## htdocs/luci-static/resources/view/example/rpc.js

These are the changes you need in the `rpc.js` file.

Declare the RPC call

```
var load_sample_yaml = rpc.declare({
object: 'luci.example',
method: 'get_yaml_file_sample'
});
```

Add this declaration to the `view.extend()` call

```
render_sample_yaml: function(sample) {
console.log('render_sample_yaml()');
console.log(sample);
if (sample.error) {
return this.generic_failure(sample.error)
}
// Basically, a fully static table declaration.
var table = E('table', { 'class': 'table', 'id': 'sample-yaml' }, [
E('tr', {}, [
E('td', { 'class': 'td left', 'width': '33%' }, _("Top Level Int")),
E('td', { 'class': 'td left' }, sample.top_level_int),
]),
E('tr', {}, [
E('td', { 'class': 'td left', 'width': '33%' }, _("Top Level String")),
E('td', { 'class': 'td left' }, sample.top_level_string),
])
]);
return table;
},
```

Add a call to the `load` function in `view.extend()`

```
load: function () {
return Promise.all([
load_sample_yaml(),
load_sample1()
]);
},
```

Add this code to the `render` function in `view.extend()`

```
E('div', { 'class': 'cbi-section', 'id': 'cbi-sample-yaml' }, [
E('div', { 'class': 'left' }, [
E('h3', _('Sample YAML via RPC')),
E('div', {}), _("YAML transformed to JSON, table built explicitly"),
this.render_sample_yaml(sample_yaml),
]),
]),
```

## root/usr/share/rpcd/acl.d/luci-app-example.json

Allow access to the new RPC API

```
"read": {
"ubus": {
"luci.example": [
"get_yaml_file_sample",
"get_sample1",
"get_sample2"
]
},
```

## root/etc/luci.example.yaml

Set up the sample YAML file, by placing it either in `root/etc` of the development tree, or directly
in `/etc` on the target machine and call it `luci.example.yaml` to match up to the `reading_from_yaml`
function's expectations.

```
top_level_string: example
top_level_int: 8080
top_level:
list_elements:
- foo
- bar
```

That's it. Don't forget to also update the `LUCI_DEPENDS` segment of the `Makefile` to include
`+lyaml` so that the packaging system knows your code needs the YAML parsing package.
Original file line number Diff line number Diff line change
Expand Up @@ -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();
},
});
Loading

0 comments on commit d993287

Please sign in to comment.