Plugin tutorial: Dashboard widget

  Visit at GitHub

In this tutorial we will create a new widget for the Serverboards Dashboard.

This simple plugin will show the state of all services in the current Serverboard, with some extra goodies, as hover to see the state description, and click to go to the service.

Source code can be obtained at: https://github.com/serverboards/serverboards-plugin-serviceheatmap

Set up the project

To set up the project template there is a small utility at the Serverboards source code, the serverboards-template.py

Running it will give an output as this one:

$ ./serverboards-template.py rollup ~/src/serverboards-plugins/service-heatmap
Please answer how to fill these placeholders:
 author-email> dmoreno@serverboards.io
 author-name> David Moreno
 component-type> widget
 description> Show status of all services in this Serverboard in one heatmap.
 plugin-id> serverboards.plugin.serviceheatmap
 plugin-name> Service Heatmap
 url> https://serverboards.io
Postinst OK
Ready at /home/dmoreno/src/serverboards-plugins/service-heatmap
$

Here we are using the rollup template, and install it at my current plugin working directory.

Serverboards can have several plugin directories; they can be set at serverboards.ini, and normally, if unconfigured, can be found at ~/.local/serverboards/plugins/.

The rollup template is currently prepared to be used on both screens and widgets. As for our plugin we just want a widget, I will remove the parts that talk about screens.

Its a good momento to review what constitutes a plugin.

A plugin is

In serverboards the only minimum required element for plugins is the manifest.yaml plugin description file. Each plugin has some data about creator, name, id… and one or several components. Each component has a type, as a widget, screen, cmd, and so on. Check the documentation for the full list.

In our case we will only do a widget, so there is only one component.

At runtime Serverboards will get this manifest.yaml and as it has a widget type will show it to the user to be used in the Dashboard. When it is used Serverboards loads the files at static/COMPONENTID.js and static/COMPONENTID.html. If one of them does not exist its ok; for example we will not have an html file.

Had it have an html file it will rewritten to use the proper paths for its own data (images, js…) and be inserted into the plugin slot.

A JS Widget

For JS its a bit more complicated. There is a global object Serverboards that allow access to Serverboard functionalities, and to many components.

The idea is that as Serverboard is quite complex and needs libraries as React, jQuery, momentjs, and had developed many many utility functions, as rpc access, React components, its a good idea to share this code. For parts as the rpc module this is required to be able to comunicate with the backend.

The most important function for widgets is the Serverboards.add_widget function, that allows to register this plugin into the plugin registry, to be properly used later. Due to the asynchronous nature of JavaScript on browsers, this is required to be this way.

const plugin_id="serverboards.plugin.serviceheatmap"
function main(el, config){  ... }
Serverboards.add_widget(`${plugin_id}/widget`, main)

main receives the DOM element on which the widget has to mount, and the config data as stated in the component in the manifest.yaml, and set up by the user at the widget settings dialog.

Back to our plugin

As we are using the rollup template we can use React, ES2016, and JSX.

The default view is jsut a JSONinzed version of the config. Lets just change it to something more interesting… a list of our services statuses.

Via the Serverboards.store method we have access to the redux store as used by Serverboards. There are several ways to explore the status of the store (I love the redux plugin for chrome), but we will do it using the inspector (pressing F12 on most browsers). There we can just ejecute this javascript line:

Serverboards.store.getStatus()

This returns an object and usign the inspector we can easily navigate through it. Of special interest for this plugin is the serverboards.services element.

This is a list of all the services in the current Serverboard. To get the status we can use a list mapping:

  Serverboards.store.getState()
    .serverboard.serverboard.services
    .map( (s) => s.tags.join(', ') )

Actually we will not use this as we more data than jsut the status, for example to which service it belongs, but for that we need to start coding our React components.

Using React is not required to create widgets nor screens on Serverboards; any other technique can be used: plain javascript, jquery manipulation, Angular… you name it. But as we develop the frontend using React, and we are fond of it, we recomment its use.

Our React components

We will create a container component, that stores the state of our plugin and a view component, that shows this data to the user.

The container component will be fairly easy: jsut the list of services on our current serverboard. As we saw this is just a property on our store, so we will use it as is. Our first version will not deal with state changes, but we will get there later.

Out initial implementation for the container model will be something like this:

const {store} = Serverboards
const Model = React.createClass({
  getInitialState(){
    return {
      services:
        store.getState().serverboard.serverboard.services
    }
  },
  render(){
    return <View services={this.state.services}/>
  }
})

For the view component will be:

function View(props){
  return (
    <div className="ui heatmap">
      {props.services.map( (s) => (
        <span
          className={`cell ${Serverboards.utils.colorize(s.tags[0] || "")}`}
          title={`${s.name}: ${s.tags.join(', ')}`}
          data-tooltip={`${s.name}: ${s.tags.join(', ')}`}
          onClick={() => Serverboards.store.goto(`/services/${s.uuid}`)}
          />
      ) ) }
    </div>
  )
}

With that this we have a working plugin!

Async changes

But we need something more, we need to know when the state changes, and change acordingly. This encompases several dificulties: first we need to be told when the services have changed, and then, when we stop needing this advices we need to unsubscribe from this.

For subscribing we will use the redux subscribe method, that will just ping us on every change on the store. This may look overkill, but in practive there is no problem to recalculate our state (as its just a pointer), and then React will figure out it the DOM has to be changed.

Finally to unsubscribe we just unsubscribe from the redux store.

So we will change out model slightly to be like this.

const {store} = Serverboards
const Model = React.createClass({
  getInitialState(){
    return {
      services:
        store.getState().serverboard.serverboard.services
    }
  },
  updateServices(){
    this.setState( {
      services:
        store.getState().serverboard.serverboard.services
    } )
  },
  componentDidMount(){
    this.unsubscribe = store.subscribe(
      () => this.updateServices()
    )
  },
  componentWillUnmount(){
    this.unsubscribe()
  },
  render(){
    return <View services={this.state.services}/>
  }
})