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.
1<?php
2namespace OPNsense\GridExample;
3
4use OPNsense\Base\BaseModel;
5
6class GridExample extends BaseModel
7{
8}
1<model>
2 <mount>//OPNsense/GridExample</mount>
3 <description>
4 the OPNsense "GridExample" application
5 </description>
6 <items>
7 <addresses>
8 <address type="ArrayField">
9 <enabled type="BooleanField">
10 <default>1</default>
11 <Required>Y</Required>
12 </enabled>
13 <email type="EmailField">
14 <Required>Y</Required>
15 </email>
16 <description type="DescriptionField"/>
17 </address>
18 </addresses>
19 </items>
20</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)
1namespace OPNsense\GridExample\Api;
2
3use \OPNsense\Base\ApiMutableModelControllerBase;
4
5class SettingsController extends ApiMutableModelControllerBase
6{
7 protected static $internalModelName = 'gridexample';
8 protected static $internalModelClass = 'OPNsense\GridExample\GridExample';
9
10 public function searchItemAction()
11 {
12 return $this->searchBase("addresses.address", null, "email");
13 }
14
15 public function setItemAction($uuid)
16 {
17 return $this->setBase("address", "addresses.address", $uuid);
18 }
19
20 public function addItemAction()
21 {
22 return $this->addBase("address", "addresses.address");
23 }
24
25 public function getItemAction($uuid = null)
26 {
27 return $this->getBase("address", "addresses.address", $uuid);
28 }
29
30 public function delItemAction($uuid)
31 {
32 return $this->delBase("addresses.address", $uuid);
33 }
34
35 public function toggleItemAction($uuid, $enabled = null)
36 {
37 return $this->toggleBase("addresses.address", $uuid, $enabled);
38 }
39}
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 "address": {
3 "enabled": "1",
4 "email": "test@example.com"
5 "description": "Test Address"
6 }
7}
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.
For the grid creation, the same form will also create columns for each field id. The grid column formatting can be controlled via
the grid_view
tag. Inside this tag, we can define width
, type
, formatter
, sequence
, visible
, ignore
and
other tags.
<form>
<field>
<id>address.enabled</id>
<label>Enabled</label>
<type>checkbox</type>
<help>Enable this address</help>
<grid_view>
<width>6em</width>
<type>boolean</type>
<formatter>rowtoggle</formatter>
</grid_view>
</field>
<field>
<id>address.email</id>
<label>Email</label>
<type>text</type>
<help>Enter the email address</help>
</field>
<field>
<id>address.description</id>
<label>Description</label>
<type>text</type>
<help>Enter an optional description</help>
<grid_view>
<visible>false</visible>
</grid_view>
</field>
</form>
UI controller
The user interface controller sets the template (view) to use and collects the dialog form and grid properties from the xml file defined earlier.
1namespace OPNsense\GridExample;
2
3class IndexController extends \OPNsense\Base\IndexController
4{
5 public function indexAction()
6 {
7 $this->view->pick('OPNsense/GridExample/index');
8 $this->view->formDialogAddress = $this->getForm("dialogAddress");
9 $this->view->formGridAddress = $this->getFormGrid("dialogAddress");
10 }
11}
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 {{formGridAddress['table_id']}}
using UIBootgrid()
. Then we define the table which will be
changed into a dynamic searchable grid and we link our dialog content. Both grid (base_bootgrid_table) and dialog (base_dialog)
use 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.
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 'id':formGridAddress['edit_dialog_id']
, which is referenced in
the table {{formGridAddress['table_id']}}
, and finally the caption of the dialog.
To apply changes, there is a (base_apply_button) volt partial()
which calls a reconfigure API endpoint.
The event listener is attached to SimpleActionButton(), a shared javascript function for most reconfiguration use cases.
Whenever a change in the grid is performed, the (base_apply_button) will automatically show a data change dialog.
1<script>
2 $(document).ready(function() {
3 $("#{{formGridAddress['table_id']}}").UIBootgrid(
4 { search:'/api/gridexample/settings/search_item/',
5 get:'/api/gridexample/settings/get_item/',
6 set:'/api/gridexample/settings/set_item/',
7 add:'/api/gridexample/settings/add_item/',
8 del:'/api/gridexample/settings/del_item/',
9 toggle:'/api/gridexample/settings/toggle_item/'
10 }
11 );
12
13 $("#reconfigureAct").SimpleActionButton();
14 });
15
16</script>
17
18<div class="content-box">
19 {{ partial('layout_partials/base_bootgrid_table', formGridAddress) }}
20</div>
21{{ partial('layout_partials/base_apply_button', {'data_endpoint': '/api/gridexample/service/reconfigure'}) }}
22{{ partial("layout_partials/base_dialog",['fields':formDialogAddress,'id':formGridAddress['edit_dialog_id'],'label':lang._('Edit address')])}}
Service controller
The service controller in this example is a stub and generates a generic status response when calling /api/gridexample/service/reconfigure
via
the SimpleActionButton().
1namespace OPNsense\GridExample\Api;
2
3use OPNsense\Base\ApiControllerBase;
4
5class ServiceController extends ApiControllerBase
6{
7 public function reconfigureAction()
8 {
9 sleep(1);
10 return ["status" => "ok"];
11 }
12}