mirror of
https://github.com/opnsense/docs.git
synced 2026-05-28 04:02:12 -04:00
Dashboard: merge pending changes (#583)
* development: add dashboard framework documentation * dashboard: duplicate link * dashboard: update development documentation --------- Co-authored-by: Stephan de Wit <stephan.de.wit@deciso.com>
This commit is contained in:
parent
206430d50c
commit
5e92a50d40
2 changed files with 368 additions and 0 deletions
|
|
@ -14,3 +14,4 @@ The OPNsense frontend is implemented with `PHP/Phalcon <https://en.wikipedia.org
|
|||
frontend/routing
|
||||
frontend/controller
|
||||
frontend/view_js_helpers
|
||||
frontend/dashboard
|
||||
|
|
|
|||
367
source/development/frontend/dashboard.rst
Normal file
367
source/development/frontend/dashboard.rst
Normal file
|
|
@ -0,0 +1,367 @@
|
|||
=================
|
||||
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.
|
||||
|
||||
---
|
||||
ACL
|
||||
---
|
||||
|
||||
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, any :code:`<widget>.js` file must start
|
||||
with the following line(s):
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
// endpoint:/api/endpoint/used/by/widget
|
||||
|
||||
For example:
|
||||
|
||||
.. code-block:: javascript
|
||||
|
||||
// endpoint:/api/interfaces/overview/*
|
||||
|
||||
Multiple lines can be used if the widget uses multiple endpoints. If any of these endpoints are inaccessible,
|
||||
the widget will not be loaded. Note that the same rules as for any other
|
||||
`ACL <../../development/examples/helloworld.html#plugin-to-access-control-acl>`__ applies here.
|
||||
|
||||
---------
|
||||
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 ms) in which the :code:`onWidgetTick()` function is called. The default is 5000
|
||||
|
||||
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 found in the :code:`this.config` property, if any.
|
||||
|
||||
*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()`
|
||||
|
||||
*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 ajaxGet('/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` milliseconds. 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.
|
||||
|
||||
*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.
|
||||
Loading…
Reference in a new issue