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
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
Add the following content to config.js. This information will be displayed on the plugin listing page in the UI:
(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));
Create a new file, csMyFirstPlugin.js with the following content:
(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));
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:
(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!
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:
(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)
... // 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) } }); } } ...
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.
... 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) { ... ...
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:
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(); } } };