Countly Documentation

Countly Resources

Here you'll find comprehensive guides to help you start working with Countly as quickly as possible.

Creating UI View

Now let's discuss how you can build a UI for your plugin by adding new Countly views to the dashboard.

We know that some files are loaded automatically into Countly dashboard, and now using these files (basically, countly.models.js and countly.views.js) we can both get data from API and display them.

So what we need to achieve is:

  • Create a model which can fetch data from API
  • Create a Backbone view which would get the data from model and load it into template and append to a page
  • Add this view to App Router
  • Inject some Javascript to modify page, like create a menu item for our plugin

countly.models.js

Now let's define a simply model which fetched data from api on path /o?method=ourplugin

(function (countlyOurplugin, $) {

	//we will store our data here
  var _data = {};

  //Initializing model
  countlyOurplugin.initialize = function () {
  
  	//returning promise
		return $.ajax({
        type:"GET",
        url:"/o",
        data:{
          //providing current user's api key
        	"api_key":countlyGlobal.member.api_key,
          //providing current app's id
          "app_id":countlyCommon.ACTIVE_APP_ID,
          //specifying method param
          "method":"ourplugin"
        },
        success:function (json) {
           //got our data, let's store it
           _data = json;
        }
    });
 	};
  
  //return data that we have
 	countlyOurplugin.getData = function () {
		return _data;
  };
	
}(window.countlyOurplugin = window.countlyOurplugin || {}, jQuery));

Localization

There are two ways to provide localized string in Countly.

Through javascript by using global jQuery.i18n.map object which pre fetched values for you from your localized .properties files

alert(jQuery.i18n.map["ourplugin.hello"]);

Or through data attributes, which elements will have localized string added as contents of the element at render time

<!-- This will be changed  -->
<div data-localize="ourplugin.title"></div>

<!-- To this, automatically -->
<div data-localize="ourplugin.title">Our Plugin</div>

countly.views.js

Now let's create a basic Countly view, which will use our model to fetch data

window.OurpluginView = countlyView.extend({
  
  //need to provide at least empty initialize function
  //to prevent using default template
	initialize:function (){
		//we can initialize stuff here
	},
  
  beforeRender: function() {
    
		//check if we already have template
		if(this.template)
      
			//then lets initialize our mode
			return $.when(countlyOurplugin.initialize()).then(function () {});
		else{
      
			//else let's fetch our template and initialize our mode in paralel
			var self = this;
			return $.when($.get(countlyGlobal["path"]+'/ourplugin/templates/default.html', function(src){
        
				//precompiled our template
				self.template = Handlebars.compile(src);
			}), countlyOurplugin.initialize()).then(function () {});
		}
  },
  
	//here we need to render our view
  renderCommon:function () {
		
				//provide template data
        this.templateData = {
            "page-title":"OurPlugin",
            "logo-class":"",
						"data":countlyOurplugin.getData()
        };
		
				//populate template with data and attach it to page's content element
        $(this.el).html(this.template(this.templateData));
  },
  
	//here we need to refresh data
  refresh:function () {
      var self = this;
      $.when(countlyOurplugin.initialize()).then(function () {
		
				//our view is not active
        if (app.activeView != self) {
            return false;
        }
			
				//here basically we want to do the same we did in renderCommon method
        self.renderCommon();
      });
    }
});

Then lets create instance of our view and add it to app router to some specific url

//create view
app.ourpluginView = new OurpluginView();

//register route
app.route('/ourplugin', 'ourplugin', function () {
	this.renderWhenReady(this.ourpluginView);
});

After that you should be able to view your view at http://yourdomain.com/dashboard#/ourplugin

Injecting scripts and html

But we do not want to type in our url, we want to get there by clicking on the menu button. To accomplish that we can inject the html to render our menu item.

This is a one time task which should be accomplished when webpage has loaded, so here we can simply wait for document to be ready and add our plugin item to menu.

$( document ).ready(function() {
	var menu = '<a href="#/ourplugin" class="item" ">'+
        '<div class="logo fa fa-plugin" style="background-image:none; font-size:24px; text-align:center; width:35px; margin-left:14px; line-height:42px;"></div>'+
        '<div class="text" data-localize="ourplugin.title"></div>'+
    '</a>';
	
	if($('.sidebar-menu #management-menu').length)
		$('.sidebar-menu #management-menu').before(menu);
	else
		$('.sidebar-menu').append(menu);
});

Ok, but what if we need to inject html on specific view, which might not be available when page is loaded. How can we know if user accessed this specific view, so we could modify it.

We can add a page script which will be executed on specified route. So for example, this script will be executed every time user visits page "/dashboard#/analytics/sessions"

app.addPageScript("/analytics/sessions", function(){
   //You can perform any dom manipulations here
   alert("You are viewing Sessions Analytics");
});

But what if we need to do something on every page view? Easy, we use # as a page script route

app.addPageScript("#", function(){
   //You can perform any dom manipulations here
   console.log("new page view loaded");
});

How about dynamic URLs which you don't know upfront? Easy!

// All pages which URL starts with '/users/' and follows some string, for example: 
// /users/c49ebdad8f39519af9e0bfbf79332f4ec50b6d0f
app.addPageScript("/users/#", function(){
   console.log("new user profile view loaded");
});

Great, but sometimes we need to change something in the view, but then it refreshes and changes back, how to tackle that?

We can add a refresh page script to specific routes, same as add page scripts.

app.addRefreshScript("/analytics/sessions", function(){
  //You can perform any dom manipulations here
	console.log("sessions view refreshed");
});

Processing metric data

As common usage for plugins are adding new metrics, let's view an example on how you can display your metric data.

First again model. The data saved and displayed differs much, thus there is a difficult transformation performed on metric data, but you can use the helper method to transform it easily

CountlyHelpers.createMetricModel(window.countlyOurmetric = window.countlyOurmetric || {}, "ourmetric", jQuery);

Then we create a view to use our model to fetch metric data and generate pie charts and datatable with data

Since we will be using default template already used in the Countly, we don't need to load our own.

window.OurmetricView = countlyView.extend({
  //initalize out model
	beforeRender: function() {
  	return $.when(countlyOurmetric.initialize()).then(function () {});
  },
  
  //render our data
  renderCommon:function (isRefresh) {
  	var data = countlyOurmetric.getData();

    //prepare template data
		this.templateData = {
    	"page-title":jQuery.i18n.map["ourmetric.title"],
      "logo-class":"",
      "graph-type-double-pie":true,
      "pie-titles":{
      	"left":jQuery.i18n.map["common.total-users"],
        "right":jQuery.i18n.map["common.new-users"]
      }
    };

    //if loading first time and not refershing
    if (!isRefresh) {
      
      //build template with data
    	$(this.el).html(this.template(this.templateData));

      //create datatable with chart data
      this.dtable = $('.d-table').dataTable($.extend({}, $.fn.dataTable.defaults, {
        //provide data to datatables
      	"aaData": data.chartData,
        
        //specify which columns to show
        "aoColumns": [
        	{ "mData": "ourmetric", sType:"session-duration", "sTitle": jQuery.i18n.map["ourmetric.title"] },
          { "mData": "t", sType:"formatted-num", "mRender":function(d) { return countlyCommon.formatNumber(d); }, "sTitle": jQuery.i18n.map["common.table.total-sessions"] },
          { "mData": "u", sType:"formatted-num", "mRender":function(d) { return countlyCommon.formatNumber(d); }, "sTitle": jQuery.i18n.map["common.table.total-users"] },
          { "mData": "n", sType:"formatted-num", "mRender":function(d) { return countlyCommon.formatNumber(d); }, "sTitle": jQuery.i18n.map["common.table.new-users"] }
         ]
 			}));
      
      //make table headers sticky
			$(".d-table").stickyTableHeaders();
       
      //draw chart with total data
      countlyCommon.drawGraph(data.chartDPTotal, "#dashboard-graph", "pie");
      
      //draw chart with new data
      countlyCommon.drawGraph(data.chartDPNew, "#dashboard-graph2", "pie");
    }
  },
  
  //refreshing out chart
  refresh:function () {
  	var self = this;
    $.when(countlyOurmetric.refresh()).then(function () {
      
      //not our view
    	if (app.activeView != self) {
      	return false;
      }
      
      //populate and regenerate template data
      self.renderCommon(true);

      //replace existing elements in view with new data
      newPage = $("<div>" + self.template(self.templateData) + "</div>");  
     	$(self.el).find(".dashboard-summary").replaceWith(newPage.find(".dashboard-summary"));
      
      var data = countlyOurmetric.getData();
      
      //refresh charts
      countlyCommon.drawGraph(data.chartDPTotal, "#dashboard-graph", "pie");
      countlyCommon.drawGraph(data.chartDPNew, "#dashboard-graph2", "pie");
      
      //refresh datatables
			CountlyHelpers.refreshTable(self.dtable, data.chartData);
    });
  }
});

//create view
app.ourmetricView = new OurmetricView();

//register route
app.route("/analytics/ourmetric", 'ourmetric', function () {
	this.renderWhenReady(this.ourmetricView);
});

//add menu item
$( document ).ready(function() {
	var menu = '<a href="#/analytics/ourmetric" class="item">'+
		'<div class="logo"></div>'+
		'<div class="text" data-localize="ourmetric.title"></div>'+
	'</a>';
	$('#analytics-submenu').append(menu);
});

And we have added our new metric to Countly installation and additionally to API part example, we can share the plugin with everybody else.

Now we only need to modify your Countly SDK installation to report our custom metrics to our server and we are done.

Creating UI View