LuCI Save & Apply

Hello,

I am developing an interface for an OpenWrt functionality that exposes a REST API.

So I have

devices_table = m:section(Table, devices, "Devices", "Devices")
devices_table:option(DummyValue, "hardwareAddress", "Hardware Address")
DeviceType = devices_table:option(ListValue, "deviceType", "Device Type")
deviceTypes(DeviceType)
devices_table:option(Value, "vendor", "Vendor")
devices_table:option(Value, "deviceName", "Device Name")
devices_table:option(Value, "deviceLocation", "Device Location")
sDeviceMode = devices_table:option(ListValue, "deviceMode", "Device Mode")
deviceMode(sDeviceMode)

I would like to override the save & Apply button to loop through my data and post the changed data to the REST API, is that possible, if so. How would I be able to do it?

I found some method to override the “save & apply” here :

But none of them work, I always have :

/usr/lib/lua/luci/cbi.lua:1524: attempt to call method 'set' (a nil value)

and the method is not called. I am missing something

I tried the section of the non-uci form, but I have an error with the form as it is in the documentation

I am at a loss here, I searched the documentation, and the code (or maybe there is some documentation that I did not find?).

Thank you very much

First of all, which LuCI and OpenWrt version? A lot has changed throughout the last few years. Without knowing further details (and assuming you do need to stick to a Lua based server side form solution) I would suggest to use the on_after_commit hook for the map.

I am with OpenWrt 21.02.2 / LuCI openwrt-21.02 branch git-22.046.85957-59c3392.

I don't necessarily need to stick with a Lua based server side solution, but I found more documentation on lua based serverside than the javascript one.

the m.on_after_commit hook still gives me the error :

/usr/lib/lua/luci/cbi.lua:1524: attempt to call method 'set' (a nil value)

Because my application is not based on uci, it is based on a REST API. So how would I by-pass the cui bit for the save & apply to work?

If you want to do development that's going to last a bit, you will have to look at the current code master (which has mostly gotten rid of lua and far more into ucode/ ACL control, etc.).

Ok, thank you all for the input.

I have reworked my ui in luci javascipt. The interesting thing I found for my case is JSONMap.

I load data from a REST API and then generate a GridSection from the JSON. But when I click “edit”, I got the error: "RPCError: RPC call to uci/get failed with ubus code 4: Resource not found".

I seem to be missing something here, I tried to define the extedit property to no avail.

My understanding is that a uci-like structure is created in memory to render the table, is that possible that when the "edit" button is clicked in the table, the UI tries to fetch the data from a file (which would not exist)?

Is there a way to manipulate my data in memory and do a REST POST on the "save - apply" or do I absolutely need a file in “/etc/config”?

Thank you very much for your support

Can you please post your current JSONMap definition?

Yes, sorry,

the JSONMap definition is :

  var o;
    m = new form.JSONMap(data, _('Device List'),
      _('A List of device and some properties.'));
    
    section = m.section(form.GridSection, 'device', _('Devices'));

    section.anonymous = true;
  
    o = section.option(form.DummyValue, 'hardwareAddress', _('Hardware Address'));
    o = section.option(form.ListValue, 'deviceType', _('Device Type'));

    o.value("tv", _("Tv"));
    o.value("gameConsole", _("Game Console"));
    o.value("phone", _("Phone"));
    o.value("computer", _("Computer"));
    o.value("Printer", _("Printer"));
    o.value("thermostat", _("Thermostat"));
    o.value("camera", _("Camera"));
    o.value("speaker", _("Speaker"));
    o.value("other", _("Other"));

    o = section.option(form.ListValue, 'deviceMode', _('Device Mode'));

    o.value("filtering", _("filtering"));
    o.value("analyzing", _("analyzing"));
    o.value("noAccess", _("noAccess"));
    o.value("fullAccess", _("fullAccess"));

    o = section.option(form.Value, 'vendor', _('Vendor'));
    o = section.option(form.Value, 'deviceName', _('Device Name'));
    o = section.option(form.Flag, 'isIoT', _('Is Iot Device'));
    o = section.option(form.Value, 'deviceLocation', _('Device Location'));

With the JSON being something like:

let data = {}
data['device'] = [
      {
        "deviceType" : "speaker",
        "hardwareAddress" : "E0:8f:7C:31:00:FF",
        "deviceMode" : "fullAccess",
        "vendor" : "Marley",
        "isIot" : false,
        "deviceName" : "Isaiah Gibson",
        "deviceLocation" : "Basement"
      },{
        "deviceType" : "tv",
        "hardwareAddress" : "E1:83:74:3a:CA:FA",
        "deviceMode" : "analyzing",
        "vendor" : "LG",
        "isAdmin" : false,
        "deviceName" : "Restroom TV",
        "deviceLocation" : "Restroom"
      },{
        "deviceType" : "comuter",
        "hardwareAddress" : "A0:00:75:CA:FA",
        "deviceMode" : "filtering",
        "vendor" : "Wemo",
        "isIot" : true,
        "deviceName" : "Centaurii",
        "deviceLocation" : "Kitchen"
      },{
        "deviceType" : "thermostat",
        "hardwareAddress" : "00:FF:00:FF:CA:FA",
        "deviceMode" : "fullAccess",
        "vendor" : "Hilo",
        "isAdmin" : true,
        "deviceName" : "Bob",
        "deviceLocation" : "Workshop"
      },{
        "deviceType" : "thermostat",
        "hardwareAddress" : "AA:FF:60:FF:CA:FA",
        "deviceMode" : "fullAccess",
        "vendor" : "Huawei",
        "isIot" : false,
        "deviceName" : "cell phone",
        "deviceLocation" : "Unknown"
      }
    ]

Thanks, it's a bug in the form class. Will look into it and get back to you.

The following view works for me:

'use strict';
'require dom';
'require view';
'require form';
'require request';

let formdata = {
    device: [
        {
            "deviceType" : "speaker",
            "hardwareAddress" : "E0:8f:7C:31:00:FF",
            "deviceMode" : "fullAccess",
            "vendor" : "Marley",
            "isIot" : false,
            "deviceName" : "Isaiah Gibson",
            "deviceLocation" : "Basement"
        }, {
            "deviceType" : "tv",
            "hardwareAddress" : "E1:83:74:3a:CA:FA",
            "deviceMode" : "analyzing",
            "vendor" : "LG",
            "isAdmin" : false,
            "deviceName" : "Restroom TV",
            "deviceLocation" : "Restroom"
        }, {
            "deviceType" : "comuter",
            "hardwareAddress" : "A0:00:75:CA:FA",
            "deviceMode" : "filtering",
            "vendor" : "Wemo",
            "isIot" : true,
            "deviceName" : "Centaurii",
            "deviceLocation" : "Kitchen"
        }, {
            "deviceType" : "thermostat",
            "hardwareAddress" : "00:FF:00:FF:CA:FA",
            "deviceMode" : "fullAccess",
            "vendor" : "Hilo",
            "isAdmin" : true,
            "deviceName" : "Bob",
            "deviceLocation" : "Workshop"
        }, {
            "deviceType" : "thermostat",
            "hardwareAddress" : "AA:FF:60:FF:CA:FA",
            "deviceMode" : "fullAccess",
            "vendor" : "Huawei",
            "isIot" : false,
            "deviceName" : "cell phone",
            "deviceLocation" : "Unknown"
        }
    ]
};

return view.extend({
    load: function() {
        // Simulate data loading by returning JSON fixture after 1000ms
        return new Promise((resolveFn, rejectFn) => {
            window.setTimeout(() => resolveFn(formdata), 1000);
        });
    },

    render: function(data) {
        var m, s, o;

        m = new form.JSONMap(data, _('Device List'),
          _('A List of device and some properties.'));

        s = m.section(form.GridSection, 'device', _('Devices'));

        s.anonymous = true;
        s.addremove = true;

        // Workaround uci/get bug by setting config property to
        // a uci configuration which is allowed by default ACLs
        // and by resetting the modal dialog map data provider
        // to the one of the parent JSON map.
        m.config = 'luci';
        s.addModalOptions = (ss) => { ss.map.data = m.data; };

        o = s.option(form.DummyValue, 'hardwareAddress', _('Hardware Address'));
        o = s.option(form.ListValue, 'deviceType', _('Device Type'));

        o.value("tv", _("Tv"));
        o.value("gameConsole", _("Game Console"));
        o.value("phone", _("Phone"));
        o.value("computer", _("Computer"));
        o.value("Printer", _("Printer"));
        o.value("thermostat", _("Thermostat"));
        o.value("camera", _("Camera"));
        o.value("speaker", _("Speaker"));
        o.value("other", _("Other"));

        o = s.option(form.ListValue, 'deviceMode', _('Device Mode'));

        o.value("filtering", _("filtering"));
        o.value("analyzing", _("analyzing"));
        o.value("noAccess", _("noAccess"));
        o.value("fullAccess", _("fullAccess"));

        o = s.option(form.Value, 'vendor', _('Vendor'));
        o = s.option(form.Value, 'deviceName', _('Device Name'));
        o = s.option(form.Flag, 'isIoT', _('Is Iot Device'));
        o = s.option(form.Value, 'deviceLocation', _('Device Location'));

        return m.render();
    },

    // Override save action
    handleSave: function(ev) {
        // Find map instance
        var map = document.querySelector('.cbi-map');

        // Invoke save method on map (return value may be a promise)
        var ret = dom.callClassMethod(map, 'save');

        // Resolve save result promise and initiate HTTP POST request with updated formdata
        return Promise.resolve(ret).then(() => {
            return request.post('https://example.com/myapi/v1', formdata);
        });
    },

    // Disable Save & Apply button
    handleSaveApply: null,

    // Disable Reset button
    handleReset: null
});

The two lines

        m.config = 'luci';
        s.addModalOptions = (ss) => { ss.map.data = m.data; };

will workaround the uci/get error on pressing Edit in a grid section row or a JSON map.

Fix pushed with

If you apply the workaround suggested above, your form should work with and without the upstream LuCI fix.