Extending Canvas with Plugins

Much like Kibana, Canvas can be extended through the use of plugins. And also much like Kibana, Canvas uses the plugin system to provide its base functionality, so Canvas itself makes a good reference for extending it. Adding custom functions or elements is as simple as writing a plugin and installing it.

We have a boilerplate repo to help you get started, and some existing plugins to reference. As a guide to writing your own plugin, let's take a look at one of the existing plugins, the Canvas github demo. You can follow along by grabbing a copy of the boilerplate, and change the name of the path to my-plugin.

Before we get started, it's important to understand that Canvas is driven by an expression language, which uses function execution to produce its output. For the most part, plugins use this function system to provide additional functionality. If this is news to you, take a look at the videos section on this site for some quick information to get you up to speed.

The github demo provides a few things:

First, let's change the name of our plugin. In the boilerpate, open the package.json file and change the value for the name. Do the same thing in the root index.js file as well. Call it whatever you want (my-plugin would be a fine name), but make both of the values the same.

Next, we need to write our github-tags function. Since Github's API doesn't have any CORS restrictions, this function can run either on the server or in the browser, so we'll create it as a common function, which will allow it to run in either place.

We're going to use a library called axios to do web requests, so install it first by running npm install --save axios from the root of the project. Then, create the file common/functions/github_tags.js, and put the following in there:

import axios from 'axios';

const getTagSha = (tag, headers) => {
  return axios
    .get(tag.commit.url, { headers })
    .then(res => res.data)
    .then(commit => ({
      name: tag.name,
      sha: commit.sha,
      committer: commit.commit.committer.name,
      date: commit.commit.committer.date,
    }));
};

export const githubTags = () => ({
  name: 'github-tags',
  type: 'datatable',
  help: 'Talk directly to the Github API and get back tabular data.',
  args: {
    repo: {
      types: ['string'],
      default: 'elastic/kibana',
    },
    limit: {
      types: ['number', null],
      default: 10,
    },
    token: {
      types: ['string', null],
    },
  },
  fn(context, args) {
    const { repo, token, limit: tagLimit } = args;
    const tagsUrl = `https://api.github.com/repos/${repo}/tags`;
    const headers = token ? { Authorization: `token ${token}` } : {};

    return axios
      .get(tagsUrl, { headers })
      .then(res => res.data)
      .then(tags => Promise.all(tags.slice(0, tagLimit).map(tag => getTagSha(tag, headers))))
      .then(rows => ({
        type: 'datatable',
        columns: Object.keys(rows[0]).map(col => ({ name: col, type: 'unknown' })),
        rows,
      }))
      .catch(err => ({
        type: 'datatable',
        columns: ['error'],
        rows: [{ error: err.message }],
      }));
  },
});

Functions are defined as javascript functions that return a plain object containing properties that define what they are called in the expression, what arguments they take, and what happens when they are called. The getTagSha function at the top is just a helper that will pull data from Github, the important part here is the githubTags object that is being exported. Here's a rundown of the properties on that object:

Looking at this function definition, we see that it will take repo, limit, and token arguments, and return a datatable. Seems simple enough, right? Now to register it with the plugin, open common/functions/index.js import the function, and add it to the commonFunctions array.

import { canary } from './canary';
import { githubTags } from './github_tags';

export const commonFunctions = [canary, githubTags];

Now if you start Canvas with this plugin, you'll have access to the github-tags function. The easiest way to do this is to run Canvas in development mode and make use of the kibana-plugin-helpers it uses. Simply put the kibana-canvas path and your plugin path at the same level as your kibana path.

.
├── kibana
├── kibana-canvas
└── my-plugin

You'll also need to have X-Pack installed on both Elasticsearch and Kibana. Use the elasticsearch-plugin and kibana-plugin commands, respectively, to install it. Canvas won't start without X-Pack, so you won't be able to test your plugin without it.

Next, navigate to the kibana-canvas path, and create a new file named .kibana-plugin-helpers.dev.json, and inside that file, put the following:

{
  "includePlugins": ["../my-plugin"]
}

Now, from the kibana-canvas path, run npm start, and it should load Kibana with both Canvas and your plugin installed. Add an element and change the expression to github-tags and you should see output like this:

github-tags datatable

That's it, now you've got a custom function reading tag data off github repos. In the next post, we'll create the timeline element and use it to render the tag dates.