mirror of
https://github.com/opnsense/docs.git
synced 2026-04-06 17:34:58 -04:00
446 lines
17 KiB
ReStructuredText
446 lines
17 KiB
ReStructuredText
=================
|
|
Dashboard widgets
|
|
=================
|
|
|
|
-------
|
|
General
|
|
-------
|
|
|
|
OPNsense provides an easy framework for developing dashboard widgets within a simple abstraction layer.
|
|
Each widget is a separate Javascript module that extends from a base widget class. Each widget exposes a set of functions
|
|
that are called by the dashboard framework logic. Each widget class also exposes the API endpoints it uses to fetch
|
|
data, so that the dashboard controller can apply per-widget ACL checks.
|
|
|
|
The framework provides the following features:
|
|
|
|
- A responsive and dynamic grid that allows the user to build up a custom layout.
|
|
- The layout can be saved per logged in user.
|
|
- Administrators that restrict access to specific pages for specific users will automatically restrict
|
|
access to the widgets that also use the same data feed.
|
|
- Widgets are fully asynchronous, meaning they are fully independent from one another, making sure that resource intensive
|
|
widgets will not hold up other widgets.
|
|
|
|
This framework uses `GridStack <https://gridstackjs.com/>`__ to create the dashboard grid.
|
|
|
|
Widgets are placed in the :code:`src/opnsense/www/js/widgets/` directory.
|
|
|
|
-------
|
|
Example
|
|
-------
|
|
|
|
Before going into any details, it is often most useful to present an example that includes most of the core logic:
|
|
the `interfaces overview <https://github.com/opnsense/core/blob/master/src/opnsense/www/js/widgets/Interfaces.js>`__ widget.
|
|
|
|
|
|
.. _functions:
|
|
|
|
---------
|
|
Functions
|
|
---------
|
|
|
|
The `BaseWidget <https://github.com/opnsense/core/blob/master/src/opnsense/www/js/widgets/BaseWidget.js>`__ shows the skeleton
|
|
of the widget Javascript module. Widgets extend this class to provide defaults to the framework. To make life a little
|
|
easier for common patterns, other base widgets may also be exposed. Currently these are:
|
|
|
|
- `BaseTableWidget <https://github.com/opnsense/core/blob/master/src/opnsense/www/js/widgets/BaseTableWidget.js>`__:
|
|
Exposes a dynamic table that can be configured in multiple orientations and only needs a data feed.
|
|
|
|
- `BaseGaugeWidget <https://github.com/opnsense/core/blob/master/src/opnsense/www/js/widgets/BaseGaugeWidget.js>`__:
|
|
Exposes a Gauge widget that allows presenting simple current/total values with multiple hooks to customize the widget.
|
|
|
|
The following functions are available to be overridden by the widget when extended from the BaseWidget:
|
|
|
|
*Constructor*
|
|
=====================================================================================================================
|
|
|
|
.. code-block:: javascript
|
|
|
|
constructor(config) {}
|
|
|
|
To provide sensible defaults to the framework, a derived javascript class should always call :code:`super()` first in the constructor.
|
|
Afterwards, the defaults can be overridden. The properties are:
|
|
|
|
- :code:`this.title`. Sets the title of the widget in the header.
|
|
- :code:`this.tickTimeout`. Sets the interval (in seconds) in which the :code:`onWidgetTick()` function is called. The default is 10 seconds.
|
|
|
|
If the widget has been persisted (the user pressed 'save'), the loaded widget configuration is passed in the constructor. Any
|
|
custom data necessary for the widget to properly reload itself can be fetched using the :code:`await this.getWidgetConfig()` function.
|
|
See the :ref:`Configurable widgets <configurable_widget>` section.
|
|
|
|
*getGridOptions*
|
|
=====================================================================================================================
|
|
|
|
.. code-block:: javascript
|
|
|
|
getGridOptions() {}
|
|
|
|
To provide flexibility, the widget can optionally override this function and return an object that will be merged and loaded
|
|
into the GridStack API. This function is called before the widget is rendered to the DOM. For example, the following code:
|
|
|
|
.. code-block:: javascript
|
|
|
|
getGridOptions() {
|
|
return {
|
|
// trigger overflow-y:scroll after 650px height
|
|
sizeToContent: 650
|
|
}
|
|
}
|
|
|
|
will insert the :code:`sizeToContent: 650` key-value pair into the GridStack options, making sure that the height of the widget
|
|
does not exceed a maximum of 650 pixels before a scrollbar is inserted. The GridStack API reference can be found
|
|
`here <https://github.com/gridstack/gridstack.js/blob/master/doc/README.md>`__.
|
|
|
|
This object is also persisted once the dashboard has been saved, meaning these properties are also passed in the constructor
|
|
on a widget reload.
|
|
|
|
The properties do not have to correspond to the GridStack API, any custom data can be pushed here.
|
|
|
|
*getMarkup*
|
|
=====================================================================================================================
|
|
|
|
.. code-block:: javascript
|
|
|
|
getMarkup() {}
|
|
|
|
This function must return a jQuery object that contains the static markup that's necessary to build the layout
|
|
of the widget. This function will usually just return the container (with styling attached) where dynamic content
|
|
will be loaded using `onMarkupRendered()`
|
|
|
|
*ajaxCall*
|
|
=====================================================================================================================
|
|
|
|
.. code-block:: javascript
|
|
|
|
ajaxCall(url, data, method='GET') {}
|
|
|
|
This function is a wrapper around the jQuery AJAX function. It is used to make API calls to the backend. The function
|
|
is internally bound to a retry mechanism, so if the API call fails, it will be retried after a short interval. By default
|
|
the call will fail after three attempts, after which the widget will show a generic error message.
|
|
|
|
Certain calls require a POST HTTP verb to be used to be able to send data to the backend. In this case, the :code:`method` parameter
|
|
can be changed to :code:`POST` and the :code:`data` parameter can be filled with the data to be sent.
|
|
|
|
*onMarkupRendered*
|
|
=====================================================================================================================
|
|
|
|
.. code-block:: javascript
|
|
|
|
async onMarkupRendered() {}
|
|
|
|
As soon as the dashboard has loaded, and all widget markup has been rendered to the DOM, dynamic content can be
|
|
provided to fill the widget by defining this function. Since this is an :code:`async` function, any API call
|
|
within this function must be awaited. For example:
|
|
|
|
.. code-block:: javascript
|
|
|
|
async onMarkupRendered() {
|
|
await this.ajaxCall('/api/interfaces/overview/interfacesInfo', {}, (data, status) => {
|
|
// do something with the data
|
|
});
|
|
}
|
|
|
|
This will make sure that all other widgets remain responsive, and a spinner appears while the data is being loaded.
|
|
Use jQuery to update the markup as prepared by :code:`getMarkup()`.
|
|
|
|
*onWidgetResize*
|
|
=====================================================================================================================
|
|
|
|
.. code-block:: javascript
|
|
|
|
onWidgetResize(elem, width, height) {}
|
|
|
|
If a widget is resized by the user, or is resized due to layout constraints / browser resize, this function will be called
|
|
with the updated width and height. The widget element is passed into the function as well.
|
|
|
|
Use this function to keep the widget responsive and the layout coherent for different sizes. For example:
|
|
|
|
.. code-block:: javascript
|
|
|
|
onWidgetResize(elem, width, height) {
|
|
if (width > 500) {
|
|
$('.interface-info-detail').parent().show();
|
|
$('.interface-info').css('justify-content', 'initial');
|
|
$('.interface-info').css('text-align', 'left');
|
|
} else {
|
|
$('.interface-info-detail').parent().hide();
|
|
$('.interface-info').css('justify-content', 'center');
|
|
$('.interface-info').css('text-align', 'center');
|
|
}
|
|
}
|
|
|
|
The above code will make sure that if the width of the widget is less than 500px wide, less critical
|
|
information is removed. Adjust the styling as necessary.
|
|
|
|
.. warning::
|
|
|
|
While this function is debounced (throttled to prevent excessive calls), it is still executed often during a resize.
|
|
If this function is doing a lot of heavy lifting, make sure you implement a notion of state to prevent
|
|
the same logic from executing more than necessary. An example of this can be found in the
|
|
`BaseTableWidget <https://github.com/opnsense/core/blob/master/src/opnsense/www/js/widgets/BaseTableWidget.js>`__.
|
|
|
|
If you return true from this function, the grid will be forcefully updated to adjust to a new layout.
|
|
|
|
*onWidgetTick*
|
|
=====================================================================================================================
|
|
|
|
.. code-block:: javascript
|
|
|
|
onWidgetTick() {}
|
|
|
|
This function is called every :code:`this.tickTimeout` seconds. While the dashboard is open, this function
|
|
is used to update the data presented on the dashboard.
|
|
|
|
*onWidgetClose*
|
|
=====================================================================================================================
|
|
|
|
.. code-block:: javascript
|
|
|
|
onWidgetClose() {}
|
|
|
|
Executed when a widget is removed from the grid. Make sure to clean up any resources in use by this widget. It is
|
|
not always necessary to override this function, but it's possible you're using a third party library that requires
|
|
action to be taken when the widget is removed. An example is the cleanup of a rendered chart.
|
|
|
|
.. attention::
|
|
|
|
If you're using the BaseWidget EventSource mechanism, make sure to call :code:`super.onWidgetClose()` to cleanup
|
|
the persistent connection to the server.
|
|
|
|
*onVisibilityChanged*
|
|
=====================================================================================================================
|
|
|
|
.. code-block:: javascript
|
|
|
|
onVisibilityChanged(visible) {}
|
|
|
|
Executed when the visibility of the page has changed (tab or instance switch). You're very likely not going to need
|
|
this function, but if you do, make sure to call :code:`super.onVisibilityChanged(visible)`.
|
|
|
|
*openEventSource*
|
|
=====================================================================================================================
|
|
|
|
.. code-block:: javascript
|
|
|
|
openEventSource(url, onMesage);
|
|
|
|
When your widget requires a persistent connection to stream data, use the :code:`super.openEventSource()` function
|
|
with the API endpoint and a callback function. The :code:`onMessage` callback function takes in a single :code:`event`
|
|
parameter, of which the :code:`data` property contains the event data.
|
|
|
|
This function is bound to the same retry mechanism as the :code:`ajaxCall()` function.
|
|
|
|
*closeEventSource*
|
|
=====================================================================================================================
|
|
|
|
.. code-block:: javascript
|
|
|
|
closeEventSource();
|
|
|
|
Closes the current active :code:`EventSource`. This will be called automatically if the widget closes and you don't
|
|
have the :code:`onWidgetClose` function overridden. If you do, make sure to call :code:`super.onWidgetClose()`.
|
|
|
|
|
|
---------------
|
|
BaseTableWidget
|
|
---------------
|
|
|
|
The BaseTableWidget exposes a set of functions to easily create a responsive table that is capable of some basic
|
|
CRUD functionality. To make use of this, simply extend from the BaseTableWidget, which automatically exposes the
|
|
BaseWidget functions as well. E.g.:
|
|
|
|
.. code-block:: javascript
|
|
|
|
import BaseTableWidget from "./BaseTableWidget.js";
|
|
|
|
export default class YourWidget extends BaseTableWidget {}
|
|
|
|
*createTable*
|
|
=====================================================================================================================
|
|
|
|
.. code-block:: javascript
|
|
|
|
super.createTable(id, options);
|
|
|
|
Creates and returns a jQuery object with the id attribute set to the id parameter of this function. The :code:`options`
|
|
parameters is an object with the following structure:
|
|
|
|
.. code-block:: javascript
|
|
|
|
let options = {
|
|
headerPosition: 'top'|'left'|'none',
|
|
}
|
|
|
|
If the :code:`headerPosition` is :code:`top`, some extra options are defined:
|
|
|
|
.. code-block:: javascript
|
|
|
|
let options = {
|
|
headerPosition: 'top',
|
|
rotation: <number>,
|
|
headers: [],
|
|
sortIndex: <number>,
|
|
sortOrder: 'asc'|'desc'
|
|
}
|
|
|
|
- :code:`rotation` will limit the amount of table entries to this value, and 'scroll' new data into view.
|
|
- :code:`headers` defines a static array of strings that contain the table headers. The position in the array also implicitly
|
|
defines the index of the column.
|
|
- :code:`sortIndex` specifies the index of the headers array to sort on
|
|
- :code:`sortOrder` if the sortIndex is specified, the sort order will be either ascending or descending.
|
|
|
|
:code:`headerPosition` :code:`left` is a key-value structure while :code:`headerPosition` :code:`none` allows for
|
|
arbitrary rows of data without state.
|
|
|
|
*updateTable*
|
|
=====================================================================================================================
|
|
|
|
.. code-block:: javascript
|
|
|
|
super.updateTable(id, data = [], rowIdentifier = null);
|
|
|
|
Inserts one or more rows into the table with id parameter :code:`id`. If a rowIdentifier is specified, only a single
|
|
row of the table is upserted.
|
|
|
|
The data layout is as follows for :code:`headerPosition` :code:`top` and :code:`none`:
|
|
|
|
.. code-block:: javascript
|
|
|
|
|
|
[
|
|
['x', 'y', 'z'],
|
|
['x', 'y', 'z']
|
|
]
|
|
|
|
The data layout for :code:`headerPosition` :code:`left` also allows nested columns:
|
|
|
|
.. code-block:: javascript
|
|
|
|
|
|
[
|
|
['x', 'x1'],
|
|
['y', 'y1'],
|
|
['z', ['z1', 'z2']]
|
|
]
|
|
|
|
---------------
|
|
BaseGaugeWidget
|
|
---------------
|
|
|
|
:code:`BaseGuageWidget` defines a simple responsive gauge chart. An example implementation can be found in the
|
|
`Memory Usage Widget <https://github.com/opnsense/core/blob/master/src/opnsense/www/js/widgets/Memory.js>`__
|
|
|
|
*createGaugeChart*
|
|
=====================================================================================================================
|
|
|
|
.. code-block:: javascript
|
|
|
|
super.createGaugeChart(options);
|
|
|
|
|
|
|
|
*updateChart*
|
|
=====================================================================================================================
|
|
|
|
.. code-block:: javascript
|
|
|
|
super.updateChart(data);
|
|
|
|
-------
|
|
Styling
|
|
-------
|
|
|
|
Any styling can be added to the `Dashboard CSS file <https://github.com/opnsense/core/blob/master/src/opnsense/www/css/dashboard.css>`__
|
|
or a themed version of this file.
|
|
|
|
Since a lot of the charts have programmatic approaches to colors, the special
|
|
|
|
.. code-block:: css
|
|
|
|
:root {
|
|
--chart-js-background-color: #f7e2d6;
|
|
--chart-js-border-color: #d94f00;
|
|
--chart-js-font-color: #d94f00;
|
|
}
|
|
|
|
CSS selector is defined so you can override these colors for custom themes.
|
|
|
|
.. _configurable_widget:
|
|
|
|
--------------------
|
|
Configurable widgets
|
|
--------------------
|
|
|
|
To make widgets configurable, make sure you pass in the :code:`config` object in the constructor and, call :code:`super(config)` as
|
|
described in the :ref:`Functions - constructor <functions>` section. Also set :code:`this.configurable = true` in the constructor as well.
|
|
|
|
The `CPU graph widget <https://github.com/opnsense/core/blob/master/src/opnsense/www/js/widgets/Cpu.js>`__ is an example of a configurable widget.
|
|
|
|
Two functions must be overridden to make a widget configurable:
|
|
|
|
*getWidgetOptions*
|
|
=====================================================================================================================
|
|
|
|
.. code-block:: javascript
|
|
|
|
async getWidgetOptions();
|
|
|
|
Function must return an object with the following structure:
|
|
|
|
.. code-block:: javascript
|
|
|
|
{
|
|
option_name: {
|
|
title: <translated string>,
|
|
type: 'select_multiple',
|
|
options: [
|
|
{
|
|
value: 'value',
|
|
label: <translated string>
|
|
},
|
|
{
|
|
value: 'value',
|
|
label: <translated string>
|
|
}
|
|
],
|
|
default: ['value', 'value']
|
|
}
|
|
}
|
|
|
|
This function is asynchronous as it may require an API call to fetch the available options.
|
|
|
|
*onWidgetOptionsChanged*
|
|
=====================================================================================================================
|
|
|
|
.. code-block:: javascript
|
|
|
|
async onWidgetOptionsChanged(options);
|
|
|
|
Callback function that is called when the user changes the options of the widget. The options parameter is an object
|
|
with the same structure as the object returned by :code:`getWidgetOptions()`, but mapped to selected values.
|
|
|
|
*getWidgetConfig*
|
|
=====================================================================================================================
|
|
|
|
.. code-block:: javascript
|
|
|
|
async getWidgetConfig();
|
|
|
|
|
|
Gets the current persisted widget configuration. This function implicitly calls the :code:`getWidgetOptions()` function
|
|
to account for the option defaults.
|
|
|
|
--------------------
|
|
ACL and translations
|
|
--------------------
|
|
|
|
Every widget must expose the endpoints it's using to the framework, so the controller can determine whether
|
|
this widget is accessible for the current logged in user. To do this, you must create a section in the
|
|
:code:`src/opnsense/www/js/widgets/Metadata/<Core|Plugin-specific>.xml` file.
|
|
The `Core XML file <https://github.com/opnsense/core/blob/master/src/opnsense/www/js/widgets/Metadata/Core.xml>`__ shows
|
|
how widget metadata is structured.
|
|
|
|
If any of the defined endpoints is inaccesible, the widget will not be available for the user. Note that the same rules
|
|
as for any other `ACL <../../development/examples/helloworld.html#plugin-to-access-control-acl>`__ applies here.
|
|
|
|
Translations are provided in the same XML file, you can access these values by using the :code:`this.translations.<key>` variables
|
|
in the widget class, The value of `key` is defined by the opening/closing XML tags.
|