Getting started: Write a plugin - Infomaker/Dashboard-Plugin GitHub Wiki

Getting started

This guide will teach you how to build a plugin with an application, agent and widget. Also how to communicate between them and fetching external data.

Before you get started you should read "The anatomy of a plugin" and have understanding of a plugins life cycle.

Introduction

Our scenario is to build a plugin that fetches data from Krisinformation (SE) and displays it in a list with a details view in a modal, we also want to refresh the data on a user selected interval. To let the user know when the latest update was we put that info in a widget. The data should be cached and persisted between users.

Prerequisites

  • Git
  • NodeJS

We also need to get the Dashboard Developer Plugin. Follow the install instructions in the README.md

Building the plugin

Step 1: Configuring the plugin

The information about the plugin is in the manifest.json file in the root of the plugin. I will call the plugin Krisinformation, we will also give is a bundle name in reversed domain style.

{
  "bundle": "se.infomaker.Krisinformation",
  "name": "Krisinformation",
  "description" : "Krisinformation plugin",
  "author": {
    "name": "Team Dashboard",
    "organization": "Infomaker Scandinavia AB",
    "email": "[email protected]",
    "website": "www.infomaker.se"
  },
  "version": "1.0.0"
}

Step 2: Install the plugin

As the Developer Plugin contains a basic example its now possible to install the plugin. We want to follow the process in the Dashboard so we will start install it before we start the implementation.

After cloning the Dashboard-Plugin we will install all the dependencies and build it either with npm || yarn see Dashboard-Plugin Doc. Open your Dashboard and go to the Store, click the "Add" tab and enter the URL to your local environment (get the URL from the terminal) and fetch the plugin. Click the "install"-button.

The plugin will now show up in the list of installed plugins but as "inactive". Click on the plugin and then the "activate"-button.

Step 3: The Settings

As we want the user to be able to choose the refresh interval we will create a plugin settings method in the settings object. Open your editor and MySettings.jsx located in /src/js/plugin/components/. We will now work with the Settings class.

To enable the plugin to take settings we will add a plugin() method. Using the pre-built settings objects in the GUI-library we can take the refresh interval as an ConfigInput and validate it as required and numerical, we will also give it a reference so we can access it from the config object later on. Settings class now look like this:

import { Settings, GUI } from 'Dashboard'

export default class MySettings extends Settings {
    plugin() {
        return (
            <GUI.ConfigInput
                name="Refresh rate"
                ref={ref => this.handleRefs(ref, 'refreshRate')}
                validation={['required', 'numerical']}
            />
        )
    }
}

If you now go back to the Store and open the plugin, a new button is added called "settings". This now open a dialogue with the Refresh rate input. Thats all you need to take plugin settings from the user, everything else is magic.

Step 4: The Agent

We will now switch to our Agent class, this will be our data layer and model for our plugin. We're going to remove the connect method and only save the constructor for now.

In step 3 we created a settings input, we can now use this within the agent, this will be passed to the constructor through a props object, we will call it props, resulting in:

import { Agent, moment } from 'Dashboard'

export default class MyAgent extends Agent {

    constructor(props) {
        super(props)
        
        const { config } = props 
    }
}

To fetch external data there is a Request module available on this.request. For Krisinformations API there is no need to send headers or post data. When the data is fetched our callback will be invoked. We will add a new method to the agent class and call it update.

update() {
	this.request('http://api.krisinformation.se/v1/feed', data => {
		// Handle the data
	})
}

When we get the data from the API we want to store it and dispatch it to the application, the agent will also send a timestamp to the widget so it can display when the lastest fetch was.

The Storage module is available on this.store, it a key/value storage so we will just use the key krisinformation and send in the data, this.store('krisinformation', data). The agent also have to notify the application that fresh data has been fetched via the Dispatch module available on this.send and this.on. This is also a key/value but we want to be more specific of what type the dispatch is, as this is an update we will use krisinformation:update as key and send the data, this.send('krisinformation:update', data). Before sending it we will add a timestamp to the data with moment.js that is available on Dashboard object (see import above), data.timestamp = moment().

For now we will fetch new data in the time interval the user sets, in a production plugin you should be more careful when and how often requesting an API. We can use the setInterval method, Dashboard makes sure that it gets cleaned up if the agent is inactivated or uninstalled.

Back to the constructor, the refreshRate is now available on the config object if the user has set one, else it will be undefined so a good practice is always using default values, in this case a 5 minute (300000 ms) interval will be good

this.setInterval(() => this.update(), config.refreshRate * 1000 || 300000)

The data should also be fetched when the agent is installed or a new dashboard instance is loaded so we'll also need to fire a this.update() in the constructor.

When the application and widget is loaded in the dashboard they could either read directly from store to retrieve the data or use the Dispatch module, in this case we will use the Dispatch so only the agent reads from the store. The dispatch will listen to the key krisinformation:getdata who will retrieve the data from the store, check the passed in userData for a callback function, and call it with the retrieved data.

this.on('krisinformation:getdata', (userData) => {
    this.store('krisinformation', response => {
        if (userData.callback) {
            userData.callback(response.data)
        }
    })
})

Step 5: The Widget

The widget in this case it not really useful, but we want it anyways. This time we'll use a more advance way to work with the event system. We'll start out by using this.ready() with the "bundle name"-agent as argument. This will wait for the agent to be ready before sending an event that askes for the current stored information. To the send function we'll add an object as argument with name to be the event we're asking for and userData to add custom data to the event request. Here we'll add a callback function that could be called when the agent has retrieved the stored information. In the render function we will return the time and a nice little 📢.

import { Widget, moment } from 'Dashboard'

export default class MyWidget extends Widget {

    constructor(props) {
        super(props)

        this.state = {
            updated: 'Unknown'
        }

        this.ready('se.infomaker.Krisinformation-agent', () => {
            this.send({
                name: 'krisinformation:getdata',
                userData: {
                    callback: (data) => {
                        this.setState({
                            'updated': moment(data.timestamp).format('HH:mm')
                        })
                    }
                }
            })
        })
    }

    render() {
        return (
            <div>
                📢  {this.state.updated}
            </div>
        )
    }
}

Step 6: The Application

For our Application class the example loads in config as default and sets them to the default state, this plugin does not have any applications settings so the config key can be replaced with a items key and the value to an empty array.

this.state = {
	items: []
}

After that the Application needs to listen to the krisinformation:update event and set the items found in the API to the state. According to the API documentation the items is an array on the key Entries in the JSON format, the data does not need to be processed so lets just add them to the state.

this.on('krisinformation:update', userData => {
    this.setState({
        'items': userData.Entries
    })
})

To let the Agent know that a application instance has been setup lets add a new React default method called componentDidMount() and send a krisinformation:getdata event. This is useful because the application could be on multiple workspaces and be loaded and added at a different time than the agent.

componentDidMount() {
    this.send({
        name: 'krisinformation:getdata',
        userData: {
            callback: (data) => {
                this.setState({
                    items: data.Entries
                })
            }
        }
    })
}

Now we have all the data in place and will use a List and ListItem from the GUI Library to display it. This will take place in the render method. We will save the constant GUI from the example to save some typing later on.

As the items are on the state in this.state.items we can map that into an new array with the specs from the GUI.List component. The content will be time time published with a moment.js format. When a user clicks the item in the list the Modal should open with the details from the event, in the modal callback its possible to send a event that the modal can receive so lets send the item directly with the key krisinformation:details.

import { Application, Modal, GUI, moment, createUUID } from 'Dashboard'

let listitems = this.state.items.map(item => {
    return {
        id: createUUID(),
        title: item.Title,
        content: moment(item.Published).format('dddd Do MMMM [@] HH:mm:ss'),
        onClick: () => {
            this.openModal(MyModal, () => {
                this.send('krisinformation:details', item)
            })
        }
    }
})

In the render function of the application its now possible to use <GUI.List items={listitems} />, lets update it a little bit and the result is:

return (
    <GUI.Wrapper className="se-infomaker-krisinformation">
        <GUI.Title text="Krisinformation"/>

        <GUI.List items={listitems} />
    </GUI.Wrapper>
)

Voila! The plugin now got an agent, widget and a application that lists the items from the API.

Step 7: The Modal

As we did with the widget, the modal will also get a constructor with a default state, but with an item value instead. The componentDidMount() method will implement a dispatch listener on the key krisinformation:details as sent from the click event in the application. The render() method will be called before we get the event so we need to make sure that the state has an item, check if an item exist or return a message.

if(!this.state.item) return <GUI.Wrapper>Waiting for data</GUI.Wrapper>

Then we will just use GUI components and display the items data from the API and return it.

return (
	<GUI.Wrapper>
		<GUI.Section title={this.state.item.Title}>
			{this.state.item.Summary}
		</GUI.Section>

		<GUI.Section title="Date">
			Published: {this.state.item.Published}<br />
			Updated: {this.state.item.Updated}
		</GUI.Section>
	</GUI.Wrapper>
)

Summary

We got an plugin talking to an API, list the items and a details view with only a couple lines of code

Exercises

  • Some items from this API contains a GEO coordinates, render them on a map in the details view.
  • Rethink the agent. The agent could check if the store contains any data and how old it is before fetching new data. Its also not good if multiple users use the same app, could the data be shared somehow?
  • Build another plugin on the agent. With the dispatcher different plugins can communicate, make an plugin with only an app that uses the data from the agent.

Complete code

index.jsx

import { register } from 'Dashboard'
import MyAgent from './components/MyAgent'
import MyWidget from './components/MyWidget'
import MySettings from './components/MySettings'
import MyApplication from './components/MyApplication'

(() => {
	/**
	 * Register your plugin in the Dashboard.
	*/

	register({
		bundle: "@plugin_bundle",
		
		agent: MyAgent,
		widget: MyWidget,
		application: MyApplication,

		// Settings is optional.
		settings: MySettings
	})
})()

Agent

import { Agent, moment } from 'Dashboard'

export default class MyAgent extends Agent {
    constructor(props) {
        super(props)

        const { config } = props

        this.setInterval(() => this.update(), config.refreshRate * 1000 || 30000)
        this.update()

        this.on('krisinformation:getdata', (userData) => {
            this.store('krisinformation', data => {
                if (userData.callback) {
                    userData.callback(data)
                }
            })
        })
    }

    update() {
        this.request('http://api.krisinformation.se/v1/feed', data => {
            data.timestamp = moment()
            this.store('krisinformation', data)
            this.send('krisinformation:update', data)
        })
    }
}

Widget

import { Widget, moment } from 'Dashboard'

export default class MyWidget extends Widget {

    constructor(props) {
        super(props)

        this.state = {
            updated: 'Unknown'
        }

        this.ready('se.demo.krisinformation-agent', () => {
            this.send({
                name: 'krisinformation:getdata',
                userData: {
                    callback: (data) => {
                        this.setState({
                            'updated': moment(data.timestamp).format('HH:mm')
                        })
                    }
                }
            })
        })
    }

    render() {
        return (<div>📢  {this.state.updated}</div>)
    }
}

Application/Modal

import { Application, Modal, GUI, createUUID, moment } from 'Dashboard'

export default class MyApplication extends Application {
    constructor(props) {
        super(props)

        this.state = {
            items: []
        }

        this.on('krisinformation:update', userData => {
            this.setState({ 'items': userData.Entries })
        })
    }

    componentDidMount() {
        this.send({
            name: 'krisinformation:getdata',
            userData: {
                callback: (data) => {
                    this.setState({
                        items: data.Entries
                    })
                }
            }
        })
    }

    render() {
        let listitems = this.state.items.map(item => {
            return {
                id: createUUID(),
                title: item.Title,
                content: moment(item.Published).format('dddd Do MMMM [@] HH:mm:ss'),
                onClick: () => {
                    this.openModal(MyModal, () => {
                        this.send('krisinformation:details', item)
                    })
                }
            }
        })

        return (
            <GUI.Wrapper className="se-infomaker-krisinformation">
                <GUI.Title text="Krisinformation" />

                <GUI.List items={listitems} />
            </GUI.Wrapper>
        )
    }
}

/**
 * Create an Modal by extending the Modal class
 * Read more about Modal (https://github.com/Infomaker/Dashboard-Plugin/wiki/Modal)
*/
class MyModal extends Modal {
    constructor() {
        super()

        this.state = {
            item: null
        }
    }

    componentDidMount() {
        this.on('krisinformation:details', item => {
            this.setState({
                item: item
            })
        })
    }

    componentWillMount() {
        // Call setTitle to set the component most upper title.
        this.props.setTitle("My Modal")
    }

    render() {
        if (!this.state.item) return <GUI.Wrapper>Waiting for data</GUI.Wrapper>

        return (
            <GUI.Wrapper>
                <GUI.Section title={this.state.item.Title}>
                    {this.state.item.Summary}
                </GUI.Section>

                <GUI.Section title="Date">
                    Published: {this.state.item.Published}<br />
                    Updated: {this.state.item.Updated}
                </GUI.Section>
            </GUI.Wrapper>
        )
    }
}
⚠️ **GitHub.com Fallback** ⚠️