Creating Custom Widgets
Get the latest docs
You are looking at documentation for an older release. Not what you want? Go to the current release documentation.Cloudify enables you to create your own widgets, to assist you in orchestrating your applications in the Cloud.
Widgets can be written using two different methods.
Using the React Utility is the recommended method, and requires a build operation to be executed. You can build the
widget.js
file yourself, or use the Cloudify build system.Pure Vanilla Java Script which enables attachment of an HTML template file. The callbacks for this method are described later in this topic.
Widget File Structure
A widget comprises a number of files, as described below.
widget.js
‑ Holds the widget’s definition (mandatory)widget.png
‑ The preview image of the widget in the widgets catalog (mandatory)widget.html
‑ A widget template file. Relevant only when you are writing a widget using vanilla JavaScript with an HTML template (optional)widget.css
‑ The CSS file that the widget uses (optional)
You place the widget in the widgets library, which must hold the ID of the widget. For example, if you are creating a blueprint (ID is blueprint
), the blueprint library will be
/widgets
/blueprint
widget.js
widget.html
widget.png
widget.css
To use the Cloudify build system, you must put your widget.js
file into an src
library, together 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
widget.js (generated by the build system)
widget.html
widget.png
widget.css
Defining the Widget
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 (refer to the previous paragraph 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 is a number of configuration fields that you can provide when designing a widget.
The Stage.defineWidget
function receives a settings object with the options described in the following table.
Option | Type | Default | Description |
---|---|---|---|
id |
string | - | The ID of the widget definition. Must match the name of the directory into which it is placed. Mandatory. |
name |
string | - | The display name of the widget, which is displayed in the Add Widget dialog. It is also used as the default widget name. Mandatory. |
description |
string | - | Description of the widget that is displayed in the Add Widget dialog. Optional. |
initialWidth |
string | - | The default width of the widget when added to a page. Mandatory. |
initialHeight |
string | - | The default height of the widget when added to a page. Mandatory. |
color |
string | red |
The color of the widget. One of the following: red , orange , yellow , olive , green , teal , blue , violet , purple , pink , brown , grey or black |
showHeader |
boolean | true |
Whether to display a header. If a header is not displayed, a user cannot change the widget name. |
isReact |
boolean | false |
Set as true when writing a React widget. |
fetchUrl |
string/object | - | 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. |
initialConfiguration |
array | - | 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. |
pageSize |
integer | - | The initial page size for widgets that support pagination. |
categories |
array | - | 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’). Optional. |
permission |
string | - | 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). Mandatory. |
Note: initialConfiguration supports 4 generic pre-made config fields (see fetchUrl
for example):
* Stage.GenericConfig.POLLING_TIME_CONFIG(int)
- How often to refresh the data (in seconds)
* Stage.GenericConfig.PAGE_SIZE_CONFIG()
- Takes no arguments and set’s the page size to default 5
* Stage.GenericConfig.SORT_COLUMN_CONFIG(string)
- Column name to sort by
* Stage.GenericConfig.SORT_ASCENDING_CONFIG(boolean)
- Change sorting order (true=ascending)
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]'
- The
[manager]
token will be replaced with the current Cloudify manager’s IP address (or proxy, if applicable). - The
[params]
token, on the other hand, is quite special. This placeholder can be expanded into a number of things depending on usage:[params]
alone anywhere in the URL will be expanded to default pagination parameters (_size
,_offset
,_sort
) if available (seeinitialConfiguration
). This mode is inclusive - all params availavble in the widget will be appended to URL.[params:param_name1(,param_name2)]
will be replaced with “¶mName1:paramValue1” in the URL. Please note that this can be used both to selectively pick pagination parameter as well as custom parameters (seefetchParams()
). This mode is exclusive - parameters not specified explicitly will be skipped. When using selective param picking ([params:param_name]
) you can use a pre-definedgridParams
tag to include all pagination parameters (_size
,_offset
,_sort
) instead of specifying explicitly each of the three.
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:
* The widget object itself
* The fetched data, either using fetchUrl
or fetchData
. The data is null
if fetchData
or fetchUrl
is not specified. The data will also pass null
to the render
method until data is fetched. If you are expecting data, you can render a “loading” indicator.
* The error if data fetching failed
* The toolbox object.
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>
);
}
Take a note of how the KeyIndicator
component is imported into the widget. From within the render method it is defined as
let {KeyIndicator} = Stage.Basic;
Similarly, you can import multiple components in the same line, ie:
let {KeyIndicator, Checkmark} = Stage.Basic;
There is a number of components ready for use in the Stage.Basic
library. See Basic components reference documentation for details.
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}
],
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 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:
* on (event, callback, context)
* trigger (event)
* off (event, offCallback)
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’.
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: * setValue(key,value) * getValue(key) - returns value
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)
Drilling down to a page requires passing the drilldown page template name. Templates will be described in the next section. When a widget is on a page, and drilldown action done (through link click event to a button for example), if it’s the first time we access this drilldown page, the app will create a new page based on the passed template. Once this page is created the user can edit it like any other page. All next accesses to this page will use this page. Also you can pass a ‘drilldownContext’ to the drilldown page. This context will be saved on the URL and will be available through the app context. This value will be saved upon refresh, so if a user drilldown to a page, and then refreshes the page, the context will be saved (for example - 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});
}
The ‘deployment’ template looks like this:
{
"name": "Deployment",
"widgets": [
{
"name": "topology",
"widget": "topology",
"width": 12,
"height": 5,
"x": 0,
"y": 0
},
{
"name": "CPU Utilization - System",
"width": 6,
"height": 4,
"widget": "cpuUtilizationSystem",
"x": 0,
"y": 5
},
{
"name": "CPU Utilization - User",
"width": 6,
"height": 4,
"widget": "cpuUtilizationUser",
"x": 6,
"y": 5
},
{
"name": "Deployment Inputs",
"width": 5,
"height": 3,
"widget": "inputs",
"x": 0,
"y": 9
},
{
"name": "Deployment Events",
"width": 7,
"height": 3,
"widget": "events",
"x": 5,
"y": 9
}
]
}
Drilldown page templates
Drill down page templates are defined in the ‘/templates’ library.
The library looks like this:
/templates
template1.json
template2.json
...
templates.json
The templates.json contains a list of the available templates (temporary until we’ll have a server that will handle this). Each template file contains one page template configuration.
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 |
widget | 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 |
If x and/or y are not defined the page will be auto arranged (not recommended)
For example:
{
"name": "template-name",
"widgets": [
{
"name": "topology",
"widget": "topology",
"width": 12,
"height": 5,
"x": 0,
"y": 0
},
...
]
}
Additional libraries that are available to a widget
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 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)();
}