Creating Custom Widgets

Cloudify enables you to create your own widgets to help you to orchestrate your applications in the Cloud.

Widget Development Methods

Widgets can be written using two different methods:

  1. Using the React utility is the recommended method, and it requires a build operation. You must use the build system described in Widget building section.

  2. Plain JavaScript that enables attachment of an HTML template file. The callbacks for this method are described later in this topic. You must create widget package yourself. No ES6 is supported in that method.

Widget Structure

A widget is made up of these files:

If you want to use the React utility (recommended), you must put your widget.js file (and optionally backend.js file) into the src directory along with any other required files. In the widget.js file, you can use import to include the additional files. You can split the widget into a number of files. You can also use ES6 features.

Using this method, the file system will look as follows:

/widgets   
   /blueprint   
      /src   
         widget.js
         backend.js
      widget.js (generated by build system)
      backend.js (generated by build system)
      widget.html
      widget.png
      widget.css

If you want to use plain JavaScript and you want to create a widget named blueprint, the widget library looks like this:

/widgets   
   /blueprint   
      widget.js
      backend.js
      widget.html
      widget.png
      widget.css

Widget Definition

Each widget.js file must have a call to the Stage.defineWidget global function.

Example

The following code demonstrates how easy it is to create a simple widget. You can copy this code and put it in a widget.js file to produce a fully working widget. See the previous section for file structure guidelines.

Stage.defineWidget({
    id: 'sampleWidget',
    name: 'A basic example',
    description: 'This widget displays "Hello World!" text',
    initialWidth: 4,
    initialHeight: 2,
    showHeader: false,
    showBorder: false,
    initialConfiguration: [],
    isReact: true,
    categories: [Stage.GenericConfig.CATEGORY.OTHERS],
    permission: Stage.GenericConfig.CUSTOM_WIDGET_PERMISSIONS.CUSTOM_ALL,

    render: function(widget,data,error,toolbox) {
        return (
            <span>Hello World!</span>
        );
    }    
});

Widget Settings

As seen in the example above, there are a number of configuration fields that you can provide when you design a widget.

The Stage.defineWidget function receives a settings object with the options described in this table.

Option Type Required Default Description
categories array No OTHERS This property specifies in which categories this widget shall be visible. It may take an array containing one or more of the values defined in Stage.GenericConfig.CATEGORY object: BLUEPRINTS (‘Blueprints’ category), DEPLOYMENTS (‘Deployments’), BUTTONS_AND_FILTERS (‘Buttons and Filters’), CHARTS_AND_STATISTICS (‘Charts and Statistics’), EXECUTIONS_NODES (‘Executions/Nodes’), SYSTEM_RESOURCES (‘System Resources’), OTHERS (‘Others’), ALL (‘All’).
color string No blue The color of the widget. One of the following: red, orange, yellow, olive, green, teal, blue, violet, purple, pink, brown, grey or black.
description string No - Description of the widget that is displayed in the Add Widget dialog.
fetchUrl string/object No - If fetchUrl exists, the data from the URL is fetched by the application and passed to the render and postRender methods. To fetch multiple URLs, you must pass an object where the key is a name you select for this data, and the value is the URL. It is important to note that the render is called once before the data is fetched (to enable information about loading or partial data can be displayed) and once after the data is fetched.
hasStyle boolean No false Whether to use style.css file. File must be present in widget’s main directory.
hasTemplate boolean No false Whether to use widget.html file as template. File must be present in widget’s main directory.
id string Yes - The ID of the widget definition. Must match the name of the directory into which it is placed.
initialConfiguration array No [] A list of widget configuration options. The options are displayed when a user clicks the Configure icon in the top-right corner of the widget in edit mode. It can also be accessed in the widget, to determine the current selected configuration.
initialHeight string No 12 The default height of the widget when added to a page.
initialWidth string No 3 The default width of the widget when added to a page.
isReact boolean Yes - Set as true when writing a React widget.
name string Yes - The display name of the widget that is displayed in the Add Widget dialog. It is also used as the default widget name.
showBorder boolean No true Whether to display widget’s border.
showHeader boolean No true Whether to display a header. If a header is not displayed, a user cannot change the widget name.
permission string Yes - This property specifies which user may later access and view this widget. It may take one of the following three values defined in Stage.GenericConfig.CUSTOM_WIDGET_PERMISSIONS object: CUSTOM_ADMIN_ONLY (applies for ‘sys_admin’ and ‘manager’ roles), CUSTOM_SYS_ADMIN_ONLY (applies for ‘sys_admin’ only, CUSTOM_ALL (applies to all user-roles).

initialConfiguration

initialConfiguration supports 4 generic pre-made config fields (see fetchUrl for example):

In addition to listed above, you can create your own configuration fields. Example of configuration with user-defined fields:

    initialConfiguration: [
        Stage.GenericConfig.PAGE_SIZE_CONFIG(3),
        {id: 'username', name: 'Fetch with', placeHolder:"GitHub user", description:"...", default:"cloudify-examples", type: Stage.Basic.GenericField.STRING_TYPE},
        {id: 'filter', name: 'Filter', placeHolder:"GitHub filter", description:"...", default:"blueprint in:name NOT local", type: Stage.Basic.GenericField.STRING_TYPE},
        {id: "displayStyle",name: "Display style", items: [{name:'Table', value:'table'}, {name:'Catalog', value:'catalog'}], default: "catalog", type: Stage.Basic.GenericField.LIST_TYPE},
        Stage.GenericConfig.SORT_COLUMN_CONFIG('created_at'),
        Stage.GenericConfig.SORT_ASCENDING_CONFIG(false)
    ]

You can find descriptions of the fields in the GenericField component documentation).

Configuration fields values can be fetched in render method using widget.configuration object. See Accessing data in render() for details.

fetchUrl

There are two primary ways of pulling data from remote sources: fetchUrl and fetchData().

fetchUrl is an object member and may be defined either as a string or an object with multiple string properties (property:URL) where each property represents a separate URL to query. A single URL’s results will be available directly in the data object. In case fetchUrl is defined with multiple URLs, the results will be accessible by property name of this URL (i.e. data.nodes).

Single URL Example
fetchUrl:  'localhost:50123/public/nodes'
// ...
render: function(widget,data,error,toolbox) {
    let your_data = data;
    //...
}
Mulitple URL Example
fetchUrl: {
    nodes: '[manager]/nodes?_include=id,deployment_id,blueprint_id,type,type_hierarchy,number_of_instances,host_id,relationships,created_by[params:blueprint_id,deployment_id,gridParams]',
    nodeInstances: '[manager]/node-instances?_include=id,node_id,deployment_id,state,relationships,runtime_properties[params:deployment_id]',
    deployments: '[manager]/deployments?_include=id,groups[params:blueprint_id,id]'
}
// ...
render: function(widget,data,error,toolbox) {
    let nodes = data.nodes.items;
    let deployments = data.nodeInstances.items; 
    //...
}

As seen in the example above, URLs provided in fetchUrl can be parametrized with several special tokens:

fetchUrl: '[manager]/executions?is_system_workflow=false[params]'
fetchUrl - Inclusive Params

The following example illustrates fetchUrl with both tokens along with resulting URL:

initialConfiguration: [
    Stage.GenericConfig.POLLING_TIME_CONFIG(60),
    Stage.GenericConfig.PAGE_SIZE_CONFIG(),
    Stage.GenericConfig.SORT_COLUMN_CONFIG('column_name'),
    Stage.GenericConfig.SORT_ASCENDING_CONFIG(false)
],
fetchUrl: {
    nodes: '[manager]/nodes[params]'
},
fetchParams: function(widget, toolbox) {
    return {
        sampleFuncParam: 'dummy'
    }
}

Result URL: http://localhost:3000/sp/?su=/api/v3.1/nodes?&_sort=-column_name&_size=5&_offset=0&sampleFuncParam=dummy

This url can be divided into 3 separate parts:

Field Example Description
manager address http://localhost:3000/sp/?su=/api/v3.1/ The internal value of Cloudify manager [manager]
endpoint name nodes? Remaining part of the REST endpoint address
generic params &_sort=-column_name&_size=5&_offset=0 Parameters that were implicitly added to request. These parameters are inferred from the GenericConfig objects in initialConfiguration and are responsible for pagination of the results. It is possible to omit them by explicitly specifying param names to be used like so [params:my-param]. Alternatively, gridParams (sort, size, offset) can be simply removed from initialConfiguration.
custom params &sampleFuncParam=dummy Custom parameters can be defined in fetchParams() function. Each custom parameter must be returned as a property of an Object returned by fetchParams() function.
fetchUrl - Exclusive Params

The same URL, this time with explicit param names (and the gridParams tag):

initialConfiguration: [
    Stage.GenericConfig.POLLING_TIME_CONFIG(60),
    Stage.GenericConfig.PAGE_SIZE_CONFIG(),
    Stage.GenericConfig.SORT_COLUMN_CONFIG('column_name'),
    Stage.GenericConfig.SORT_ASCENDING_CONFIG(false)
],
fetchUrl: {
    nodes: '[manager]/nodes[params:sampleFuncParam,gridParams]'
//  which is essentially the same as
//  nodes: '[manager]/nodes[params:sampleFuncParam,_size,_offset_,_sort]'
},
fetchParams: function(widget, toolbox) {
    return {
        sampleFuncParam: 'dummy'
    }
}

Result URL: http://localhost:3000/sp/?su=/api/v3.1/nodes?&sampleFuncParam=dummy&_sort=-column_name&_size=5&_offset=0

Widget Functions

The following functions are available for widgets.

init()

Called when the widget definition is loaded, which occurs after the system is loaded. Can be used to define certain elements, for example classes and objects that will be used in the widget definition.

render(widget, data, error, toolbox)

Called each time that the widget needs to draw itself. This might occur when the page is loaded, widget data is changed, context data is changed, widget data is fetched, and so on. render parameters are:

render() is focal to the appearance of the widget as the return value of this function will be rendered to UI by React engine. As such it is important to understand how to build widgets. The following example illustrates the simplest usage:

render: function(widget,data,error,toolbox) {
    return (
        <span>Hello World!</span>
    );
}

Please note that render() may only return a single DOM node (refer to JSX spec for more detail). In order to render more than one HTML element they must be wrapped in a parent element (a div is usually a good choice):

render: function(widget,data,error,toolbox) {
    return (
        <div>
            <span>Hello World!</span>
            <p>Writing Cloudify UI widgets is <strong>super</strong> easy</p>
        </div>
    );
}
Using Ready Components In render()

Although using plain HTML tags gives you extreme flexibility, usually it is much quicker to design your widget with the use of Cloudify UI ready-made components. These components were designed with UI uniformity and ease-of-use in mind, and as are very easy to learn and use. The following example illustrates how to use a KeyIndicator component:

render: function(widget,data,error,toolbox) {
let {KeyIndicator} = Stage.Basic;
    return (
        <div>
            <KeyIndicator title='User Stars' icon='star' number={3} />
        </div>
    );
}

Notice that the KeyIndicator component is imported into the widget. It is defined in the render method as:

let {KeyIndicator} = Stage.Basic;

You can also import multiple components in the same line, for example:

let {KeyIndicator, Checkmark} = Stage.Basic;

Other components are available in the Stage.Basic library.

Accessing Data In render()

There can be several independent data sources for your widget. Two most commonly used are the configuration and data objects. The following example illustrates how to access both of them:

Stage.defineWidget({
    id: 'sampleWidget',
    name: 'A basic example',
    description: 'This widget polls data from two different sources',
    initialWidth: 2,
    initialHeight: 2,
    showHeader: false,
    showBorder: false,
    isReact: true,
    initialConfiguration: [
        {id: 'confText', name: 'Conf Item',  placeHolder: 'Configuration text item', default: 'Conf text', type: Stage.Basic.GenericField.STRING_TYPE}
    ],
    permission: Stage.GenericConfig.CUSTOM_WIDGET_PERMISSIONS.CUSTOM_ALL,

    fetchData(widget, toolbox){
        return Promise.resolve({fetchedText: 'Fetched text'});
    },

    render: function(widget,data,error,toolbox) {
        let {Loading} = Stage.Basic

        if (_.isEmpty(data)) // Make sure the data is already fetched, if not show a loading spinner
            return (<Loading message='Loading data...'></Loading>)
        else 
            return (
                <div>
                    <p>confItem value: {widget.configuration.confText}</p>
                    <p>fetchedText value: {data.fetchedText}</p>
                </div>
            );
    }
});

The above widget prints will display two lines containing the strings defined in the data sources: “Conf text” and “Fetched Text”. Please note how the widget makes sure data has been loaded has completed before rendering it. Skipping this check would result in an error in browser console.

initialConfiguration, as the name suggests is only used if there are no user defined values for these properties. A user can change them by entering the ‘Edit Mode’ where he can modify widget’s configuration. From that point, the current widget will use the value provided by the user. To reset it to it’s default value, the widget must be removed and re-added to the workbench.

Moreover, please remember to remove and re-add the widget to the dashboard if changing the initialConfiguration field. It is only loaded for newly ‘mounted’ widgets.

postRender(el, widget, data, toolbox)

Non-React widgets only. PostRender is called immediately after the widget has been made visible in the UI. This function has access to the same objects as the render function with one addition - the el object containing a reference to the widget’s container (parent) object.

fetchData(widget, toolbox)

An alternative to using fetchUrl is the fetchData() function. It provides greater flexibility when you need to pre-process your results or chain them into nested Promises (ie. Pull a list of URLs and resolve each of those URLs). The return value for fetchData() is expected to be a promise. As such if you would like to return a primitive value you would need to wrap it in a promise:

fetchData(widget, toolbox){
    return Promise.resolve({key:value});
}

Please note that should the result be a single primitive value you still need to return it as a property of an Object, since referencing the Object directly is illegal in React. With this in mind, the following example would not work:

// THIS WILL NOT WORK
fetchData(widget, toolbox){ return 10; }
render(widget,data,error,toolbox){ 
    return (
        <div>
            {data}  // This will produce a runtime error
        </div>
    )
}

Instead, you can return the int value as a property of the object like so:

fetchData(widget, toolbox){ return {myInt: 10}; }
render(widget,data,error, toolbox) { 
    return (
        <div>
            {data.myInt}  // OK
        </div>
    )
}

Note: fetchUrl and fetchData() are mutually exclusive, that is if you define fetchUrl in your widget, then fetchData() definition will be ignored.

fetchParams(widget, toolbox)

fetchParams() function delivers parameters to fetchData() function which can be applied with [params] wildcard.

Example:

fetchParams: function(widget, toolbox) {
    let deploymentId = toolbox.getContext().getValue('deploymentId');

    return {deployment_id: deploymentId};
}

Widget Building

When you use the React utility to develop the widget, you must use the Widget build system to generate the widget package.

Development Tools

The widget development tools include built-in features, widget objects, functions, templating mechanism and available libraries.

Widget Object

The widget object has the following attributes:

Attribute Description
id The ID of the widget
name The display name of the widget. (The widget definition name is the default name for the widget, but a user can change it)
height The height of the widget on the page
width The width of the widget on the page
x The x location of the widget on the page
y The y location of the widget on the page
definition The widget definition object as it was passed to defineWidget method. All widget definitions are contained in the widget definition object. The only additional field that the widget can access is template, which is fetched from the HTML and added to the widget definition.

Toolbox Object

The toolbox object provides the widget with tools to communicate with the application and other widgets. It also provides generic tools that the widget might require.

The toolbox provides access to the following tools:

getEventBus()

Used to register (listen to) and trigger events. The event bus is used to enable a widget to broadcast an event, usually a change that it made that will affect others. For example, if a blueprints widget creates a new deployment, other widgets need to be aware that the the deployment list has changed. The listening widgets then call a refresh. Event bus supports the following methods:

For example:

componentDidMount() {
    this.props.toolbox.getEventBus().on('deployments:refresh',this._refreshData,this);
}
componentWillUnmount() {
    this.props.toolbox.getEventBus().off('deployments:refresh',this._refreshData);
}
_deleteDeployment() {
    // Do somehting...
    actions.doDelete(deploymentToDelete).then(()=>{
        this.props.toolbox.getEventBus().trigger('deployments:refresh');
    }).catch((err)=>{
        // Handle errors...
    });
} 

getManager()

Returns manager object. Used to read current manager’s properties. Available calls:

getIp()
getCurrentUsername()
getManagerUrl(url, data)
getApiVersion()
getSelectedTenant()
doGetFull(url, params, parseResponse, fullData, size)

getExternal()

Used to access the connected Cloudify Manager. The Manager provides access to the Manager REST API. The URL is the service URL, without the /api/vX.X

doGet(url,params)
doPost(url,params,data)
doDelete(url,params,data)
doPut(url,params,data)
doUpload(url,params,file,method)

It also exposes a method that only constructs the URL. Use this with caution because some request headers require being passed to the Manager.

getManagerUrl(url,data)

For example,

return this.toolbox.getManager().doDelete('/deployments/${blueprint.id}');

doUpload(blueprintName, blueprintFileName, file) {   
    return this.toolbox.getManager().doUpload('/blueprints/${blueprintName}',
                                              _.isEmpty(blueprintFileName) 
                                                ? null   
                                                : {application_file_name: blueprintFileName+'.yaml'},
                                              file);
}

Please note that it is recommended to use fetchData() instead of doGet(URL, params) since fetchData() not only utilizes doGet() but also gives easy access to helper params.

getInternal()

Same as getExternal(), but on a secured connection. All headers are appended with an ‘Authentication-Token’.

getWidgetBackend()

Same as getInternal(), but it allows you to call previously defined widget backend endpoints (see Widget backend section for details).

getNewManager(ip)

Returns a manager object connected on the specified IP. May be needed in order to join a different manager (eg. for cluster joining).

getContext()

A widget context gives access to the application context. Using the context we can pass arguments between widgets, for example when a blueprint is selected, set the context to the selected blueprint, and all the widgets that can filter by blueprint can read this value and filter accordingly.

The context supports these methods:

getConfig()

Returns global widget configuration as defined in conf/widgets.json.

refresh()

If we did some actions in the widget that will require fetching the data again (for example we added a record) we can ask the app to refresh only this widget by calling refresh().

loading(boolean)

Will show/hide a loading spinner in widget header. Not allowed in render() and postRender() methods as it changes store’s state leading to render() and postRender() re-run.

drillDown(widget,defaultTemplate,drilldownContext)

When you drill down to a page, you must pass the drilldown page template name. When a widget is on a page and you use the drilldown action (for example, in a link click event to a button) for the first time to access the page, the app creates a new page based on the passed template. When this page is created, the user can edit it like any other page. Each time the user accesses to this page, the existing page is shown.

You can also pass a drilldownContext to the drilldown page. This context is saved on the URL and is available through the app context. This value is persistent, so if a user drills down to a page and then refreshes the page, the context is saved. For example, with the selected deployment in drilldown deployment page.

For example when selecting a deployment we drill down to a deployment page. It looks like this:

    _selectDeployment(item) {
        this.props.toolbox.drillDown(this.props.widget,'deployment',{deploymentId: item.id});
    }

You can see an example of the deployment template in the Cloudify UI repository in the /pages/deployment.json file.

Drilldown Page Templates

Drilldown page templates are defined in the /templates/pages directory. Each file contains one page template configuration.

Page template configuration has a name which is the default page name, and list of widgets. Each widget will have the following fields

field description
name Widget default name
definition The ID of the widget to use
width The initial width of the widget on the page
height The initial height of the widget on the page
x The initial x location of the widget on the page
y The initial y location of the widget on the page
configuration The initial configuration of the widget (Optional)

If x and/or y are not defined the page will be auto arranged (not recommended)

For example:

{
  "name": "template-name",
  "widgets": [
    {
      "name": "Deployments",
      "definition": "deployments",
      "width": 12,
      "height": 24,
      "x": 7,
      "y": 35,
      "configuration":{
        "displayStyle":"list"
      }
    },
    ...
  ]
}

External Libraries

The external libraries available to a widget are: moment, jQuery and Lodash.

moment - Date/Time parsing utility. Moment documentation

for example:

var formattedData = Object.assign({},data,{
    items: _.map (data.items,(item)=>{
        return Object.assign({},item,{
            created_at: moment(item.created_at,'YYYY-MM-DD HH:mm:ss.SSSSS').format('DD-MM-YYYY HH:mm'),
            updated_at: moment(item.updated_at,'YYYY-MM-DD HH:mm:ss.SSSSS').format('DD-MM-YYYY HH:mm'),
        })
    })
});

jQuery - Feature-rich JS library. jQuery API

for example:

postRender: function(el,widget,data,toolbox) {
    $(el).find('.ui.dropdown').dropdown({
        onChange: (value, text, $choice) => {
            context.setValue('selectedValue',value);
        }
    });
})

Lodash - Modern JavaScript utility library delivering modularity, performance & extras. Lodash documentation

for example:

_.each(items, (item)=>{
    //...
});

Widget Backend

With widget backend support user can create HTTP endpoints in UI backend. They allow to define specific actions when endpoint is called. There can be used helper services not available in widget frontend.

Security Aspects

Defining Endpoints

To create endpoint per widget you need to create backend.js file with at least one endpoint definition. That file must be placed in widget main folder similarly to widget.js file.

backend.js file structure

Example of backend.js file is presented below:

module.exports = function(r) {

    r.register('manager', 'GET', (req, res, next, helper) => {
        var _ = require('lodash');
        var jsonBody = require('body/json');
        var url = req.query.endpoint;
        var params = _.omit(req.query, 'endpoint');
        var headers = req.headers;

        jsonBody(req, res, function (error, body) {
            helper.Manager.doPost(url, params, body, headers)
                .then((data) => res.send(data))
                .catch(next);
        })
    });
}

backend.js file should export a function taking one argument (r in example). This function’s body contains calls to register method (r.register in example). Each call registers HTTP endpoint in the backend.

Syntax of register method:

function register(name, method, body)

where

Helper Services

In this section helper services, which can be used from helper object in endpoints body are described.

Manager

Available methods:

where:

Request

Available methods:

where:

Calling Endpoints

Previously defined endpoints can be accessed in widget’s frontend using toolbox.getWidgetBackend() method (see getWidgetBackend() for details).

Example of calling endpoint status with GET method widget.js:

Stage.defineWidget({
    // ... all stuff necessary to define widget ...

    fetchData: function(widget, toolbox, params) {
        return toolbox.getWidgetBackend().doGet('status')
            .then((data) => Promise.resolve({status: data}))
            .catch((error) => Promise.reject('Error fetching status. Error: ' + error));
    },

    render: function(widget,data,error,toolbox) {
        var status = data ? data.status : 'unknown';
        return (
            <p>
                Status: <b>{status}</b>
            </p>
        );
    }
});

The status endpoint for GET method must be defined in backend.js file:

module.exports = function(r) {
    r.register('status', 'GET', (req, res, next, helper) => {
        res.send('OK');
    });
}

Widget Template

The widget template is an html file written with lodash template engine.

Widget template if fetched when the widget definition is loaded, and its passed to the render function. To access it use widget.definition.template. To render the template using the built in lodash templates engine use _.template(widget.definition.template)(data);, where ‘data’ is any context you want to pass on to the template. For example, a simple render function will look like this:

render: function(widget,data,toolbox) {
    if (!widget.definition.template) {
        return 'missing template';
    }
    return _.template(widget.definition.template)();
}