diff --git a/source/development/frontend.rst b/source/development/frontend.rst index a75dfd80..3d4b7a2b 100644 --- a/source/development/frontend.rst +++ b/source/development/frontend.rst @@ -14,3 +14,4 @@ The OPNsense frontend is implemented with `PHP/Phalcon `__ to create the dashboard grid. + +Widgets are placed in the :code:`src/opnsense/www/js/widgets/` directory. + +A complete example of a simple widget can be found `here `__ + +------- +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 `__ 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:`.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 `__ 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 this is only the +`BaseTableWidget `__ +which exposes a dynamic table that can be configured in multiple orientations and only needs a data feed. + +The following functions are available to be overridden by the widget: + +*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 `__. + +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 `__. + +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. + +------- +Styling +------- + +Any styling can be added to the `Dashboard CSS file `__