Using grids module & plugin

Goal

The purpose of this example is to show how to build data grids in OPNsense, using the various components within our framework.

If you haven’t read the HelloWorld example yet, we advise you to start there. This example assumes you already know the basics.

Our topic of choice for this module is a basic list for email addresses, for which you should be able to add, remove and change items.

Model

Our example starts with a model, which is constructed by creating a php class deriving from BaseModel and an XML file containing the actual model definition.

/usr/local/opnsense/mvc/app/models/OPNsense/GridExample/GridExample.php
1
2
3
4
5
6
7
8
<?php
namespace OPNsense\GridExample;

use OPNsense\Base\BaseModel;

class GridExample extends BaseModel
{
}
/usr/local/opnsense/mvc/app/models/OPNsense/GridExample/GridExample.xml
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<model>
    <mount>//OPNsense/GridExample</mount>
    <description>
        the OPNsense "GridExample" application
    </description>
    <items>
        <addresses>
            <address type="ArrayField">
                <enabled type="BooleanField">
                    <default>1</default>
                    <Required>Y</Required>
                </enabled>
                <email type="EmailField">
                    <Required>Y</Required>
                </email>
            </address>
        </addresses>
    </items>
</model>

Note the ArrayField type in the XML, this is a special field type for nested items in automatically includes an internal uuid for easy referencing when written to disk. Both other field types are also used in the HelloWorld example earlier. All the preinstalled types can be found in our field type directory on GitHub.

API controller

The ApiMutableModelControllerBase class supports most model manipulations, all *Base methods embody shared functionality to operate on either new or existing model items.

Our example below uses the base methods to link all operations we need and link them on endpoints ending at Item:

  • searchItemAction, queries the items in your configuration
  • getItemAction, fetches an existing record (or returns a blank one with all defaults)
  • addItemAction, add a new record
  • setItemAction, update a record
  • delItemAction, delete a record
  • toggleItemAction, toggle [0|1] the “enabled” property (see the enabled BooleanField in the model)
/usr/local/opnsense/mvc/app/controllers/OPNsense/GridExample/Api/SettingsController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
namespace OPNsense\GridExample\Api;

use \OPNsense\Base\ApiMutableModelControllerBase;

class SettingsController extends ApiMutableModelControllerBase
{
    protected static $internalModelName = 'gridexample';
    protected static $internalModelClass = 'OPNsense\GridExample\GridExample';

    public function searchItemAction()
    {
        return $this->searchBase("addresses.address", array('enabled', 'email'), "email");
    }

    public function setItemAction($uuid)
    {
        return $this->setBase("address", "addresses.address", $uuid);
    }

    public function addItemAction()
    {
        return $this->addBase("address", "addresses.address");
    }

    public function getItemAction($uuid = null)
    {
        return $this->getBase("address", "addresses.address", $uuid);
    }

    public function delItemAction($uuid)
    {
        return $this->delBase("addresses.address", $uuid);
    }

    public function toggleItemAction($uuid, $enabled = null)
    {
        return $this->toggleBase("addresses.address", $uuid, $enabled);
    }
}

The parameters of all methods contain at least the root of the ArrayField type you want to operate on and in cases the action involves form data the name of the root property used as in the container to transport data in.

For example, a getItem (/api/gridexample/settings/getItem/my-uuid-id) would return a response like this (highlighted the container):

1
2
3
4
5
6
{
  "address": {
    "enabled": "1",
    "email": "test@example.com"
  }
}

Define dialog items

To edit the data we define which fields should be presented to the user and how they are formatted. Below a simple layout, the id fields reference the actual data points to map (address.enabled for example), which is exactly what the api endpoint returns.

/usr/local/opnsense/mvc/app/controllers/OPNsense/GridExample/forms/dialogAddress.xml
<form>
    <field>
        <id>address.enabled</id>
        <label>enabled</label>
        <type>checkbox</type>
        <help>Enable this address</help>
    </field>
    <field>
        <id>address.email</id>
        <label>Email</label>
        <type>text</type>
    </field>
</form>

Constructing the volt template

We ship a javascript wrapper to implement a slightly modified version of jquery-bootgrid, to use this in our template (view) we define three different blocks.

First of all we bind a table by id (grid-addresses) using UIBootgrid(), then we define the table which will be changed into a dynamic searchable grid and finally we link our dialog content using a volt partial().

The basic “UIBootgrid” bind connects all actions which we have defined in our API controller earlier, there are more options available, but these are not needed for this use-case.

When defining the table, we need to add all fields that should be displayed and the order in which they should appear. If fields should not be visible by default, simply use data-visible="false" on the <th> tag.

Our edit dialog is being written in advance so the javascript code can open the statically defined form when needed, the last highlighted block takes care of this. The partial uses three argument, the variable connected via the controller containing all form entries, the name (id) of the form, which is referenced in the table (data-editDialog) and the caption of the dialog.

/usr/local/opnsense/mvc/app/views/OPNsense/GridExample/index.volt
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<script>
    $( document ).ready(function() {
        $("#grid-addresses").UIBootgrid(
            {   search:'/api/gridexample/settings/searchItem/',
                get:'/api/gridexample/settings/getItem/',
                set:'/api/gridexample/settings/setItem/',
                add:'/api/gridexample/settings/addItem/',
                del:'/api/gridexample/settings/delItem/',
                toggle:'/api/gridexample/settings/toggleItem/'
            }
        );
    });
</script>
<table id="grid-addresses" class="table table-condensed table-hover table-striped" data-editDialog="DialogAddress">
    <thead>
        <tr>
            <th data-column-id="uuid" data-type="string" data-identifier="true"  data-visible="false">{{ lang._('ID') }}</th>
            <th data-column-id="enabled" data-width="6em" data-type="string" data-formatter="rowtoggle">{{ lang._('Enabled') }}</th>
            <th data-column-id="email" data-type="string">{{ lang._('Email') }}</th>
            <th data-column-id="commands" data-width="7em" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
        </tr>
    </thead>
    <tbody>
    </tbody>
    <tfoot>
        <tr>
            <td></td>
            <td>
                <button data-action="add" type="button" class="btn btn-xs btn-default"><span class="fa fa-plus"></span></button>
                <button data-action="deleteSelected" type="button" class="btn btn-xs btn-default"><span class="fa fa-trash-o"></span></button>
            </td>
        </tr>
    </tfoot>
</table>


{{ partial("layout_partials/base_dialog",['fields':formDialogAddress,'id':'DialogAddress','label':lang._('Edit address')])}}

UI controller

The user interface controller sets the template (view) to use and collects the dialog form properties from the xml file defined earlier.

/usr/local/opnsense/mvc/app/controllers/OPNsense/GridExample/IndexController.php
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
namespace OPNsense\GridExample;

class IndexController extends \OPNsense\Base\IndexController
{
    public function indexAction()
    {
        $this->view->pick('OPNsense/GridExample/index');
        $this->view->formDialogAddress = $this->getForm("dialogAddress");
    }
}

Test drive your app

Now go to http[s]://your.host/ui/gridexample and try it out.

../../_images/grid-test-drive.png