First: Disclaimers

  • This documentation requires a decent understanding of JavaScript and the CloudStack API. You don't need knowledge of specific frameworks for this tutorial (jQuery, etc.), since the CloudStack UI handles the front-end rendering for you.
  • There is much more to the CloudStack UI framework than can be placed here, and a dedicated API reference still needs to be written on how all the various widgets work. The UI is very flexible to handle many use cases, so there are countless options and variations that can be made! The best reference right now is to read the pre-existing code in the /ui folder, as plugins are written in a very similar way to the 'main' UI.
  • Explanations of the important parts of the code are done as in-line comments in the examples.

Setting up

Create basic folder structure

All plugins follow a common set of files, so that the UI framework can properly interpret the plugin.

Plugins reside in the /ui/plugins/(pluginID), where (pluginID) is a short name for your plugin. It's recommended that you prefix your folder name (i.e., bfMyPlugin) to avoid naming conflicts from other developers' plugins.

For this example, we will call the plugin csMyFirstPlugin

$ cd cloudstack/ui/plugins
$ mkdir csMyFirstPlugin
$ ls -l

total 8
drwxr-xr-x  2 bfederle  staff   68 Feb 11 14:44 csMyFirstPlugin
-rw-r--r--  1 bfederle  staff  101 Feb 11 14:26 plugins.js
Add required files under plugin folder

All UI plugins have the following set of files:

+-- ui/
  +-- plugins/
    +-- csMyFirstPlugin/
      +-- config.js            --> Plugin metadata (title, author, vendor URL, etc.)
      +-- icon.png             --> Icon, shown on side nav bar and plugin listing (should be square, and ~50x50px)
      +-- csMyFirstPlugin.css  --> CSS file, loaded automatically when plugin loads
      +-- csMyFirstPlugin.js   --> Main JS file, containing plugin code
Configure plugin

Add the following content to config.js. This information will be displayed on the plugin listing page in the UI:

config.js
(function (cloudStack) {
  cloudStack.plugins.csMyFirstPlugin.config = {
    title: 'My first plugin',
    desc: 'Tutorial plugin',
    externalLink: 'http://www.cloudstack.org/',
    authorName: 'Test Plugin Developer',
    authorEmail: 'plugin.developer@example.com'
  };
}(cloudStack));

Adding a new main section

Create a new file, csMyFirstPlugin.js with the following content:

csMyFirstPlugin.js
(function (cloudStack) {
  cloudStack.plugins.csMyFirstPlugin = function(plugin) {
    plugin.ui.addSection({
      id: 'csMyFirstPlugin',
      title: 'My Plugin',
      preFilter: function(args) {
        return isAdmin();
      },
      show: function() {
        return $('<div>').html('Content will go here');
      }
    });
  };
}(cloudStack));
Register plugin in CloudStack UI

We now have the minimal content needed to run the plugin, so we can 'activate' the plugin in the UI by adding it to plugins.js:

plugins.js
(function($, cloudStack) {
  cloudStack.plugins = [
    'csMyFirstPlugin'
  ];
}(jQuery, cloudStack));

Reload the UI – rerun the build UI portion of the mvn build, if not in the active Tomcat/jetty folder, and then refresh the browser. You should see your new plugin listed in the 'Plugins' section. Click on its tile to see it work!

Creating a list view

Right now, we just have placeholder content in our new plugin. Let's write a basic list view, which renders data from an API call. For this example, we'll list all virtual machines owned by the logged-in user.

To do this, we have to replace the 'show' function in our plugin code with a 'listView' block, containing the required syntax for a list view:

csMyFirstPlugin.js, with minimal list view
(function (cloudStack) {
  cloudStack.plugins.csMyFirstPlugin = function(plugin) {
    plugin.ui.addSection({
      id: 'csMyFirstPlugin',
      title: 'My Plugin',
      preFilter: function(args) {
        return isAdmin();
      },

      // Render page as a list view
      listView: {
        id: 'testPluginInstances',
        fields: {
          name: { label: 'label.name' },
          instancename: { label: 'label.internal.name' },
          displayname: { label: 'label.display.name' },
          zonename: { label: 'label.zone.name' }
        },
        dataProvider: function(args) {
          args.response.success({ data: [] });
        }
      }
    });
  };
}(cloudStack));

After re-loading the UI code, you can see that our placeholder content was replaced with a list table, containing 4 columns and no data:

Let's write an API call to get our virtual machines.

To do this, we'll use the listVirtualMachines API call. Without any parameters, it will return VMs only for our active user. We will use the provided 'apiCall' helper method to handle the server call. Of course, you are free to use any other method for making the AJAX call (i.e., jQuery's $.ajax method)

csMyFirstPlugin.js, with VMs listed (code snipped)
...
      // Render page as a list view
      listView: {
        id: 'testPluginInstances',

        ...
        dataProvider: function(args) {
          // API calls go here, to retrive the data asynchronously
          //
          // -- on successful retrieval, call args.response.success({ data: [data array] });
          plugin.ui.apiCall('listVirtualMachines', {
            success: function(json) {
              var vms = json.listvirtualmachinesresponse.virtualmachine;

              args.response.success({ data: vms });
            },
            error: function(errorMessage) {
              args.respons.error(errorMessage)
            }
          });
        }
      }
...

Adding an action

Let's add an action button to the list view, which will reboot the VM. To do this, we have to add an actions block under listView. After specifying the correct format, the actions will appear automatically to the right of each row of data.

csMyFirstPlugin.js, with action (code snipped)
...
      listView: {
        id: 'testPluginInstances',
        ...

        actions: {
          // The key/ID you specify here will determine what icon is shown in the UI for this action,
          // and will be added as a CSS class to the action's element
          // (i.e., '.action.restart')
          //
          // -- here, 'restart' is a predefined name in CloudStack that will automatically show
          //    a 'reboot' arrow as an icon; this can be changed in csMyFirstPlugin.css
          restart: {
            label: 'Restart VM',
            messages: {
              confirm: function() { return 'Are you sure you want to restart this VM?' },
              notification: function() { return 'Rebooted VM' }
            },
            action: function(args) {
              // Get the instance object of the selected row from context
              //
              // -- all currently loaded state is stored in 'context' as objects,
              //    such as the selected list view row, the selected section, and active user
              //
              // -- for list view actions, the object's key will be the same as listView.id, specified above;
              //    always make sure you specify an 'id' for the listView, or else it will be 'undefined!'
              var instance = args.context.testPluginInstances[0];

              plugin.ui.apiCall('rebootVirtualMachine', {
                // These will be appended to the API request
                //
                // i.e., rebootVirtualMachine&id=...
                data: {
                  id: instance.id
                },
                success: function(json) {
                  args.response.success({
                    // This is an async job, so success here only indicates that the job was initiated.
                    //
                    // To pass the job ID to the notification UI (for checking to see when action is completed),
                    // '_custom: { jobID: ... }' needs to always be passed on success, in the same format as below
                    _custom: { jobId: json.rebootvirtualmachineresponse.jobid }
                  });
                },
                error: function(errorMessage) {
                  args.response.error(errorMessage); // Cancel action, show error message returned
                }
              });
            },

            // Because rebootVirtualMachine is an async job, we need to add a poll function, which will
            // perodically check the management server to see if the job is ready (via pollAsyncJobResult API call)
            //
            // The plugin API provides a helper function, 'plugin.ui.pollAsyncJob' which will work for most jobs
            // in CloudStack
            notification: {
              poll: plugin.ui.pollAsyncJob
            }
          }
        },

        dataProvider: function(args) {
          ...
...

 

Extending an existing section with a new action

NOTE: Only works on latest master code.

 

You can also extend the existing UI via the plugin framework, by extending the 'cloudStack' object tree. Any modifications you make to it will be pulled in when the UI loads.

For example, you can add an action to an existing section's detail view or list view. The example below will create a new action under the 'events' detail page:

csMyFirstPlugin.js, adding action to existing UI
 cloudStack.plugins.csMyFirstPlugin = function(plugin) {
      // Add an action to an existing section
      // First, find the correct path to the respective section you want to extend
      // -- standard practice in the UI is to put all actions under the detail view
      var section = cloudStack.sections.events;
      var actions = section.sections.events.listView.detailView.actions;
      // Then, define a new action, using standard widget syntax
      // (see other actions in CloudStack code as examples)
      actions.pluginAction = {
          label: 'Test plugin action', // Label that will appear as alt tag/quickview label
          messages: {
              confirm: function(args) {
                return 'Please confirm you want this test action performed' // This appears before the action runs
              },
              notification: function(args) {
                  return 'Test plugin action' // This will appear in the notifications box when action is performed
              }
          },
          // Determines whether action will be shown/hidden;
          // this is evaluated on load/refresh of detail view
          //
          // return true to show action, false to hide
          preFilter: function(args) {
              var context = args.context; // Stores any existing JSON data loaded into the UI
 
              return isAdmin(); // isAdmin() causes action to only be shown to root admins
          },
          action: function(args) {
              // ... ajax calls, etc here
              //
              // -- call args.response.success() when action is completed,
              //    or args.response.error(errorMsg) if there was an error, and pass in errorMsg to display
              args.response.success();
          }
      }
  };

 

To add a new action icon, just edit csMyFirstPlugin.css and add/override any required styling, using the CSS tools in Firebug/Web Inspector as a reference.

For example, to add an icon for the new action we added to the events detail view:

Adding an action icon
/* Put your CSS here */
.detail-group table td.detail-actions .pluginAction a span.icon {
    background: url('action-icon.png') no-repeat 0 0;
}
  • No labels