countly/countly.template.js

/* global Backbone, Handlebars, countlyEvent, countlyCommon, countlyGlobal, CountlyHelpers, countlySession, moment, Drop, _, store, countlyLocation, jQuery, $, T, countlyTaskManager*/
/**
* Default Backbone View template from which all countly views should inherit.
* A countly view is defined as a page corresponding to a url fragment such
* as #/manage/apps. This interface defines common functions or properties
* the view object has. A view may override any function or property.
* @name countlyView
* @global
* @namespace countlyView
* @example <caption>Extending default view and overwriting its methods</caption>
*  window.DashboardView = countlyView.extend({
*       renderCommon:function () {
*           if(countlyGlobal["apps"][countlyCommon.ACTIVE_APP_ID]){
*               var type = countlyGlobal["apps"][countlyCommon.ACTIVE_APP_ID].type;
*               type = jQuery.i18n.map["management-applications.types."+type] || type;
*               $(this.el).html("<div id='no-app-type'><h1>"+jQuery.i18n.map["common.missing-type"]+": "+type+"</h1></div>");
*           }
*           else{
*               $(this.el).html("<div id='no-app-type'><h1>"+jQuery.i18n.map["management-applications.no-app-warning"]+"</h1></div>");
*           }
*       }
*   });
*/
var countlyView = Backbone.View.extend({
    /**
    * Checking state of view, if it is loaded
    * @type {boolean}
    * @instance
    * @memberof countlyView
    */
    isLoaded: false,
    /**
    * Handlebar template
    * @type {object}
    * @instance
    * @memberof countlyView
    */
    template: null, //handlebars template of the view
    /**
    * Data to pass to Handlebar template when building it
    * @type {object}
    * @instance
    * @memberof countlyView
    */
    templateData: {}, //data to be used while rendering the template
    /**
    * Main container which contents to replace by compiled template
    * @type {jquery_object}
    * @instance
    * @memberof countlyView
    */
    el: $('#content'), //jquery element to render view into
    _myRequests: {}, //save requests called for this view
    /**
    * Initialize view, overwrite it with at least empty function if you are using some custom remote template
    * @memberof countlyView
    * @instance
    */
    initialize: function() { //compile view template
        this.template = Handlebars.compile($("#template-analytics-common").html());
    },
    _removeMyRequests: function() {
        for (var url in this._myRequests) {
            for (var data in this._myRequests[url]) {
                //4 means done, less still in progress
                if (parseInt(this._myRequests[url][data].readyState) !== 4) {
                    this._myRequests[url][data].abort();
                }
            }
        }
        this._myRequests = {};
    },
    /**
    * This method is called when date is changed, default behavior is to call refresh method of the view
    * @memberof countlyView
    * @instance
    */
    dateChanged: function() { //called when user changes the date selected
        if (Backbone.history.fragment === "/") {
            this.refresh(true);
        }
        else {
            this.refresh();
        }
    },
    /**
    * This method is called when app is changed, default behavior is to reset preloaded data as events
    * @param {function=} callback  - callback function
    * @memberof countlyView
    * @instance
    */
    appChanged: function(callback) { //called when user changes selected app from the sidebar
        countlyEvent.reset();
        $.when(countlyEvent.initialize()).always(function() {
            if (callback) {
                callback();
            }
        });
    },
    /**
    * This method is called before calling render, load your data and remote template if needed here
    * @returns {boolean} true
    * @memberof countlyView
    * @instance
    * @example
    *beforeRender: function() {
    *   var self = this;
    *   return $.when(T.render('/density/templates/density.html', function(src){
    *       self.template = src;
    *   }), countlyDeviceDetails.initialize(), countlyTotalUsers.initialize("densities"), countlyDensity.initialize()).then(function () {});
    *}
    */
    beforeRender: function() {
        return true;
    },
    /**
    * This method is called after calling render method
    * @memberof countlyView
    * @instance
    */
    afterRender: function() {
        CountlyHelpers.makeSelectNative();
    },
    /**
    * Main render method, better not to over write it, but use {@link countlyView.renderCommon} instead
    * @returns {object} this
    * @memberof countlyView
    * @instance
    */
    render: function() { //backbone.js view render function
        var currLink = Backbone.history.fragment;

        // Reset any active views and dropdowns
        $("#main-views-container").find(".main-view").removeClass("active");
        $("#top-bar").find(".dropdown.active").removeClass("active");

        // Activate the main view and dropdown based on the active view
        if (/^\/custom/.test(currLink) === true) {
            $("#dashboards-main-view").addClass("active");
            $("#dashboard-selection").addClass("active");
        }
        else {
            $("#analytics-main-view").addClass("active");
            $("#app-navigation").addClass("active");
        }
        $("#content-top").html("");
        this.el.html('');

        if (countlyCommon.ACTIVE_APP_ID) {
            var self = this;
            $.when(this.beforeRender(), initializeOnce()).fail(function(XMLHttpRequest, textStatus, errorThrown) {
                if (XMLHttpRequest && XMLHttpRequest.status === 0) {
                    // eslint-disable-next-line no-console
                    console.error("Check Your Network Connection");
                }
                else if (XMLHttpRequest && XMLHttpRequest.status === 404) {
                    // eslint-disable-next-line no-console
                    console.error("Requested URL not found: " + XMLHttpRequest.my_set_url + " with " + JSON.stringify(XMLHttpRequest.my_set_data));
                }
                else if (XMLHttpRequest && XMLHttpRequest.status === 500) {
                    // eslint-disable-next-line no-console
                    console.error("Internel Server Error: " + XMLHttpRequest.my_set_url + " with " + JSON.stringify(XMLHttpRequest.my_set_data));
                }
                else if ((XMLHttpRequest && typeof XMLHttpRequest.status === "undefined") || errorThrown) {
                    // eslint-disable-next-line no-console
                    console.error("Unknow Error: ");
                    if (XMLHttpRequest) {
                        // eslint-disable-next-line no-console
                        console.log(XMLHttpRequest.my_set_url + " with " + JSON.stringify(XMLHttpRequest.my_set_data) + "\n" + (XMLHttpRequest.responseText) + "\n");
                    }
                    // eslint-disable-next-line no-console
                    console.error(textStatus + "\n" + errorThrown);
                }
            })
                .always(function() {
                    if (app.activeView === self) {
                        self.isLoaded = true;
                        self.renderCommon();
                        self.afterRender();
                        app.pageScript();
                    }
                });
        }
        else {
            if (app.activeView === this) {
                this.isLoaded = true;
                this.renderCommon();
                this.afterRender();
                app.pageScript();
            }
        }

        if (countlyGlobal.member.member_image) {
            $('.member_image').html("");
            $('.member_image').css({'background-image': 'url(' + countlyGlobal.member.member_image + '?now=' + Date.now() + ')', 'background-size': '100%'});
        }
        else {
            var defaultAvatarSelector = countlyGlobal.member.created_at % 16 * 30;
            var name = countlyGlobal.member.full_name.split(" ");
            $('.member_image').css({'background-image': 'url("images/avatar-sprite.png")', 'background-position': defaultAvatarSelector + 'px', 'background-size': '510px 30px', 'text-align': 'center'});
            $('.member_image').html("");
            $('.member_image').prepend('<span style="text-style: uppercase;color: white;position: relative; top: 6px; font-size: 16px;">' + name[0][0] + name[name.length - 1][0] + '</span>');
        }
        // Top bar dropdowns are hidden by default, fade them in when view render is complete
        $("#top-bar").find(".dropdown").fadeIn(2000);

        return this;
    },
    /**
    * Do all your rendering in this method
    * @param {boolean} isRefresh - render is called from refresh method, so do not need to do initialization
    * @memberof countlyView
    * @instance
    * @example
    *renderCommon:function (isRefresh) {
    *    //set initial data for template
    *    this.templateData = {
    *        "page-title":jQuery.i18n.map["density.title"],
    *        "logo-class":"densities",
    *        "chartHTML": chartHTML,
    *    };
    *
    *    if (!isRefresh) {
    *        //populate template with data and add to html
    *        $(this.el).html(this.template(this.templateData));
    *    }
    *}
    */
    renderCommon: function(/* isRefresh*/) {}, // common render function of the view
    /**
    * Called when view is refreshed, you can reload data here or call {@link countlyView.renderCommon} with parameter true for code reusability
    * @returns {boolean} true
    * @memberof countlyView
    * @instance
    * @example
    * refresh:function () {
    *    var self = this;
    *    //reload data from beforeRender method
    *    $.when(this.beforeRender()).then(function () {
    *        if (app.activeView != self) {
    *            return false;
    *        }
    *        //re render data again
    *        self.renderCommon(true);
    *
    *        //replace some parts manually from templateData
    *        var newPage = $("<div>" + self.template(self.templateData) + "</div>");
    *        $(self.el).find(".widget-content").replaceWith(newPage.find(".widget-content"));
    *        $(self.el).find(".dashboard-summary").replaceWith(newPage.find(".dashboard-summary"));
    *        $(self.el).find(".density-widget").replaceWith(newPage.find(".density-widget"));
    *    });
    *}
    */
    refresh: function() { // resfresh function for the view called every 10 seconds by default
        return true;
    },
    /**
    * This method is called when user is active after idle period
    * @memberof countlyView
    * @instance
    */
    restart: function() { // triggered when user is active after idle period
        this.refresh();
    },
    /**
    * This method is called when view is destroyed (user entered inactive state or switched to other view) you can clean up here if there is anything to be cleaned
    * @memberof countlyView
    * @instance
    */
    destroy: function() { }
});

/**
 * View class to expand by plugins which need configuration under Management->Applications.
 * @name countlyManagementView
 * @global
 * @namespace countlyManagementView
 */
window.countlyManagementView = countlyView.extend({
    /**
     * Handy function which returns currently saved configuration of this plugin or empty object.
     * @memberof countlyManagementView
     * @return {Object} app object
     */
    config: function() {
        return countlyGlobal.apps[this.appId] &&
            countlyGlobal.apps[this.appId].plugins &&
            countlyGlobal.apps[this.appId].plugins[this.plugin] || {};
    },

    /**
     * Set current app id
     * @memberof countlyManagementView
     * @param {string} appId - app Id to set
     */
    setAppId: function(appId) {
        if (appId !== this.appId) {
            this.appId = appId;
            this.resetTemplateData();
            this.savedTemplateData = JSON.stringify(this.templateData);
        }
    },

    /**
     * Reset template data when changing app
     * @memberof countlyManagementView
     */
    resetTemplateData: function() {
        this.templateData = {};
    },

    /**
     * Title of plugin configuration tab, override with your own title.
     * @memberof countlyManagementView
     * @return {String} tab title
     */
    titleString: function() {
        return 'Default plugin configuration';
    },

    /**
     * Saving string displayed when request takes more than 0.3 seconds, override if needed.
     * @memberof countlyManagementView
     * @return {String} saving string
     */
    savingString: function() {
        return 'Saving...';
    },

    /**
     * Callback function called before tab is expanded. Override if needed.
     * @memberof countlyManagementView
     */
    beforeExpand: function() {},

    /**
     * Callback function called after tab is collapsed. Override if needed.
     * @memberof countlyManagementView
     */
    afterCollapse: function() {},

    /**
     * Function used to determine whether save button should be visible. Used whenever UI is redrawn or some value changed. Override if needed.
     * @memberof countlyManagementView
     * @return {Boolean} true if enabled
     */
    isSaveAvailable: function() {
        return JSON.stringify(this.templateData) !== this.savedTemplateData.toString();
    },

    /**
     * Callback function called to apply changes. Override if validation is needed.
     * @memberof countlyManagementView
     * @return {String} error to display to user if validation didn't pass
     */
    validate: function() {
        return null;
    },

    /**
     * Function which prepares data to the format required by the server, must return a Promise.
     * @memberof countlyManagementView
     * @return {Promise} which resolves to object of {plugin-name: {config: true, options: true}} format or rejects with error string otherwise
     */
    prepare: function() {
        var o = {}; o[this.plugin] = this.templateData; return $.when(o);
    },

    /**
     * Show error message returned by server or by validate function. Override if needed.
     * @memberof countlyManagementView
     * @param {string} error - error message to show
     */
    showError: function(error) {
        CountlyHelpers.alert(error, 'popStyleGreen', {title: jQuery.i18n.map['management-applications.plugins.smth'], image: 'empty-icon', button_title: jQuery.i18n.map['management-applications.plugins.ok']});
    },

    /**
     * Called whenever element value with name in parameter have been changed. Override if needed.
     * @memberof countlyManagementView
     */
    onChange: function(/* name */) { },

    /**
     * Called whenever element value with name in parameter have been changed.
     * @memberof countlyManagementView
     * @param {string} name - key
     * @param {string} value - value to set
     */
    doOnChange: function(name, value) {

        if (name && countlyCommon.dot(this.templateData, name) !== value) {
            countlyCommon.dot(this.templateData, name, value);
        }

        if (this.isSaveAvailable()) {
            this.el.find('.icon-button').show();
        }
        else {
            this.el.find('.icon-button').hide();
        }

        if (name) {
            this.onChange(name, value);
        }
    },

    /**
     * Save logic: validate, disable save button, submit to the server,
     * show loading dialog if it takes long enough, hide it when done, show error if any, enable save button.
     * @memberof countlyManagementView
     * @param {event} ev - event
     * @returns {object} error
     */
    save: function(ev) {
        ev.preventDefault();

        if (this.el.find('.icon-button').hasClass('disabled') || !this.isSaveAvailable()) {
            return;
        }

        var error = this.validate(), self = this;
        if (error) {
            return this.showError(error === true ? jQuery.i18n.map['management-applications.plugins.save.nothing'] : error);
        }

        this.el.find('.icon-button').addClass('disabled');

        this.prepare().then(function(data) {
            var dialog, timeout = setTimeout(function() {
                dialog = CountlyHelpers.loading(jQuery.i18n.map['management-applications.plugins.saving']);
            }, 300);

            $.ajax({
                type: "POST",
                url: countlyCommon.API_PARTS.apps.w + '/update/plugins',
                data: {
                    app_id: self.appId,
                    args: JSON.stringify(data)
                },
                dataType: "json",
                success: function(result) {
                    self.el.find('.icon-button').removeClass('disabled');
                    clearTimeout(timeout);
                    if (dialog) {
                        CountlyHelpers.removeDialog(dialog);
                    }
                    if (result.result === 'Nothing changed') {
                        CountlyHelpers.notify({type: 'warning', message: jQuery.i18n.map['management-applications.plugins.saved.nothing']});
                    }
                    else {
                        CountlyHelpers.notify({title: jQuery.i18n.map['management-applications.plugins.saved.title'], message: jQuery.i18n.map['management-applications.plugins.saved']});
                        if (!countlyGlobal.apps[result._id].plugins) {
                            countlyGlobal.apps[result._id].plugins = {};
                        }
                        self.savedTemplateData = JSON.stringify(self.templateData);
                        for (var k in result.plugins) {
                            countlyGlobal.apps[result._id].plugins[k] = result.plugins[k];
                        }
                        self.resetTemplateData();
                        self.render();
                    }
                    self.doOnChange();
                },
                error: function(resp) {
                    try {
                        resp = JSON.parse(resp.responseText);
                    }
                    catch (ignored) {
                        //ignored excep
                    }

                    self.el.find('.icon-button').removeClass('disabled');
                    clearTimeout(timeout);
                    if (dialog) {
                        CountlyHelpers.removeDialog(dialog);
                    }
                    self.showError(resp.result || jQuery.i18n.map['management-applications.plugins.error.server']);
                }
            });
        }, function(error1) {
            self.el.find('.icon-button').removeClass('disabled');
            self.showError(error1);
        });
    },

    beforeRender: function() {
        var self = this;
        if (this.templatePath && this.templatePath !== "") {
            return $.when(T.render(this.templatePath, function(src) {
                self.template = src;
            }));
        }
        else {
            return;
        }
    },

    render: function() { //backbone.js view render function
        if (!this.savedTemplateData) {
            this.savedTemplateData = JSON.stringify(this.templateData);
        }
        this.el.html(this.template(this.templateData));
        if (!this.el.find('.icon-button').length) {
            $('<a class="icon-button green" data-localize="management-applications.plugins.save" href="#"></a>').hide().appendTo(this.el);
        }

        var self = this;
        this.el.find('.cly-select').each(function(i, select) {
            $(select).off('click', '.item').on('click', '.item', function() {
                self.doOnChange($(select).data('name') || $(select).attr('id'), $(this).data('value'));
            });
        });

        this.el.find(' input[type=number]').off('input').on('input', function() {
            self.doOnChange($(this).attr('name') || $(this).attr('id'), parseFloat($(this).val()));
        });

        this.el.find('input[type=text], input[type=password]').off('input').on('input', function() {
            self.doOnChange($(this).attr('name') || $(this).attr('id'), $(this).val());
        });

        this.el.find('input[type=file]').off('change').on('change', function() {
            self.doOnChange($(this).attr('name') || $(this).attr('id'), $(this).val());
        });

        this.el.find('.on-off-switch input').on("change", function() {
            var isChecked = $(this).is(":checked"),
                attrID = $(this).attr("id");
            self.doOnChange(attrID, isChecked);
        });

        this.el.find('.icon-button').off('click').on('click', this.save.bind(this));
        if (this.isSaveAvailable()) {
            this.el.find('.icon-button').show();
        }
        else {
            this.el.find('.icon-button').hide();
        }

        app.localize();

        this.afterRender();

        return this;
    },
});

/**
* Drop class with embeded countly theme, use as any Drop class/instance
* @name CountlyDrop
* @global
* @namespace CountlyDrop
*/
var CountlyDrop = Drop.createContext({
    classPrefix: 'countly-drop',
});

var initializeOnce = _.once(function() {
    return $.when(countlyEvent.initialize()).then(function() { });
});

//redefine contains selector for jquery to be case insensitive
$.expr[":"].contains = $.expr.createPseudo(function(arg) {
    return function(elem) {
        return $(elem).text().toUpperCase().indexOf(arg.toUpperCase()) >= 0;
    };
});

/**
* Set menu items by restriction status, hiding empty menu-categories
* @name setMenuItems
* @global
*/
function setMenuItems() {
    // hide empty section headers
    var type = countlyCommon.ACTIVE_APP_ID && countlyGlobal.apps[countlyCommon.ACTIVE_APP_ID] && countlyGlobal.apps[countlyCommon.ACTIVE_APP_ID].type ? countlyGlobal.apps[countlyCommon.ACTIVE_APP_ID].type : "mobile";
    var categories = $('#' + type + '-type .menu-category');
    for (var j = 0; j < categories.length; j++) {
        var children = categories[j].children;
        var isEmpty = true;
        for (var k = 0; k < children.length; k++) {
            if (children[k].className.indexOf('restrict') === -1 && children[k].className.indexOf('item') !== -1) {
                isEmpty = false;
            }
        }
        if (isEmpty) {
            $(categories[j]).hide();
        }
        else {
            $(categories[j]).show();
        }
    }
}

/**
 * Main app instance of Backbone AppRouter used to control views and view change flow
 * @name app
 * @global
 * @instance
 * @namespace app
 */
var AppRouter = Backbone.Router.extend({
    routes: {
        "/": "dashboard",
        "*path": "main"
    },
    /**
    * View that is currently being displayed
    * @type {countlyView}
    * @instance
    * @memberof app
    */
    activeView: null, //current view
    dateToSelected: null, //date to selected from the date picker
    dateFromSelected: null, //date from selected from the date picker
    activeAppName: '',
    activeAppKey: '',
    _isFirstLoad: false, //to know if we are switching between two apps or just loading page
    refreshActiveView: 0, //refresh interval function reference
    _myRequests: {}, //save requests not connected with view to prevent calling the same if previous not finished yet.
    /**
    * Navigate to another view programmatically. If you need to change the view without user clicking anything, like redirect. You can do this using this method. This method is not define by countly but is direct method of AppRouter object in Backbone js
    * @name app#navigate
    * @function
    * @instance
    * @param {string} fragment - url path (hash part) where to redirect user
    * @param {boolean=} triggerRoute - to trigger route call, like initialize new view, etc. Default is false, so you may want to use false when redirecting to URL for your own same view where you are already, so no need to reload it
    * @memberof app
    * @example <caption>Redirect to url of the same view</caption>
    * //you are at #/manage/systemlogs
    * app.navigate("#/manage/systemlogs/query/{}");
    *
    * @example <caption>Redirect to url of other view</caption>
    * //you are at #/manage/systemlogs
    * app.navigate("#/crashes", true);
    */
    _removeUnfinishedRequests: function() {
        for (var url in this._myRequests) {
            for (var data in this._myRequests[url]) {
                //4 means done, less still in progress
                if (parseInt(this._myRequests[url][data].readyState) !== 4) {
                    this._myRequests[url][data].abort();
                }
            }
        }
        this._myRequests = {};
        if (this.activeView) {
            this.activeView._removeMyRequests();//remove requests for view(if not finished)
        }
    },
    switchApp: function(app_id, callback) {
        countlyCommon.setActiveApp(app_id);
        $("#active-app-name").text(countlyGlobal.apps[app_id].name);
        $("#active-app-name").attr('title', countlyGlobal.apps[app_id].name);
        $("#active-app-icon").css("background-image", "url('" + countlyGlobal.path + "appimages/" + app_id + ".png')");

        //removing requests saved in app
        app._removeUnfinishedRequests();
        if (app && app.activeView) {
            if (typeof callback === "function") {
                app.activeView.appChanged(function() {
                    app.onAppSwitch(app_id);
                    callback();
                });
            }
            else {
                app.activeView.appChanged(function() {
                    app.onAppSwitch(app_id);
                });
            }
        }
        else {
            if (typeof callback === "function") {
                callback();
            }
        }
    },
    _menuForTypes: {},
    _subMenuForTypes: {},
    _menuForAllTypes: [],
    _subMenuForAllTypes: [],
    _subMenuForCodes: {},
    _subMenus: {},
    _internalMenuCategories: ["management", "user"],
    /**
    * Add menu category. Categories will be copied for all app types and its visibility should be controled from the app type plugin
    * @memberof app
    * @param {string} category - new menu category
    * @param {Object} node - object defining category lement
    * @param {string} node.text - key for localization string which to use as text
    * @param {number} node.priority - priority order number, the less it is, the more on top category will be
    * @param {string} node.classes - string with css classes to add to category element
    * @param {string} node.style - string with css styling to add to category element
    * @param {string} node.html - additional HTML to append after text
    * @param {function} node.callback - called when and each time category is added passing same parameters as to this method plus added jquery category element as 3th param
    **/
    addMenuCategory: function(category, node) {
        if (this._internalMenuCategories.indexOf(category) !== -1) {
            throw "Category already exists with name: " + category;
        }
        if (typeof node.priority === "undefined") {
            throw "Provide priority property for category element";
        }
        var menu = $("<div></div>");
        menu.addClass("menu-category");
        menu.addClass(category + "-category");
        menu.attr("data-priority", node.priority);
        if (node.classes) {
            menu.addClass(node.classes);
        }
        if (node.style) {
            menu.attr("style", node.style);
        }
        menu.append('<span class="menu-category-title" data-localize="' + (node.text || "sidebar.category." + category) + '"></span>');
        if (node.html) {
            menu.append(node.html);
        }
        this._internalMenuCategories.push(category);
        var added = false;
        var selector = "#sidebar-menu .sidebar-menu";
        //try adding before first greater priority
        $(selector + " > div.menu-category").each(function() {
            var cur = parseInt($(this).attr("data-priority"));
            if (node.priority < cur) {
                added = true;
                $(this).before(menu);
                return false;
            }
        });

        //if not added, maybe we are first or last, so just add it
        if (!added) {
            $(selector).append(menu);
        }
        if (typeof node.callback === "function") {
            node.callback(category, node, menu);
        }
    },
    updateLongTaskViewsNofification: function(appChanged) {
        countlyTaskManager.getLastReports(function(data) {
            if (appChanged) {
                app.haveUnreadReports = false;
                $(".orange-side-notification-banner-wrapper").css("display", "none");
            }
            if (app.haveUnreadReports) {
                $("#manage-long-tasks-icon").addClass('unread');
            }
            else {
                $("#manage-long-tasks-icon").removeClass('unread');
            }

            var newHtml = "<table>";
            for (var k = 0; k < data.length; k++) {
                var color = "#E98010";
                if (data[k].status === "completed") {
                    color = "#2FA732";
                }
                if (data[k].status === "errored") {
                    color = "#D63E40";
                }
                var name = data[k].name || "";
                if (name.length > 30) {
                    name = name.substring(0, 30) + "...";
                }

                var trclass = "";
                var dataLink = '';
                if (data[k].status === "completed") {
                    name = name + '<i class="fa fa-chevron-right circle-arrow"></i>';
                    trclass = " class='completed'";
                    dataLink = ' data-link="' + data[k].view + data[k]._id + '" ';
                }
                newHtml = newHtml + "<tr" + trclass + dataLink + '><td><p class="title">' + name + '</p><p style="text-transform:capitalize">' + data[k].type + '<span>|</span>' + countlyCommon.formatTimeAgo(data[k].start) + '</p></td>' + '<td ><span class="status-color"><i class="fa fa-circle" style="color:' + color + ';"></i>' + data[k].status + '</span></td>';
            }
            newHtml += "<table>";

            if (data.length === 0) {
                $(".manage-long-tasks-menu .tasks-wrapper").html("<div class='graph-description' style='border:0'>" + jQuery.i18n.map["taskmanager.empty-warning"] + "</div>");
            }
            else {
                $(".manage-long-tasks-menu .tasks-wrapper").html("<div class='tasks'>" + newHtml + "</div>");
            }

            $(".manage-long-tasks-menu .tasks tr").on("click", function() {
                var link = $(this).data("link");
                if (link && link !== "") {
                    window.location.hash = link;
                }
            });

            if (data.length > 5) {
                $(".manage-long-tasks-menu .tasks-wrapper").css("height", "355px");
                $(".manage-long-tasks-menu .tasks").first().slimScroll({
                    height: '100%',
                    start: 'top',
                    wheelStep: 10,
                    position: 'right',
                    disableFadeOut: true
                });
            }
            else {
                $(".manage-long-tasks-menu .tasks-wrapper").css("height", "");
            }
        });
    },
    /**
    * Add first level menu element for specific app type under specified category. You can only add app type specific menu to categories "understand", "explore", "reach", "improve", "utilities"
    * @memberof app
    * @param {string} app_type - type of the app for which to add menu
    * @param {string} category - category under which to add menu: "understand", "explore", "reach", "improve", "utilities"
    * @param {Object} node - object defining menu lement
    * @param {string} node.text - key for localization string which to use as text
    * @param {string} node.code - code name for menu to reference for children, also assigned as id attribute with -menu postfix
    * @param {string} node.icon - HTML code for icon to show, usually a div element with font icon classes
    * @param {number} node.priority - priority order number, the less it is, the more on top menu will be
    * @param {string} node.url - url where menu points. Don't provide this, if it is upper menu and will contain children
    * @param {string} node.classes - string with css classes to add to menu element
    * @param {string} node.style - string with css styling to add to menu element
    * @param {string} node.html - additional HTML to append after text (use icon to append HTML before text)
    * @param {function} node.callback - called when and each time menu is added passing same parameters as to this method plus added jquery menu element as 4th param
    **/
    addMenuForType: function(app_type, category, node) {
        if (this._internalMenuCategories.indexOf(category) === -1) {
            throw "Wrong category for menu: " + category;
        }
        if (!node.text || !node.code || !node.icon || typeof node.priority === "undefined") {
            throw "Provide code, text, icon and priority properties for menu element";
        }
        if (!this.appTypes[app_type] && category !== "management" && category !== "users") {
            //app type not yet register, queue
            if (!this._menuForTypes[app_type]) {
                this._menuForTypes[app_type] = [];
            }
            this._menuForTypes[app_type].push({category: category, node: node});
            return;
        }
        //create menu element
        var menu = $("<a></a>");
        menu.addClass("item");
        menu.attr("data-priority", node.priority);
        menu.attr("id", node.code + "-menu");
        if (node.url) {
            menu.attr("href", node.url);
        }
        if (node.classes) {
            menu.addClass(node.classes);
        }
        if (node.style) {
            menu.attr("style", node.style);
        }
        menu.append(node.icon);
        menu.append('<div class="text" data-localize="' + node.text + '">' + (jQuery.i18n.map[node.text] || node.text) + '</div>');
        if (node.html) {
            menu.append(node.html);
        }

        if (!node.url && category !== "management" && category !== "users") {
            this._subMenus[node.code] = true;
            menu.hide();
            menu = menu.add('<div class="sidebar-submenu" id="' + node.code + '-submenu">');
        }
        var added = false;
        var selector = "#sidebar-menu #" + app_type + "-type ." + category + "-category";
        if (category === "management") {
            //different selector for management menu
            selector = ".right-menu #manage-menu";
        }
        else if (category === "users") {
            //different selector for users menu
            selector = ".right-menu #user-menu";
        }
        //try adding before first greater priority
        $(selector + " > a").each(function() {
            var cur = parseInt($(this).attr("data-priority"));
            if (node.priority < cur) {
                added = true;
                $(this).before(menu);
                return false;
            }
        });

        if (category === "management" && $(selector + " > a").length > 5) {
            $(selector).addClass("columns");
        }

        //if not added, maybe we are first or last, so just add it
        if (!added) {
            $(selector).append(menu);
        }

        if (typeof node.callback === "function") {
            node.callback(app_type, category, node, menu);
        }

        //run all queued submenus for this parent
        if (!node.url && category !== "management" && category !== "users" && this._subMenuForCodes[node.code]) {
            for (i = 0; i < this._subMenuForCodes[node.code].length; i++) {
                this.addSubMenuForType(this._subMenuForCodes[node.code][i].app_type, node.code, this._subMenuForCodes[node.code][i].node);
            }
            this._subMenuForCodes[node.code] = null;
        }
        setMenuItems();
    },
    /**
    * Add second level menu element for specific app type under specified parent code.
    * @memberof app
    * @param {string} app_type - type of the app for which to add menu
    * @param {string} parent_code - code for parent element under which to add this submenu element
    * @param {Object} node - object defining menu lement
    * @param {string} node.text - key for localization string which to use as text
    * @param {string} node.code - code name for menu to reference for children, also assigned as id attribute with -menu postfix
    * @param {number} node.priority - priority order number, the less it is, the more on top menu will be
    * @param {string} node.url - url where menu points. Don't provide this, if it is upper menu and will contain children
    * @param {string} node.classes - string with css classes to add to menu element
    * @param {string} node.style - string with css styling to add to menu element
    * @param {string} node.html - additional HTML to append after text (use icon to append HTML before text)
    * @param {function} node.callback - called when and each time menu is added passing same parameters as to this method plus added jquery menu element as 4th param
    **/
    addSubMenuForType: function(app_type, parent_code, node) {
        if (!parent_code) {
            throw "Provide code name for parent category";
        }
        if (!node.text || !node.code || !node.url || !node.priority) {
            throw "Provide text, code, url and priority for sub menu";
        }
        if (!this.appTypes[app_type]) {
            //app type not yet register, queue
            if (!this._subMenuForTypes[app_type]) {
                this._subMenuForTypes[app_type] = [];
            }
            this._subMenuForTypes[app_type].push({parent_code: parent_code, node: node});
            return;
        }
        if (!this._subMenus[parent_code]) {
            //parent not yet registered, queue
            if (!this._subMenuForCodes[parent_code]) {
                this._subMenuForCodes[parent_code] = [];
            }
            this._subMenuForCodes[parent_code].push({app_type: app_type, node: node});
            return;
        }

        //create menu element
        var menu = $("<a></a>");
        menu.addClass("item");
        menu.attr("data-priority", node.priority);
        menu.attr("id", node.code + "-menu");
        menu.attr("href", node.url);
        if (node.classes) {
            menu.addClass(node.classes);
        }
        if (node.style) {
            menu.attr("style", node.style);
        }
        menu.append('<div class="text" data-localize="' + node.text + '">' + (jQuery.i18n.map[node.text] || node.text) + '</div>');
        if (node.html) {
            menu.append(node.html);
        }
        var added = false;
        //try adding before first greater priority
        $("#sidebar-menu #" + app_type + "-type #" + parent_code + "-submenu > a").each(function() {
            var cur = parseInt($(this).attr("data-priority"));
            if (node.priority < cur) {
                added = true;
                $(this).before(menu);
                return false;
            }
        });

        //if not added, maybe we are first or last, so just add it
        if (!added) {
            $("#sidebar-menu #" + app_type + "-type #" + parent_code + "-submenu").append(menu);
        }

        if ($("#sidebar-menu #" + app_type + "-type #" + parent_code + "-submenu > a").length === 1) {
            $("#sidebar-menu #" + app_type + "-type #" + parent_code + "-menu").attr("href", node.url);
        }
        else {
            $("#sidebar-menu #" + app_type + "-type #" + parent_code + "-menu").removeAttr("href");
        }

        $("#sidebar-menu #" + app_type + "-type #" + parent_code + "-menu").css('display', 'block');

        if (typeof node.callback === "function") {
            node.callback(app_type, parent_code, node, menu);
        }

    },
    /**
    * Add first level menu element for all app types and special categories. 
    * @memberof app
    * @param {string} category - category under which to add menu: "understand", "explore", "reach", "improve", "utilities", "management", "user"
    * @param {Object} node - object defining menu lement
    * @param {string} node.text - key for localization string which to use as text
    * @param {string} node.code - code name for menu to reference for children, also assigned as id attribute with -menu postfix
    * @param {string} node.icon - HTML code for icon to show, usually a div element with font icon classes
    * @param {number} node.priority - priority order number, the less it is, the more on top menu will be
    * @param {string} node.url - url where menu points. Don't provide this, if it is upper menu and will contain children
    * @param {string} node.classes - string with css classes to add to menu element
    * @param {string} node.style - string with css styling to add to menu element
    * @param {string} node.html - additional HTML to append after text (use icon to append HTML before text)
    * @param {function} node.callback - called when and each time menu is added passing same parameters as to this method plus added jquery menu element as 4th param
    **/
    addMenu: function(category, node) {
        if (category === "management" || category === "users") {
            this.addMenuForType("default", category, node);
        }
        else {
            for (var type in this.appTypes) {
                this.addMenuForType(type, category, node);
            }
            //queue for future added app types
            this._menuForAllTypes.push({category: category, node: node});
        }
    },
    /**
    * Add second level sub menu element for all app types (not available for special categories as "management" and "user")
    * @memberof app
    * @param {string} parent_code - code for parent element under which to add this submenu element
    * @param {Object} node - object defining menu lement
    * @param {string} node.text - key for localization string which to use as text
    * @param {string} node.code - code name for menu to reference for children, also assigned as id attribute with -menu postfix
    * @param {string} node.icon - HTML code for icon to show, usually a div element with font icon classes
    * @param {number} node.priority - priority order number, the less it is, the more on top menu will be
    * @param {string} node.url - url where menu points. Don't provide this, if it is upper menu and will contain children
    * @param {string} node.classes - string with css classes to add to menu element
    * @param {string} node.style - string with css styling to add to menu element
    * @param {string} node.html - additional HTML to append after text (use icon to append HTML before text)
    * @param {function} node.callback - called when and each time menu is added passing same parameters as to this method plus added jquery menu element as 4th param
    **/
    addSubMenu: function(parent_code, node) {
        for (var type in this.appTypes) {
            this.addSubMenuForType(type, parent_code, node);
        }
        //queue for future added app types
        this._subMenuForAllTypes.push({parent_code: parent_code, node: node});
    },
    main: function(/*forced*/) {
        var change = true,
            redirect = false;
        // detect app switch like
        //#/app/586e32ddc32cb30a01558cc1/analytics/events
        if (Backbone.history.fragment.indexOf("/app/") === 0) {
            var app_id = Backbone.history.fragment.replace("/app/", "");
            redirect = "#/";
            if (app_id && app_id.length) {
                if (app_id.indexOf("/") !== -1) {
                    var parts = app_id.split("/");
                    app_id = parts.shift();
                    redirect = "#/" + parts.join("/");
                }
                if (app_id !== countlyCommon.ACTIVE_APP_ID && countlyGlobal.apps[app_id]) {
                    app.switchApp(app_id, function() {
                        app.navigate(redirect, true);
                    });
                    return;
                }
            }
        }
        else if (Backbone.history.fragment.indexOf("/0/") === 0 && countlyGlobal.apps[countlyCommon.ACTIVE_APP_ID]) {
            this.navigate("#/" + countlyCommon.ACTIVE_APP_ID + Backbone.history.fragment.replace("/0", ""), true);
            return;
        }
        else if (Backbone.history.fragment !== "/" && countlyGlobal.apps[countlyCommon.ACTIVE_APP_ID]) {
            $("#" + countlyGlobal.apps[countlyCommon.ACTIVE_APP_ID].type + "-type a").each(function() {
                if (this.hash !== "#/" && this.hash !== "") {
                    if ("#" + Backbone.history.fragment === this.hash && $(this).css('display') !== 'none') {
                        change = false;
                        return false;
                    }
                    else if (("#" + Backbone.history.fragment).indexOf(this.hash) === 0 && $(this).css('display') !== 'none') {
                        redirect = this.hash;
                        return false;
                    }
                }
            });
        }

        if (redirect) {
            app.navigate(redirect, true);
        }
        else if (change) {
            if (Backbone.history.fragment !== "/") {
                this.navigate("#/", true);
            }
            else if (countlyCommon.APP_NAMESPACE !== false) {
                this.navigate("#/" + countlyCommon.ACTIVE_APP_ID + Backbone.history.fragment, true);
            }
            else {
                this.dashboard();
            }
        }
        else {
            if (countlyCommon.APP_NAMESPACE !== false) {
                this.navigate("#/" + countlyCommon.ACTIVE_APP_ID + Backbone.history.fragment, true);
            }
            else {
                this.activeView.render();
            }
        }
    },
    dashboard: function() {
        if (countlyGlobal.member.restrict && countlyGlobal.member.restrict.indexOf("#/") !== -1) {
            return;
        }
        if (_.isEmpty(countlyGlobal.apps)) {
            this.renderWhenReady(this.manageAppsView);
        }
        else if (typeof this.appTypes[countlyGlobal.apps[countlyCommon.ACTIVE_APP_ID].type] !== "undefined") {
            this.renderWhenReady(this.appTypes[countlyGlobal.apps[countlyCommon.ACTIVE_APP_ID].type]);
        }
        else {
            this.renderWhenReady(this.dashboardView);
        }
    },
    runRefreshScripts: function() {
        var i = 0;
        var l = 0;
        if (this.refreshScripts[Backbone.history.fragment]) {
            for (i = 0, l = this.refreshScripts[Backbone.history.fragment].length; i < l; i++) {
                this.refreshScripts[Backbone.history.fragment][i]();
            }
        }
        for (var k in this.refreshScripts) {
            if (k !== '#' && k.indexOf('#') !== -1 && Backbone.history.fragment.match("^" + k.replace(/#/g, '.*'))) {
                for (i = 0, l = this.refreshScripts[k].length; i < l; i++) {
                    this.refreshScripts[k][i]();
                }
            }
        }
        if (this.refreshScripts["#"]) {
            for (i = 0, l = this.refreshScripts["#"].length; i < l; i++) {
                this.refreshScripts["#"][i]();
            }
        }

    },
    performRefresh: function(self) {
        //refresh only if we are on current period
        if (countlyCommon.periodObj.periodContainsToday && self.activeView.isLoaded) {
            self.activeView.isLoaded = false;
            $.when(self.activeView.refresh()).always(function() {
                self.activeView.isLoaded = true;
                self.runRefreshScripts();
            });
        }
    },
    renderWhenReady: function(viewName) { //all view renders end up here
        // If there is an active view call its destroy function to perform cleanups before a new view renders

        if (this.activeView) {
            this.activeView._removeMyRequests();
            this.activeView.destroy();
        }

        if (window.components && window.components.slider && window.components.slider.instance) {
            window.components.slider.instance.close();
        }

        this.activeView = viewName;

        clearInterval(this.refreshActiveView);
        if (typeof countlyGlobal.member.password_changed === "undefined") {
            countlyGlobal.member.password_changed = Math.round(new Date().getTime() / 1000);
        }
        this.routesHit++;

        if (_.isEmpty(countlyGlobal.apps)) {
            if (Backbone.history.fragment !== "/manage/apps") {
                this.navigate("/manage/apps", true);
            }
            else {
                viewName.render();
            }
            return false;
        }
        else if ((countlyGlobal.security.password_expiration > 0) &&
                (countlyGlobal.member.password_changed + countlyGlobal.security.password_expiration * 24 * 60 * 60 < new Date().getTime() / 1000) &&
                (!countlyGlobal.ssr)) {
            if (Backbone.history.fragment !== "/manage/user-settings/reset") {
                this.navigate("/manage/user-settings/reset", true);
            }
            else {
                viewName.render();
            }
            return false;
        }
        viewName.render();
        var self = this;
        this.refreshActiveView = setInterval(function() {
            self.performRefresh(self);
        }, countlyCommon.DASHBOARD_REFRESH_MS);

        if (countlyGlobal && countlyGlobal.message) {
            CountlyHelpers.parseAndShowMsg(countlyGlobal.message);
        }

        // Init sidebar based on the current url
        self.sidebar.init();
    },
    sidebar: {
        init: function() {
            setTimeout(function() {
                $("#sidebar-menu").find(".item").removeClass("active menu-active");
                $("#sidebar-menu").find(".menu-category-title").removeClass("active");
                var selectedMenu = $($("#sidebar-menu").find("a[href='#" + Backbone.history.fragment + "']"));

                if (!selectedMenu.length) {
                    var parts = Backbone.history.fragment.split("/");
                    selectedMenu = $($("#sidebar-menu").find("a[href='#/" + (parts[1] || "") + "']"));
                    if (!selectedMenu.length) {
                        selectedMenu = $($("#sidebar-menu").find("a[href='#/" + (parts[1] + "/" + parts[2] || "") + "']"));
                    }
                }

                var selectedSubmenu = selectedMenu.parents(".sidebar-submenu");

                if (selectedSubmenu.length) {
                    selectedMenu.addClass("active");
                    selectedSubmenu.prev().addClass("active menu-active");
                    app.sidebar.submenu.toggle(selectedSubmenu);
                }
                else {
                    selectedMenu.addClass("active");
                    app.sidebar.submenu.toggle();
                }

                var selectedCategory = selectedMenu.parents(".menu-category");
                if (selectedCategory.length) {
                    selectedCategory.find(".menu-category-title").addClass("active");
                }

                setMenuItems();
            }, 1000);
        },
        submenu: {
            toggle: function(el) {
                $(".sidebar-submenu").removeClass("half-visible");

                if (!el) {
                    $(".sidebar-submenu:visible").animate({ "right": "-170px" }, {
                        duration: 300,
                        easing: 'easeOutExpo',
                        complete: function() {
                            $(this).hide();
                        }
                    });
                    return true;
                }

                if (!el.is(":visible")) {
                    if ($(".sidebar-submenu").is(":visible")) {
                        $(".sidebar-submenu").hide();
                        el.css({ "right": "-110px" }).show().animate({ "right": "0" }, { duration: 300, easing: 'easeOutExpo' });
                        addText();
                    }
                    else {
                        el.css({ "right": "-170px" }).show().animate({ "right": "0" }, { duration: 300, easing: 'easeOutExpo' });
                        addText();
                    }
                }
                /** function add text to menu title */
                function addText() {
                    var mainMenuText = $(el.prev()[0]).find(".text").text();

                    $(".menu-title").remove();
                    var menuTitle = $("<div class='menu-title'></div>").text(mainMenuText).prepend("<i class='submenu-close ion-close'></i>");
                    el.prepend(menuTitle);

                    // Try setting submenu title once again if it was empty
                    // during previous try
                    if (!mainMenuText) {
                        setTimeout(function() {
                            $(".menu-title").text($(el.prev()[0]).find(".text").text());
                            $(".menu-title").prepend("<i class='submenu-close ion-close'></i>");
                        }, 1000);
                    }
                }
            }
        }
    },

    hasRoutingHistory: function() {
        if (this.routesHit > 1) {
            return true;
        }
        return false;
    },
    back: function(fallback_route) {
        if (this.routesHit > 1) {
            window.history.back();
        }
        else {
            var fragment = Backbone.history.getFragment();
            //route not passed, try  to guess from current location
            if (typeof fallback_route === "undefined" || fallback_route === "") {
                if (fragment) {
                    var parts = fragment.split("/");
                    if (parts.length > 1) {
                        fallback_route = "/" + parts[1];
                    }
                }
            }
            if (fallback_route === fragment) {
                fallback_route = '/';
            }
            this.navigate(fallback_route || '/', {trigger: true, replace: true});
        }
    },
    initialize: function() { //initialize the dashboard, register helpers etc.

        this.bind("route", function(name/*, args*/) {
            $('#content').removeClass(function(index, className) {
                return (className.match(/(^|\s)routename-\S*/g) || []).join(' ');
            }).addClass("routename-" + name);
        });

        this.appTypes = {};
        this.pageScripts = {};
        this.dataExports = {};
        this.appSwitchCallbacks = [];
        this.appManagementSwitchCallbacks = [];
        this.appObjectModificators = [];
        this.appManagementViews = {};
        this.appAddTypeCallbacks = [];
        this.userEditCallbacks = [];
        this.refreshScripts = {};
        this.appSettings = {};
        this.widgetCallbacks = {};
        var self = this;
        $(document).ready(function() {
            /**
            * Add menus
            **/
            self.addMenuCategory("understand", {priority: 10});
            self.addMenuCategory("explore", {priority: 20});
            self.addMenuCategory("reach", {priority: 30});
            self.addMenuCategory("improve", {priority: 40});
            self.addMenuCategory("utilities", {priority: 50});
            self.addMenu("understand", {code: "overview", url: "#/", text: "sidebar.dashboard", icon: '<div class="logo dashboard ion-speedometer"></div>', priority: 10});
            self.addMenu("understand", {code: "analytics", text: "sidebar.analytics", icon: '<div class="logo analytics ion-ios-pulse-strong"></div>', priority: 20});
            self.addMenu("understand", {code: "engagement", text: "sidebar.engagement", icon: '<div class="logo ion-happy-outline"></div>', priority: 30});
            self.addMenu("understand", {code: "events", text: "sidebar.events", icon: '<div class="logo events"><i class="material-icons">bubble_chart</i></div>', priority: 40});
            self.addSubMenu("events", {code: "events-overview", url: "#/analytics/events/overview", text: "sidebar.events.overview", priority: 10});
            self.addSubMenu("events", {code: "all-events", url: "#/analytics/events", text: "sidebar.events.all-events", priority: 20});
            if (countlyGlobal.member.global_admin || (countlyGlobal.admin_apps && Object.keys(countlyGlobal.admin_apps).length)) {
                self.addSubMenu("events", {code: "manage-events", url: "#/analytics/manage-events", text: "sidebar.events.blueprint", priority: 100});
            }

            self.addMenu("utilities", {
                code: "management",
                text: "sidebar.utilities",
                icon: '<div class="logo management ion-wrench"></div>',
                priority: 10000000,
                callback: function(type, category, node, menu) {
                //for backwards compatability of old plugins adding menu to management
                    menu.filter("#management-submenu").append("<span class='help-toggle'></span>");
                }
            });

            self.addSubMenu("management", {code: "longtasks", url: "#/manage/tasks", text: "sidebar.management.longtasks", priority: 10});

            var jobsIconSvg = '<svg width="20px" height="16px" viewBox="0 0 12 10" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><title>list-24px 2</title><g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd"><g id="list-24px-2" fill="#9f9f9f" fill-rule="nonzero"><g id="list-24px"><path d="M0,6 L2,6 L2,4 L0,4 L0,6 Z M0,10 L2,10 L2,8 L0,8 L0,10 Z M0,2 L2,2 L2,0 L0,0 L0,2 Z M3,6 L12,6 L12,4 L3,4 L3,6 Z M3,10 L12,10 L12,8 L3,8 L3,10 Z M3,0 L3,2 L12,2 L12,0 L3,0 Z" id="Shape"></path></g></g></g></svg>';
            if (countlyGlobal.member.global_admin || (countlyGlobal.admin_apps && Object.keys(countlyGlobal.admin_apps).length)) {
                self.addMenu("management", {code: "applications", url: "#/manage/apps", text: "sidebar.management.applications", icon: '<div class="logo-icon ion-ios-albums"></div>', priority: 10});
            }
            if (countlyGlobal.member.global_admin) {
                self.addMenu("management", {code: "users", url: "#/manage/users", text: "sidebar.management.users", icon: '<div class="logo-icon fa fa-user-friends"></div>', priority: 20});
            }
            if (countlyGlobal.member.global_admin) {
                self.addMenu("management", {code: "jobs", url: "#/manage/jobs", text: "sidebar.management.jobs", icon: '<div class="logo-icon">' + jobsIconSvg + '</div>', priority: 20});
            }

            self.addMenu("management", {code: "help", text: "sidebar.management.help", icon: '<div class="logo-icon ion-help help"></div>', classes: "help-toggle", html: '<div class="on-off-switch" id="help-toggle"><input type="checkbox" class="on-off-switch-checkbox" id="help-toggle-cbox"><label class="on-off-switch-label" for="help-toggle-cbox"></label></div>', priority: 10000000});

            self.addMenu("explore", {code: "users", text: "sidebar.analytics.users", icon: '<div class="logo ion-person-stalker"></div>', priority: 10});
            self.addMenu("explore", {code: "behavior", text: "sidebar.behavior", icon: '<div class="logo ion-funnel"></div>', priority: 20});
            Backbone.history.checkUrl();

            $('.list .item_info').on("click", function(e) {
                e.stopPropagation();
            });
        });

        this.routesHit = 0; //keep count of number of routes handled by your application
        /**
        * When rendering data from server using templates from frontend/express/views we are using ejs as templating engine. But when rendering templates on the browser side remotely loaded templates through ajax, we are using Handlebars templating engine. While in ejs everything is simple and your templating code is basically javascript code betwee <% %> tags. Then with Handlebars it is not that straightforward and we need helper functions to have some common templating logic
        * @name Handlebars
        * @global
        * @instance
        * @namespace Handlebars
        */

        /**
        * Display common date selecting UI elements
        * @name date-selector
        * @memberof Handlebars
        * @example
        * {{> date-selector }}
        */
        Handlebars.registerPartial("date-selector", $("#template-date-selector").html());
        /**
        * Get id value from ObjectID string
        * @name getIdValue
        * @memberof Handlebars
        * @example
        * <span>{{#clearObjectId value}}{{/clearObjectId}}</span>
        */
        Handlebars.registerHelper('clearObjectId', function(object) {
            if (object) {
                var id = object._id;
                if (typeof id === "string") {
                    if (id.substr(0, 3) === "Obj") {
                        id = id.split("(")[1].split(")")[0];
                    }
                    return id;
                }
                else {
                    return "";
                }
            }
            else {
                return '';
            }
        });
        /**
        * Display common date time selecting UI elements
        * @name date-time-selector
        * @memberof Handlebars
        * @example
        * {{> date-time-selector }}
        */
        Handlebars.registerPartial("date-time-selector", $("#template-date-time-selector").html());
        /**
        * Display common timezone selecting UI element
        * @name timezones
        * @memberof Handlebars
        * @example
        * {{> timezones }}
        */
        Handlebars.registerPartial("timezones", $("#template-timezones").html());
        /**
        * Display common app category selecting UI element
        * @name app-categories
        * @memberof Handlebars
        * @example
        * {{> app-categories }}
        */
        Handlebars.registerPartial("app-categories", $("#template-app-categories").html());
        /**
        * Iterate object with keys and values, creating variable "property" for object key and variable "value" for object value
        * @name eachOfObject
        * @memberof Handlebars
        * @example
        * {{#eachOfObject app_types}}
        *   <div data-value="{{property}}" class="item">{{value}}</div>
        * {{/eachOfObject}}
        */
        Handlebars.registerHelper('eachOfObject', function(context, options) {
            var ret = "";
            for (var prop in context) {
                ret = ret + options.fn({ property: prop, value: context[prop] });
            }
            return ret;
        });
        /**
        * Iterate only values of object, this will reference the value of current object
        * @name eachOfObjectValue
        * @memberof Handlebars
        * @example
        * {{#eachOfObjectValue apps}}
		* <div class="app searchable">
		* 	<div class="image" style="background-image: url('/appimages/{{this._id}}.png');"></div>
		* 	<div class="name">{{this.name}}</div>
		* 	<input class="app_id" type="hidden" value="{{this._id}}"/>
		* </div>
		* {{/eachOfObjectValue}}
        */
        Handlebars.registerHelper('eachOfObjectValue', function(context, options) {
            var ret = "";
            for (var prop in context) {
                ret = ret + options.fn(context[prop]);
            }
            return ret;
        });
        /**
        * Iterate through array, creating variable "index" for element index and variable "value" for value at that index
        * @name eachOfArray
        * @memberof Handlebars
        * @example
        * {{#eachOfArray events}}
		* <div class="searchable event-container {{#if value.is_active}}active{{/if}}" data-key="{{value.key}}">
		* 	<div class="name">{{value.name}}</div>
		* </div>
		* {{/eachOfArray}}
        */
        Handlebars.registerHelper('eachOfArray', function(context, options) {
            var ret = "";
            for (var i = 0; i < context.length; i++) {
                ret = ret + options.fn({ index: i, value: context[i] });
            }
            return ret;
        });
        /**
        * Print out json in pretty indented way
        * @name prettyJSON
        * @memberof Handlebars
        * @example
        * <td class="jh-value jh-object-value">{{prettyJSON value}}</td>
        */
        Handlebars.registerHelper('prettyJSON', function(context) {
            return JSON.stringify(context, undefined, 4);
        });
        /**
        * Shorten number, Handlebar binding to {@link countlyCommon.getShortNumber}
        * @name getShortNumber
        * @memberof Handlebars
        * @example
        * <span class="value">{{getShortNumber this.data.total}}</span>
        */
        Handlebars.registerHelper('getShortNumber', function(context) {
            return countlyCommon.getShortNumber(context);
        });
        /**
        * Format float number up to 2 values after dot
        * @name getFormattedNumber
        * @memberof Handlebars
        * @example
        * <div class="number">{{getFormattedNumber this.total}}</div>
        */
        Handlebars.registerHelper('getFormattedNumber', function(context) {
            if (isNaN(context)) {
                return context;
            }

            var ret = parseFloat((parseFloat(context).toFixed(2)).toString()).toString();
            return ret.replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,");
        });
        /**
        * Convert text to upper case
        * @name toUpperCase
        * @memberof Handlebars
        * @example
        * <div class="title">{{toUpperCase page-title}}</div>
        */
        Handlebars.registerHelper('toUpperCase', function(context) {
            return context.toUpperCase();
        });
        /**
        * Convert array of app ids to comma separate string of app names. Handlebar binding to {@link CountlyHelpers.appIdsToNames}
        * @name appIdsToNames
        * @memberof Handlebars
        * @example
        * <div class="apps">{{appIdsToNames appIds}}</div>
        */
        Handlebars.registerHelper('appIdsToNames', function(context) {
            return CountlyHelpers.appIdsToNames(context);
        });
        /**
        * Loop for specified amount of times. Creating variable "count" as current index from 1 to provided value
        * @name forNumberOfTimes
        * @memberof Handlebars
        * @example
        * <ul>
        * {{#forNumberOfTimes 10}}
		*   <li>{{count}}</li>
		* {{/forNumberOfTimes}}
        * </ul>
        */
        Handlebars.registerHelper('forNumberOfTimes', function(context, options) {
            var ret = "";
            for (var i = 0; i < context; i++) {
                ret = ret + options.fn({ count: i + 1 });
            }
            return ret;
        });
        /**
        * Loop for specified amount of times. with variable "need" & "now", loop time will be ${need} - ${now}
        * @name forNumberOfTimes
        * @memberof Handlebars
        * @example
        * <ul>
        * {{#forNumberOfTimes 10 3}}  // will loop 7 times
		*   <li>{{count}}</li>
		* {{/forNumberOfTimes}}
        * </ul>
        */

        Handlebars.registerHelper('forNumberOfTimesCalc', function(need, now, options) {
            var ret = "";
            var context = parseInt(need) - parseInt(now) ;
            for (var i = 0; i < context; i++) {
                ret = ret + options.fn({ count: i + 1 });
            }
            return ret;
        });
        /**
        * Replaces part of a string with a string.
        * @name replace
        * @memberof Handlebars
        * @example
        * <span>{{#replace value "(" " ("}}{{/replace}}</span>
		*/
        Handlebars.registerHelper('replace', function(string, to_replace, replacement) {
            return (string || '').replace(to_replace, replacement);
        });
        /**
        * Limit string length.
        * @name limitString
        * @memberof Handlebars
        * @example
        * <span>{{#limitString value 15}}{{/limitString}}</span>
		*/
        Handlebars.registerHelper('limitString', function(string, limit) {
            if (string.length > limit) {
                return (string || '').substr(0, limit) + "..";
            }
            else {
                return string;
            }
        });
        Handlebars.registerHelper('include', function(templatename, options) {
            var partial = Handlebars.partials[templatename];
            var context = $.extend({}, this, options.hash);
            return partial(context);
        });
        /**
        * For loop in template providing start count, end count and increment
        * @name for
        * @memberof Handlebars
        * @example
        * {{#for start end 1}}
		* 	{{#ifCond this "==" ../data.curPage}}
		* 	<a href='#/manage/db/{{../../db}}/{{../../collection}}/page/{{this}}' class="current">{{this}}</a>
		* 	{{else}}
		* 	<a href='#/manage/db/{{../../db}}/{{../../collection}}/page/{{this}}'>{{this}}</a>
		* 	{{/ifCond}}
		* {{/for}}
        */
        Handlebars.registerHelper('for', function(from, to, incr, block) {
            var accum = '';
            for (var i = from; i < to; i += incr) {
                accum += block.fn(i);
            }
            return accum;
        });
        /**
        * If condition with different operators, accepting first value, operator and second value.
        * Accepted operators are ==, !=, ===, <, <=, >, >=, &&, ||
        * @name ifCond
        * @memberof Handlebars
        * @example
        * {{#ifCond this.data.trend "==" "u"}}
        *     <i class="material-icons">trending_up</i>
        * {{else}}
        *     <i class="material-icons">trending_down</i>
        * {{/ifCond}}
        */
        Handlebars.registerHelper('ifCond', function(v1, operator, v2, options) {
            switch (operator) {
            case '==':
                return (v1 == v2) ? options.fn(this) : options.inverse(this); // eslint-disable-line
            case '!=':
                return (v1 != v2) ? options.fn(this) : options.inverse(this); // eslint-disable-line
            case '!==':
                return (v1 !== v2) ? options.fn(this) : options.inverse(this);
            case '===':
                return (v1 === v2) ? options.fn(this) : options.inverse(this);
            case '<':
                return (v1 < v2) ? options.fn(this) : options.inverse(this);
            case '<=':
                return (v1 <= v2) ? options.fn(this) : options.inverse(this);
            case '>':
                return (v1 > v2) ? options.fn(this) : options.inverse(this);
            case '>=':
                return (v1 >= v2) ? options.fn(this) : options.inverse(this);
            case '&&':
                return (v1 && v2) ? options.fn(this) : options.inverse(this);
            case '||':
                return (v1 || v2) ? options.fn(this) : options.inverse(this);
            default:
                return options.inverse(this);
            }
        });
        /**
        * Format timestamp to twitter like time ago format, Handlebar binding to {@link countlyCommon.formatTimeAgo}
        * @name formatTimeAgo
        * @memberof Handlebars
        * @example
        * <div class="time">{{{formatTimeAgo value.time}}</div>
        */
        Handlebars.registerHelper('formatTimeAgo', function(context) {
            return countlyCommon.formatTimeAgo(parseInt(context) / 1000);
        });
        /**
        * Get value form object by specific key, this will reference value of the object
        * @name withItem
        * @memberof Handlebars
        * @example
        * <p>{{#withItem ../apps key=app_id}}{{this}}{{/withItem}}</p>
        */
        Handlebars.registerHelper('withItem', function(object, options) {
            return options.fn(object[options.hash.key]);
        });

        /**
        * Encode uri component
        * @name encodeURIComponent 
        * @memberof Handlebars
        * @example
        * <a href="/path/{{encodeURIComponent entity}}" </a>
        */
        Handlebars.registerHelper('encodeURIComponent', function(entity) {
            return encodeURIComponent(entity);
        });

        $("body").addClass("lang-" + countlyCommon.BROWSER_LANG_SHORT);
        jQuery.i18n.properties({
            name: window.production ? 'localization/min/locale' : ["localization/dashboard/dashboard", "localization/help/help", "localization/mail/mail"].concat(countlyGlobal.plugins.map(function(plugin) {
                return plugin + "/localization/" + plugin;
            })),
            cache: true,
            language: countlyCommon.BROWSER_LANG_SHORT,
            countlyVersion: countlyGlobal.countlyVersion + "&" + countlyGlobal.pluginsSHA,
            path: countlyGlobal.cdn,
            mode: 'map',
            callback: function() {
                for (var key in jQuery.i18n.map) {
                    if (countlyGlobal.company) {
                        jQuery.i18n.map[key] = jQuery.i18n.map[key].replace(new RegExp("Countly", 'ig'), countlyGlobal.company);
                    }
                    jQuery.i18n.map[key] = countlyCommon.encodeSomeHtml(jQuery.i18n.map[key]);
                }
                self.origLang = JSON.stringify(jQuery.i18n.map);
            }
        });

        $(document).ready(function() {

            CountlyHelpers.initializeSelect();
            CountlyHelpers.initializeTextSelect();
            CountlyHelpers.initializeMultiSelect();

            $(document).on('DOMNodeInserted', '.cly-select', function() {
                CountlyHelpers.makeSelectNative();
            });

            $.ajaxPrefilter(function(options) {
                var last5char = options.url.substring(options.url.length - 5, options.url.length);
                if (last5char === ".html") {
                    var version = countlyGlobal.countlyVersion || "";
                    options.url = options.url + "?v=" + version;
                }
            });
            var validateSession = function() {
                $.ajax({
                    url: countlyGlobal.path + "/session",
                    data: {check_session: true},
                    success: function(result) {
                        if (result === "logout") {
                            $("#user-logout").click();
                        }
                        if (result === "login") {
                            $("#user-logout").click();
                            window.location = "/login";
                        }
                        setTimeout(function() {
                            validateSession();
                        }, countlyCommon.DASHBOARD_VALIDATE_SESSION || 30000);
                    }
                });
            };
            setTimeout(function() {
                validateSession();
            }, countlyCommon.DASHBOARD_VALIDATE_SESSION || 30000);//validates session each 30 seconds
            if (parseInt(countlyGlobal.config.session_timeout)) {
                var minTimeout, tenSecondTimeout, logoutTimeout;
                var shouldRecordAction = false;
                var extendSession = function() {
                    shouldRecordAction = false;
                    $.ajax({
                        url: countlyGlobal.path + "/session",
                        success: function(result) {
                            if (result === "logout") {
                                $("#user-logout").click();
                            }
                            if (result === "login") {
                                $("#user-logout").click();
                                window.location = "/login";
                            }
                            else if (result === "success") {
                                shouldRecordAction = false;
                                var myTimeoutValue = parseInt(countlyGlobal.config.session_timeout) * 1000 * 60;
                                if (myTimeoutValue > 2147483647) { //max value used by set timeout function
                                    myTimeoutValue = 1800000;//30 minutes
                                }
                                setTimeout(function() {
                                    shouldRecordAction = true;
                                }, Math.round(myTimeoutValue / 2));
                                resetSessionTimeouts(myTimeoutValue);
                            }
                        },
                        error: function() {
                            shouldRecordAction = true;
                        }
                    });
                };

                var resetSessionTimeouts = function(timeout) {
                    var minute = timeout - 60 * 1000;
                    if (minTimeout) {
                        clearTimeout(minTimeout);
                        minTimeout = null;
                    }
                    if (minute > 0) {
                        minTimeout = setTimeout(function() {
                            CountlyHelpers.notify({ title: jQuery.i18n.map["common.session-expiration"], message: jQuery.i18n.map["common.expire-minute"], info: jQuery.i18n.map["common.click-to-login"] });
                        }, minute);
                    }
                    var tenSeconds = timeout - 10 * 1000;
                    if (tenSecondTimeout) {
                        clearTimeout(tenSecondTimeout);
                        tenSecondTimeout = null;
                    }
                    if (tenSeconds > 0) {
                        tenSecondTimeout = setTimeout(function() {
                            CountlyHelpers.notify({ title: jQuery.i18n.map["common.session-expiration"], message: jQuery.i18n.map["common.expire-seconds"], info: jQuery.i18n.map["common.click-to-login"] });
                        }, tenSeconds);
                    }
                    if (logoutTimeout) {
                        clearTimeout(logoutTimeout);
                        logoutTimeout = null;
                    }
                    logoutTimeout = setTimeout(function() {
                        extendSession();
                    }, timeout + 1000);
                };

                var myTimeoutValue = parseInt(countlyGlobal.config.session_timeout) * 1000 * 60;
                //max value used by set timeout function
                if (myTimeoutValue > 2147483647) {
                    myTimeoutValue = 1800000;
                }//30 minutes
                resetSessionTimeouts(myTimeoutValue);
                $(document).on("click mousemove extend-dashboard-user-session", function() {
                    if (shouldRecordAction) {
                        extendSession();
                    }
                });
                extendSession();
            }

            // If date range is selected initialize the calendar with these
            var periodObj = countlyCommon.getPeriod();
            if (Object.prototype.toString.call(periodObj) === '[object Array]' && periodObj.length === 2) {
                self.dateFromSelected = parseInt(periodObj[0], 10) + countlyCommon.getOffsetCorrectionForTimestamp(parseInt(periodObj[0], 10));
                self.dateToSelected = parseInt(periodObj[1], 10) + countlyCommon.getOffsetCorrectionForTimestamp(parseInt(periodObj[1], 10));
            }

            // Initialize localization related stuff

            // Localization test
            /*
             $.each(jQuery.i18n.map, function (key, value) {
             jQuery.i18n.map[key] = key;
             });
             */

            try {
                moment.locale(countlyCommon.BROWSER_LANG_SHORT);
            }
            catch (e) {
                moment.locale("en");
            }

            $(".reveal-language-menu").text(countlyCommon.BROWSER_LANG_SHORT.toUpperCase());

            $("#sidebar-events").click(function(e) {
                $.when(countlyEvent.refreshEvents()).then(function() {
                    if (countlyEvent.getEvents().length === 0) {
                        CountlyHelpers.alert(jQuery.i18n.map["events.no-event"], "black");
                        e.stopImmediatePropagation();
                        e.preventDefault();
                    }
                });
            });

            // SIDEBAR
            $("#sidebar-menu").on("click", ".submenu-close", function() {
                $(this).parents(".sidebar-submenu").animate({ "right": "-170px" }, {
                    duration: 200,
                    easing: 'easeInExpo',
                    complete: function() {
                        $(".sidebar-submenu").hide();
                        $("#sidebar-menu>.sidebar-menu>.menu-category>.item").removeClass("menu-active");
                    }
                });
            });

            $("#sidebar-menu").on("click", ".item", function() {
                if ($(this).hasClass("menu-active")) {
                    return true;
                }

                $("#sidebar-menu>.sidebar-menu>.menu-category>.item").removeClass("menu-active");

                var elNext = $(this).next();

                if (elNext.hasClass("sidebar-submenu")) {
                    $(this).addClass("menu-active");
                    self.sidebar.submenu.toggle(elNext);
                }
                else {
                    $("#sidebar-menu").find(".item").removeClass("active");
                    $(this).addClass("active");

                    var mainMenuItem = $(this).parent(".sidebar-submenu").prev(".item");

                    if (mainMenuItem.length) {
                        mainMenuItem.addClass("active menu-active");
                    }
                    else {
                        self.sidebar.submenu.toggle();
                    }
                }
            });

            $("#sidebar-menu").hoverIntent({
                over: function() {
                    var visibleSubmenu = $(".sidebar-submenu:visible");

                    if (!$(this).hasClass("menu-active") && $(".sidebar-submenu").is(":visible") && !visibleSubmenu.hasClass("half-visible")) {
                        visibleSubmenu.addClass("half-visible");
                        visibleSubmenu.animate({ "right": "-110px" }, { duration: 300, easing: 'easeOutExpo' });
                    }
                },
                out: function() { },
                selector: ".sidebar-menu>.menu-category>.item"
            });

            $("#sidebar-menu").hoverIntent({
                over: function() { },
                out: function() {
                    var visibleSubmenu = $(".sidebar-submenu:visible");

                    if ($(".sidebar-submenu").is(":visible") && visibleSubmenu.hasClass("half-visible")) {
                        visibleSubmenu.removeClass("half-visible");
                        visibleSubmenu.animate({ "right": "0" }, { duration: 300, easing: 'easeOutExpo' });
                    }
                },
                selector: ""
            });

            $("#sidebar-menu").hoverIntent({
                over: function() {
                    var visibleSubmenu = $(".sidebar-submenu:visible");

                    if (visibleSubmenu.hasClass("half-visible")) {
                        visibleSubmenu.removeClass("half-visible");
                        visibleSubmenu.animate({ "right": "0" }, { duration: 300, easing: 'easeOutExpo' });
                    }
                },
                out: function() { },
                selector: ".sidebar-submenu:visible"
            });

            $('#sidebar-menu').slimScroll({
                height: ($(window).height()) + 'px',
                railVisible: true,
                railColor: '#4CC04F',
                railOpacity: .2,
                color: '#4CC04F',
                disableFadeOut: false,
            });
            $(window).resize(function() {
                $('#sidebar-menu').slimScroll({
                    height: ($(window).height()) + 'px'
                });
            });

            $(".sidebar-submenu").on("click", ".item", function() {
                if ($(this).hasClass("disabled")) {
                    return true;
                }

                $(".sidebar-submenu .item").removeClass("active");
                $(this).addClass("active");
                $(this).parent().prev(".item").addClass("active");
            });

            $("#language-menu .item").click(function() {
                var langCode = $(this).data("language-code"),
                    langCodeUpper = langCode.toUpperCase();

                store.set("countly_lang", langCode);
                $(".reveal-language-menu").text(langCodeUpper);

                countlyCommon.BROWSER_LANG_SHORT = langCode;
                countlyCommon.BROWSER_LANG = langCode;

                $("body").removeClass(function(index, className) {
                    return (className.match(/(^|\s)lang-\S*/g) || []).join(' ');
                }).addClass("lang-" + langCode);

                try {
                    moment.locale(countlyCommon.BROWSER_LANG_SHORT);
                }
                catch (e) {
                    moment.locale("en");
                }

                countlyCommon.getMonths(true);

                $("#date-to").datepicker("option", $.datepicker.regional[countlyCommon.BROWSER_LANG]);
                $("#date-from").datepicker("option", $.datepicker.regional[countlyCommon.BROWSER_LANG]);

                $.ajax({
                    type: "POST",
                    url: countlyGlobal.path + "/user/settings/lang",
                    data: {
                        "username": countlyGlobal.member.username,
                        "lang": countlyCommon.BROWSER_LANG_SHORT,
                        _csrf: countlyGlobal.csrf_token
                    },
                    success: function() { }
                });

                jQuery.i18n.properties({
                    name: window.production ? 'localization/min/locale' : ["localization/dashboard/dashboard", "localization/help/help", "localization/mail/mail"].concat(countlyGlobal.plugins.map(function(plugin) {
                        return plugin + "/localization/" + plugin;
                    })),
                    cache: true,
                    language: countlyCommon.BROWSER_LANG_SHORT,
                    countlyVersion: countlyGlobal.countlyVersion + "&" + countlyGlobal.pluginsSHA,
                    path: countlyGlobal.cdn,
                    mode: 'map',
                    callback: function() {
                        for (var key in jQuery.i18n.map) {
                            if (countlyGlobal.company) {
                                jQuery.i18n.map[key] = jQuery.i18n.map[key].replace(new RegExp("Countly", 'ig'), countlyGlobal.company);
                            }
                            jQuery.i18n.map[key] = countlyCommon.encodeSomeHtml(jQuery.i18n.map[key]);
                        }

                        self.origLang = JSON.stringify(jQuery.i18n.map);
                        $.when(countlyLocation.changeLanguage()).then(function() {
                            self.activeView.render();
                        });
                    }
                });
            });

            $(document).on('click', "#save-account-details:not(.disabled)", function() {
                var username = $(".dialog #username").val(),
                    old_pwd = $(".dialog #old_pwd").val(),
                    new_pwd = $(".dialog #new_pwd").val(),
                    re_new_pwd = $(".dialog #re_new_pwd").val(),
                    api_key = $(".dialog #api-key").val();

                if (new_pwd !== re_new_pwd) {
                    $(".dialog #settings-save-result").addClass("red").text(jQuery.i18n.map["user-settings.password-match"]);
                    return true;
                }

                $(this).addClass("disabled");

                $.ajax({
                    type: "POST",
                    url: countlyGlobal.path + "/user/settings",
                    data: {
                        "username": username,
                        "old_pwd": old_pwd,
                        "new_pwd": new_pwd,
                        "api_key": api_key,
                        _csrf: countlyGlobal.csrf_token
                    },
                    success: function(result) {
                        var saveResult = $(".dialog #settings-save-result");

                        if (result === "username-exists") {
                            saveResult.removeClass("green").addClass("red").text(jQuery.i18n.map["management-users.username.exists"]);
                        }
                        else if (!result) {
                            saveResult.removeClass("green").addClass("red").text(jQuery.i18n.map["user-settings.alert"]);
                        }
                        else {
                            saveResult.removeClass("red").addClass("green").text(jQuery.i18n.map["user-settings.success"]);
                            $(".dialog #old_pwd").val("");
                            $(".dialog #new_pwd").val("");
                            $(".dialog #re_new_pwd").val("");
                            $("#menu-username").text(username);
                            $("#user-api-key").val(api_key);
                            countlyGlobal.member.username = username;
                            countlyGlobal.member.api_key = api_key;
                        }

                        $(".dialog #save-account-details").removeClass("disabled");
                    }
                });
            });

            var help = _.once(function() {
                CountlyHelpers.alert(jQuery.i18n.map["help.help-mode-welcome"], "popStyleGreen popStyleGreenWide", {button_title: jQuery.i18n.map["common.okay"] + "!", title: jQuery.i18n.map["help.help-mode-welcome-title"], image: "welcome-to-help-mode"});
            });

            $("#help-menu").click(function(e) {
                e.stopPropagation();
                $("#help-toggle-cbox").prop("checked", !$("#help-toggle-cbox").prop("checked"));
                $("#help-toggle").toggleClass("active");

                app.tipsify($("#help-toggle").hasClass("active"));

                if ($("#help-toggle").hasClass("active")) {
                    help();
                    $.idleTimer('destroy');
                    clearInterval(self.refreshActiveView);
                }
                else {
                    self.refreshActiveView = setInterval(function() {
                        self.performRefresh(self);
                    }, countlyCommon.DASHBOARD_REFRESH_MS);
                    $.idleTimer(countlyCommon.DASHBOARD_IDLE_MS);
                }
            });

            $("#help-toggle").click(function(e) {
                e.stopPropagation();
                if ($(e.target).attr("id") === "help-toggle-cbox") {
                    $("#help-toggle").toggleClass("active");

                    app.tipsify($("#help-toggle").hasClass("active"));

                    if ($("#help-toggle").hasClass("active")) {
                        help();
                        $.idleTimer('destroy');
                        clearInterval(self.refreshActiveView);
                    }
                    else {
                        self.refreshActiveView = setInterval(function() {
                            self.performRefresh(self);
                        }, countlyCommon.DASHBOARD_REFRESH_MS);
                        $.idleTimer(countlyCommon.DASHBOARD_IDLE_MS);
                    }
                }
            });

            var logoutRequest = function() {
                var logoutForm = document.createElement("form");
                logoutForm.action = countlyGlobal.path + '/logout';
                logoutForm.method = "post";
                logoutForm.style.display = "none";
                logoutForm.type = "submit";

                var logoutForm_csrf = document.createElement("input");
                logoutForm_csrf.name = '_csrf';
                logoutForm_csrf.value = countlyGlobal.csrf_token;
                logoutForm.appendChild(logoutForm_csrf);
                document.body.appendChild(logoutForm);

                logoutForm.submit();
                document.body.removeChild(logoutForm);
            };

            $("#user-logout").click(function(e) {
                e.preventDefault();
                store.remove('countly_active_app');
                store.remove('countly_date');
                store.remove('countly_location_city');
                logoutRequest();
            });

            $(".beta-button").click(function() {
                CountlyHelpers.alert("This feature is currently in beta so the data you see in this view might change or disappear into thin air.<br/><br/>If you find any bugs or have suggestions please let us know!<br/><br/><a style='font-weight:500;'>Captain Obvious:</a> You can use the message box that appears when you click the question mark on the bottom right corner of this page.", "black");
            });

            $("#content").on("click", "#graph-note", function() {
                CountlyHelpers.popup("#graph-note-popup");

                $(".note-date:visible").datepicker({
                    numberOfMonths: 1,
                    showOtherMonths: true,
                    onSelect: function() {
                        dateText();
                    }
                });

                $.datepicker.setDefaults($.datepicker.regional[""]);
                $(".note-date:visible").datepicker("option", $.datepicker.regional[countlyCommon.BROWSER_LANG]);

                $('.note-popup:visible .time-picker, .note-popup:visible .note-list').slimScroll({
                    height: '100%',
                    start: 'top',
                    wheelStep: 10,
                    position: 'right',
                    disableFadeOut: true
                });

                $(".note-popup:visible .time-picker span").on("click", function() {
                    $(".note-popup:visible .time-picker span").removeClass("selected");
                    $(this).addClass("selected");
                    dateText();
                });


                $(".note-popup:visible .manage-notes-button").on("click", function() {
                    $(".note-popup:visible .note-create").hide();
                    $(".note-popup:visible .note-manage").show();
                    $(".note-popup:visible .create-note-button").show();
                    $(this).hide();
                    $(".note-popup:visible .create-note").hide();
                });

                $(".note-popup:visible .create-note-button").on("click", function() {
                    $(".note-popup:visible .note-create").show();
                    $(".note-popup:visible .note-manage").hide();
                    $(".note-popup:visible .manage-notes-button").show();
                    $(this).hide();
                    $(".note-popup:visible .create-note").show();
                });

                dateText();
                /** sets selected date text */
                function dateText() {
                    var selectedDate = $(".note-date:visible").val(),
                        instance = $(".note-date:visible").data("datepicker"),
                        date = $.datepicker.parseDate(instance.settings.dateFormat || $.datepicker._defaults.dateFormat, selectedDate, instance.settings);

                    $(".selected-date:visible").text(moment(date).format("D MMM YYYY") + ", " + $(".time-picker:visible span.selected").text());
                }

                if (countlyGlobal.apps[countlyCommon.ACTIVE_APP_ID] && countlyGlobal.apps[countlyCommon.ACTIVE_APP_ID].notes) {
                    var noteDateIds = _.sortBy(_.keys(countlyGlobal.apps[countlyCommon.ACTIVE_APP_ID].notes), function(el) {
                        return -parseInt(el);
                    });

                    for (var i = 0; i < noteDateIds.length; i++) {
                        var currNotes = countlyGlobal.apps[countlyCommon.ACTIVE_APP_ID].notes[noteDateIds[i]];

                        for (var j = 0; j < currNotes.length; j++) {
                            $(".note-popup:visible .note-list").append(
                                '<div class="note">' +
                                '<div class="date" data-dateid="' + noteDateIds[i] + '">' + moment(noteDateIds[i], "YYYYMMDDHH").format("D MMM YYYY, HH:mm") + '</div>' +
                                '<div class="content">' + currNotes[j] + '</div>' +
                                '<div class="delete-note"><i class="fa fa-trash"></i></div>' +
                                '</div>'
                            );
                        }
                    }
                }

                if (!$(".note-popup:visible .note").length) {
                    $(".note-popup:visible .manage-notes-button").hide();
                }

                $('.note-popup:visible .note-content').textcounter({
                    max: 50,
                    countDown: true,
                    countDownText: jQuery.i18n.map["dashboard.note-title-remaining"] + ": ",
                });

                $(".note-popup:visible .note .delete-note").on("click", function() {
                    var dateId = $(this).siblings(".date").data("dateid"),
                        note = $(this).siblings(".content").text();

                    $(this).parents(".note").fadeOut().remove();

                    $.ajax({
                        type: "POST",
                        url: countlyGlobal.path + '/graphnotes/delete',
                        data: {
                            "app_id": countlyCommon.ACTIVE_APP_ID,
                            "date_id": dateId,
                            "note": note,
                            _csrf: countlyGlobal.csrf_token
                        },
                        success: function(result) {
                            if (result === false) {
                                return false;
                            }
                            else {
                                updateGlobalNotes({ date_id: dateId, note: note }, "delete");
                                app.activeView.refresh();
                            }
                        }
                    });

                    if (!$(".note-popup:visible .note").length) {
                        $(".note-popup:visible .create-note-button").trigger("click");
                        $(".note-popup:visible .manage-notes-button").hide();
                    }
                });

                $(".note-popup:visible .create-note").on("click", function() {
                    if ($(this).hasClass("disabled")) {
                        return true;
                    }

                    $(this).addClass("disabled");

                    var selectedDate = $(".note-date:visible").val(),
                        instance = $(".note-date:visible").data("datepicker"),
                        date = $.datepicker.parseDate(instance.settings.dateFormat || $.datepicker._defaults.dateFormat, selectedDate, instance.settings),
                        dateId = moment(moment(date).format("D MMM YYYY") + ", " + $(".time-picker:visible span.selected").text(), "D MMM YYYY, HH:mm").format("YYYYMMDDHH"),
                        note = $(".note-popup:visible .note-content").val();

                    if (!note.length) {
                        $(".note-popup:visible .note-content").addClass("required-border");
                        $(this).removeClass("disabled");
                        return true;
                    }
                    else {
                        $(".note-popup:visible .note-content").removeClass("required-border");
                    }

                    $.ajax({
                        type: "POST",
                        url: countlyGlobal.path + '/graphnotes/create',
                        data: {
                            "app_id": countlyCommon.ACTIVE_APP_ID,
                            "date_id": dateId,
                            "note": note,
                            _csrf: countlyGlobal.csrf_token
                        },
                        success: function(result) {
                            if (result === false) {
                                return false;
                            }
                            else {
                                updateGlobalNotes({ date_id: dateId, note: result }, "create");
                                app.activeView.refresh();
                                app.recordEvent({
                                    "key": "graph-note",
                                    "count": 1,
                                    "segmentation": {}
                                });
                            }
                        }
                    });

                    $("#overlay").trigger("click");
                });
                /** function updates global notes
                * @param {object} noteObj - note object
                * @param {string} operation - create or delete
                */
                function updateGlobalNotes(noteObj, operation) {
                    var globalNotes = countlyGlobal.apps[countlyCommon.ACTIVE_APP_ID].notes;

                    if (operation === "create") {
                        if (globalNotes) {
                            if (globalNotes[noteObj.date_id]) {
                                countlyCommon.arrayAddUniq(globalNotes[noteObj.date_id], noteObj.note);
                            }
                            else {
                                globalNotes[noteObj.date_id] = [noteObj.note];
                            }
                        }
                        else {
                            var tmpNote = {};
                            tmpNote[noteObj.date_id] = [noteObj.note];

                            countlyGlobal.apps[countlyCommon.ACTIVE_APP_ID].notes = tmpNote;
                        }
                    }
                    else if (operation === "delete") {
                        if (globalNotes) {
                            if (globalNotes[noteObj.date_id]) {
                                globalNotes[noteObj.date_id] = _.without(globalNotes[noteObj.date_id], noteObj.note);
                            }
                        }
                    }
                }
            });

            // TOPBAR
            var $topbar = $("#top-bar"),
                $appNavigation = $("#app-navigation");

            $topbar.on("click", ".dropdown", function(e) {
                var wasActive = $(this).hasClass("clicked");
                if ($(this).hasClass('manage-long-tasks-menu-dropdown')) {
                    $("#manage-long-tasks-icon").removeClass('unread');
                    app.haveUnreadReports = false;
                    $(".orange-side-notification-banner-wrapper").css("display", "none");
                }
                $topbar.find(".dropdown").removeClass("clicked");

                if (wasActive) {
                    $(this).removeClass("clicked");
                }
                else {
                    $(this).find(".nav-search input").val("");
                    $(this).find(".list").scrollTop(0);
                    $(this).addClass("clicked");
                    var _this = $(this);
                    setTimeout(function() {
                        _this.find(".nav-search input").focus();
                    }, 50);
                }

                e.stopPropagation();
            });

            $topbar.on("click", ".dropdown .nav-search", function(e) {
                e.stopPropagation();
            });

            /**
             * Clear highlights class from app items
             * @param {array} filteredItems - filtered app items list
             */
            function clearHighlights(filteredItems) {
                var length = filteredItems.length;
                for (var i = 0; i < length; i++) {
                    $(filteredItems[i]).removeClass('highlighted-app-item');
                }
            }

            var arrowed = false;
            var currentIndex;
            $('#app-navigation').on('keyup', '.nav-search input', function(e) {
                var code = (e.keyCode || e.which);
                var filteredItems = $('#app-navigation > div.menu > div.list > .filtered-app-item');
                var indexLimit = filteredItems.length;
                if (code === 38) {
                    clearHighlights(filteredItems);
                    if (!arrowed) {
                        arrowed = true;
                        currentIndex = indexLimit - 1;
                    }
                    else {
                        currentIndex = currentIndex - 1;
                        if (currentIndex === -1) {
                            currentIndex = indexLimit - 1;
                        }
                    }
                    $(filteredItems[currentIndex]).addClass('highlighted-app-item');
                }
                else if (code === 40) {
                    clearHighlights(filteredItems);
                    if (!arrowed) {
                        arrowed = true;
                        currentIndex = 0;
                    }
                    else {
                        currentIndex = currentIndex + 1;
                        if (currentIndex === indexLimit) {
                            currentIndex = 0;
                        }
                    }
                    $(filteredItems[currentIndex]).addClass('highlighted-app-item');
                }
                else if (code === 13) {
                    $('#app-navigation').removeClass('clicked');
                    var appKey = $(filteredItems[currentIndex]).data("key"),
                        appId = $(filteredItems[currentIndex]).data("id"),
                        appName = $(filteredItems[currentIndex]).find(".name").text(),
                        appImage = $(filteredItems[currentIndex]).find(".app-icon").css("background-image");

                    $("#active-app-icon").css("background-image", appImage);
                    $("#active-app-name").text(appName);
                    $("#active-app-name").attr('title', appName);

                    if (self.activeAppKey !== appKey) {
                        self.activeAppName = appName;
                        self.activeAppKey = appKey;
                        self.switchApp(appId);
                    }
                }
                else {
                    return;
                }
            });

            $topbar.on("click", ".dropdown .item", function(e) {
                $topbar.find(".dropdown").removeClass("clicked");
                e.stopPropagation();
            });

            $("body").on("click", function() {
                $topbar.find(".dropdown").removeClass("clicked");
            });

            $("#user_api_key_item").click(function() {
                $(this).find('input').first().select();
            });

            $topbar.on("click", "#hide-sidebar-button", function() {
                $("#hide-sidebar-button").toggleClass("active");
                var $analyticsMainView = $("#analytics-main-view");

                $analyticsMainView.find("#sidebar").toggleClass("hidden");
                $analyticsMainView.find("#content-container").toggleClass("cover-left");
            });

            // Prevent body scroll after list inside dropdown is scrolled till the end
            // Applies to any element that has prevent-body-scroll class as well
            $("document").on('DOMMouseScroll mousewheel', ".dropdown .list, .prevent-body-scroll", function(ev) {
                var $this = $(this),
                    scrollTop = this.scrollTop,
                    scrollHeight = this.scrollHeight,
                    height = $this.innerHeight(),
                    delta = ev.originalEvent.wheelDelta,
                    up = delta > 0;

                if (ev.target.className === 'item scrollable') {
                    return true;
                }

                var prevent = function() {
                    ev.stopPropagation();
                    ev.preventDefault();
                    ev.returnValue = false;
                    return false;
                };

                if (!up && -delta > scrollHeight - height - scrollTop) {
                    // Scrolling down, but this will take us past the bottom.
                    $this.scrollTop(scrollHeight);
                    return prevent();
                }
                else if (up && delta > scrollTop) {
                    // Scrolling up, but this will take us past the top.
                    $this.scrollTop(0);
                    return prevent();
                }
            }, {passive: false});

            $appNavigation.on("click", ".item", function() {
                var appKey = $(this).data("key"),
                    appId = $(this).data("id"),
                    appName = $(this).find(".name").text(),
                    appImage = $(this).find(".app-icon").css("background-image");

                $("#active-app-icon").css("background-image", appImage);
                $("#active-app-name").text(appName);
                $("#active-app-name").attr('title', appName);

                if (self.activeAppKey !== appKey) {
                    self.activeAppName = appName;
                    self.activeAppKey = appKey;
                    self.switchApp(appId);
                }
            });

            $appNavigation.on("click", function() {
                var appList = $(this).find(".list"),
                    apps = _.sortBy(countlyGlobal.apps, function(app) {
                        return (app.name + "").toLowerCase();
                    });

                appList.html("");

                for (var i = 0; i < apps.length; i++) {
                    var currApp = apps[i];

                    var app = $("<div></div>");
                    app.addClass("item searchable");
                    app.data("key", currApp.key);
                    app.data("id", currApp._id);

                    var appIcon = $("<div></div>");
                    appIcon.addClass("app-icon");
                    appIcon.css("background-image", "url(" + countlyGlobal.cdn + "appimages/" + currApp._id + ".png");

                    var appName = $("<div></div>");
                    appName.addClass("name");
                    appName.attr("title", currApp.name);
                    appName.text(currApp.name);

                    app.append(appIcon);
                    app.append(appName);

                    appList.append(app);
                }
            });
        });

        if (!_.isEmpty(countlyGlobal.apps)) {
            if (!countlyCommon.ACTIVE_APP_ID) {
                var activeApp = (countlyGlobal.member && countlyGlobal.member.active_app_id && countlyGlobal.apps[countlyGlobal.member.active_app_id])
                    ? countlyGlobal.apps[countlyGlobal.member.active_app_id]
                    : countlyGlobal.defaultApp;

                countlyCommon.setActiveApp(activeApp._id);
                self.activeAppName = activeApp.name;
                $('#active-app-name').html(activeApp.name);
                $('#active-app-name').attr('title', activeApp.name);
                $("#active-app-icon").css("background-image", "url('" + countlyGlobal.cdn + "appimages/" + countlyCommon.ACTIVE_APP_ID + ".png')");
            }
            else {
                $("#active-app-icon").css("background-image", "url('" + countlyGlobal.cdn + "appimages/" + countlyCommon.ACTIVE_APP_ID + ".png')");
                $("#active-app-name").text(countlyGlobal.apps[countlyCommon.ACTIVE_APP_ID].name);
                $('#active-app-name').attr('title', countlyGlobal.apps[countlyCommon.ACTIVE_APP_ID].name);
                self.activeAppName = countlyGlobal.apps[countlyCommon.ACTIVE_APP_ID].name;
            }
        }
        else {
            $("#new-install-overlay").show();
        }

        $.idleTimer(countlyCommon.DASHBOARD_IDLE_MS);

        $(document).bind("idle.idleTimer", function() {
            clearInterval(self.refreshActiveView);
        });

        $(document).bind("active.idleTimer", function() {
            self.activeView.restart();
            self.refreshActiveView = setInterval(function() {
                self.performRefresh(self);
            }, countlyCommon.DASHBOARD_REFRESH_MS);
        });

        $.fn.dataTableExt.oPagination.four_button = {
            "fnInit": function(oSettings, nPaging, fnCallbackDraw) {
                var nFirst = document.createElement('span');
                var nPrevious = document.createElement('span');
                var nNext = document.createElement('span');
                var nLast = document.createElement('span');

                nFirst.innerHTML = "<i class='fa fa-angle-double-left'></i>";
                nPrevious.innerHTML = "<i class='fa fa-angle-left'></i>";
                nNext.innerHTML = "<i class='fa fa-angle-right'></i>";
                nLast.innerHTML = "<i class='fa fa-angle-double-right'></i>";

                nFirst.className = "paginate_button first";
                nPrevious.className = "paginate_button previous";
                nNext.className = "paginate_button next";
                nLast.className = "paginate_button last";

                nPaging.appendChild(nFirst);
                nPaging.appendChild(nPrevious);
                nPaging.appendChild(nNext);
                nPaging.appendChild(nLast);

                $(nFirst).click(function() {
                    oSettings.oApi._fnPageChange(oSettings, "first");
                    fnCallbackDraw(oSettings);
                });

                $(nPrevious).click(function() {
                    oSettings.oApi._fnPageChange(oSettings, "previous");
                    fnCallbackDraw(oSettings);
                });

                $(nNext).click(function() {
                    oSettings.oApi._fnPageChange(oSettings, "next");
                    fnCallbackDraw(oSettings);
                });

                $(nLast).click(function() {
                    oSettings.oApi._fnPageChange(oSettings, "last");
                    fnCallbackDraw(oSettings);
                });

                $(nFirst).bind('selectstart', function() {
                    return false;
                });
                $(nPrevious).bind('selectstart', function() {
                    return false;
                });
                $(nNext).bind('selectstart', function() {
                    return false;
                });
                $(nLast).bind('selectstart', function() {
                    return false;
                });
            },

            "fnUpdate": function(oSettings /*,fnCallbackDraw*/) {
                if (!oSettings.aanFeatures.p) {
                    return;
                }

                var an = oSettings.aanFeatures.p;
                for (var i = 0, iLen = an.length; i < iLen; i++) {
                    var buttons = an[i].getElementsByTagName('span');
                    if (oSettings._iDisplayStart === 0) {
                        buttons[0].className = "paginate_disabled_previous";
                        buttons[1].className = "paginate_disabled_previous";
                    }
                    else {
                        buttons[0].className = "paginate_enabled_previous";
                        buttons[1].className = "paginate_enabled_previous";
                    }

                    if (oSettings.fnDisplayEnd() === oSettings.fnRecordsDisplay()) {
                        buttons[2].className = "paginate_disabled_next";
                        buttons[3].className = "paginate_disabled_next";
                    }
                    else {
                        buttons[2].className = "paginate_enabled_next";
                        buttons[3].className = "paginate_enabled_next";
                    }
                }
            }
        };

        $.fn.dataTableExt.oApi.fnStandingRedraw = function(oSettings) {
            if (oSettings.oFeatures.bServerSide === false) {
                var before = oSettings._iDisplayStart;

                oSettings.oApi._fnReDraw(oSettings);

                // iDisplayStart has been reset to zero - so lets change it back
                oSettings._iDisplayStart = before;
                oSettings.oApi._fnCalculateEnd(oSettings);
            }

            // draw the 'current' page
            oSettings.oApi._fnDraw(oSettings);
        };
        /** getCustomDateInt
        * @param {string} s - date string
        * @returns {number} number representating date
        */
        function getCustomDateInt(s) {
            if (s.indexOf("W") === 0) {
                s = s.replace(",", "");
                s = s.replace("W", "");
                dateParts = s.split(" ");
                return (parseInt(dateParts[0])) + parseInt(dateParts.pop() * 10000);
            }
            s = moment(s, countlyCommon.getDateFormat(countlyCommon.periodObj.dateString)).format(countlyCommon.periodObj.dateString);
            var dateParts = "";
            if (s.indexOf(":") !== -1) {
                if (s.indexOf(",") !== -1) {
                    s = s.replace(/,|:/g, "");
                    dateParts = s.split(" ");

                    return parseInt((countlyCommon.getMonths().indexOf(dateParts[1]) + 1) * 1000000) +
                        parseInt(dateParts[0]) * 10000 +
                        parseInt(dateParts[2]);
                }
                else {
                    return parseInt(s.replace(':', ''));
                }
            }
            else if (s.length === 3) {
                return countlyCommon.getMonths().indexOf(s) + 1;
            }
            else if (s.indexOf("W") === 0) {
                s = s.replace(",", "");
                s = s.replace("W", "");
                dateParts = s.split(" ");
                return (parseInt(dateParts[0])) + parseInt(dateParts.pop() * 10000);
            }
            else {
                s = s.replace(",", "");
                dateParts = s.split(" ");

                if (dateParts.length === 3) {
                    return (parseInt(dateParts[2]) * 10000) + parseInt((countlyCommon.getMonths().indexOf(dateParts[1]) + 1) * 100) + parseInt(dateParts[0]);
                }
                else {
                    if (dateParts[0].length === 3) {
                        return parseInt((countlyCommon.getMonths().indexOf(dateParts[0]) + 1) * 100) + parseInt(dateParts[1] * 10000);
                    }
                    else {
                        return parseInt((countlyCommon.getMonths().indexOf(dateParts[1]) + 1) * 100) + parseInt(dateParts[0]);
                    }
                }
            }
        }

        jQuery.fn.dataTableExt.oSort['customDate-asc'] = function(x, y) {
            x = getCustomDateInt(x);
            y = getCustomDateInt(y);

            return ((x < y) ? -1 : ((x > y) ? 1 : 0));
        };

        jQuery.fn.dataTableExt.oSort['customDate-desc'] = function(x, y) {
            x = getCustomDateInt(x);
            y = getCustomDateInt(y);

            return ((x < y) ? 1 : ((x > y) ? -1 : 0));
        };

        /** getDateRangeInt
        * @param {string} s - range string
        * @returns {number} number representing range
        */
        function getDateRangeInt(s) {
            s = s.split("-")[0];
            var mEnglish = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

            if (s.indexOf(":") !== -1) {
                var mName = (s.split(" ")[1]).split(",")[0];

                return s.replace(mName, parseInt(mEnglish.indexOf(mName))).replace(/[:, ]/g, "");
            }
            else {
                var parts = s.split(" ");
                if (parts.length > 1) {
                    return parseInt(mEnglish.indexOf(parts[1]) * 100) + parseInt(parts[0]);
                }
                else {
                    return parts[0].replace(/[><]/g, "");
                }
            }
        }

        jQuery.fn.dataTableExt.oSort['dateRange-asc'] = function(x, y) {
            x = getDateRangeInt(x);
            y = getDateRangeInt(y);

            return ((x < y) ? -1 : ((x > y) ? 1 : 0));
        };

        jQuery.fn.dataTableExt.oSort['dateRange-desc'] = function(x, y) {
            x = getDateRangeInt(x);
            y = getDateRangeInt(y);

            return ((x < y) ? 1 : ((x > y) ? -1 : 0));
        };

        jQuery.fn.dataTableExt.oSort['percent-asc'] = function(x, y) {
            x = parseFloat($("<a></a>").html(x).text().replace("%", ""));
            y = parseFloat($("<a></a>").html(y).text().replace("%", ""));

            return ((x < y) ? -1 : ((x > y) ? 1 : 0));
        };

        jQuery.fn.dataTableExt.oSort['percent-desc'] = function(x, y) {
            x = parseFloat($("<a></a>").html(x).text().replace("%", ""));
            y = parseFloat($("<a></a>").html(y).text().replace("%", ""));

            return ((x < y) ? 1 : ((x > y) ? -1 : 0));
        };

        jQuery.fn.dataTableExt.oSort['formatted-num-asc'] = function(x, y) {
            'use strict';

            // Define vars
            var a = [], b = [];

            // Match any character except: digits (0-9), dash (-), period (.), or backslash (/) and replace those characters with empty string.
            x = x.replace(/[^\d\-\.\/]/g, ''); // eslint-disable-line
            y = y.replace(/[^\d\-\.\/]/g, ''); // eslint-disable-line

            // Handle simple fractions
            if (x.indexOf('/') >= 0) {
                a = x.split("/");
                x = parseInt(a[0], 10) / parseInt(a[1], 10);
            }
            if (y.indexOf('/') >= 0) {
                b = y.split("/");
                y = parseInt(b[0], 10) / parseInt(b[1], 10);
            }

            return x - y;
        };

        jQuery.fn.dataTableExt.oSort['formatted-num-desc'] = function(x, y) {
            'use strict';

            // Define vars
            var a = [], b = [];

            // Match any character except: digits (0-9), dash (-), period (.), or backslash (/) and replace those characters with empty string.
            x = x.replace(/[^\d\-\.\/]/g, ''); // eslint-disable-line
            y = y.replace(/[^\d\-\.\/]/g, ''); // eslint-disable-line

            // Handle simple fractions
            if (x.indexOf('/') >= 0) {
                a = x.split("/");
                x = parseInt(a[0], 10) / parseInt(a[1], 10);
            }
            if (y.indexOf('/') >= 0) {
                b = y.split("/");
                y = parseInt(b[0], 10) / parseInt(b[1], 10);
            }

            return y - x;
        };

        jQuery.fn.dataTableExt.oSort['loyalty-asc'] = function(x, y) {
            x = countlySession.getLoyaltyIndex(x);
            y = countlySession.getLoyaltyIndex(y);

            return ((x < y) ? -1 : ((x > y) ? 1 : 0));
        };

        jQuery.fn.dataTableExt.oSort['loyalty-desc'] = function(x, y) {
            x = countlySession.getLoyaltyIndex(x);
            y = countlySession.getLoyaltyIndex(y);

            return ((x < y) ? 1 : ((x > y) ? -1 : 0));
        };

        jQuery.fn.dataTableExt.oSort['frequency-asc'] = function(x, y) {
            x = countlySession.getFrequencyIndex(x);
            y = countlySession.getFrequencyIndex(y);

            return ((x < y) ? -1 : ((x > y) ? 1 : 0));
        };

        jQuery.fn.dataTableExt.oSort['frequency-desc'] = function(x, y) {
            x = countlySession.getFrequencyIndex(x);
            y = countlySession.getFrequencyIndex(y);

            return ((x < y) ? 1 : ((x > y) ? -1 : 0));
        };

        jQuery.fn.dataTableExt.oSort['session-duration-asc'] = function(x, y) {
            x = countlySession.getDurationIndex(x);
            y = countlySession.getDurationIndex(y);

            return ((x < y) ? -1 : ((x > y) ? 1 : 0));
        };

        jQuery.fn.dataTableExt.oSort['session-duration-desc'] = function(x, y) {
            x = countlySession.getDurationIndex(x);
            y = countlySession.getDurationIndex(y);

            return ((x < y) ? 1 : ((x > y) ? -1 : 0));
        };

        jQuery.fn.dataTableExt.oSort['app_versions-asc'] = function(x, y) {
            return countlyCommon.compareVersions(x, y);
        };

        jQuery.fn.dataTableExt.oSort['app_versions-desc'] = function(x, y) {
            return countlyCommon.compareVersions(x, y);
        };

        jQuery.fn.dataTableExt.oSort['format-ago-asc'] = function(x, y) {
            return x - y;
        };

        jQuery.fn.dataTableExt.oSort['format-ago-desc'] = function(x, y) {
            return y - x;
        };
        /** saves current page
        * @param {object} dtable  - data table
        * @param {object} settings  -data table settings
        */
        function saveCurrentPage(dtable, settings) {
            var data = dtable.fnGetData();
            countlyCommon.dtSettings = countlyCommon.dtSettings || [];

            var previosTableStatus = countlyCommon.dtSettings.filter(function(item) {
                return (item.viewId === app.activeView.cid && item.selector === settings.sTableId);
            })[0];

            if (previosTableStatus) {
                previosTableStatus.dataLength = data.length;
                previosTableStatus.page = settings._iDisplayStart / settings._iDisplayLength;
            }
            else {
                countlyCommon.dtSettings.push({
                    viewId: app.activeView.cid,
                    selector: settings.sTableId,
                    dataLength: data.length,
                    page: settings._iDisplayStart / settings._iDisplayLength
                });
            }
        }
        /** sets current page
        * @param {object} dtable  - data table
        * @param {object} settings  -data table settings
        */
        function setCurrentPage(dtable, settings) {
            var tablePersistSettings = countlyCommon.dtSettings.filter(function(item) {
                return (item.viewId === app.activeView.cid && item.selector === settings.sTableId);
            })[0];

            if (tablePersistSettings && tablePersistSettings.dataLength === dtable.fnGetData().length) {
                dtable.fnPageChange(tablePersistSettings.page);
            }
        }
        /** gets page size
        * @param {object} dtable  - data table
        * @param {object} settings  -data table settings
        * @returns {boolean} states if dtable is in active view
        */
        function getPageSize(dtable, settings) {
            var pageSizeSettings = countlyCommon.getPersistentSettings().pageSizeSettings;
            if (!pageSizeSettings) {
                pageSizeSettings = [];
            }

            var tablePersistSettings = pageSizeSettings.filter(function(item) {
                return (item.viewId === app.activeView.cid && item.selector === settings.sTableId);
            })[0];

            var pageSize;

            if (tablePersistSettings && tablePersistSettings.pageSize) {
                pageSize = tablePersistSettings.pageSize;
            }
            else if (settings.oInit && settings.oInit.iDisplayLength) {
                pageSize = settings.oInit.iDisplayLength;
            }
            else {
                pageSize = settings.iDisplayLength || settings._iDisplayLength || 50;
            }

            return pageSize;
        }

        $.extend(true, $.fn.dataTable.defaults, {
            "sDom": '<"dataTable-top"lfpT>t<"dataTable-bottom"i>',
            "bAutoWidth": false,
            "bLengthChange": true,
            "bPaginate": true,
            "sPaginationType": "four_button",
            "iDisplayLength": 50,
            "bDestroy": true,
            "bDeferRender": true,
            "oLanguage": {
                "sZeroRecords": jQuery.i18n.map["common.table.no-data"],
                "sInfoEmpty": jQuery.i18n.map["common.table.no-data"],
                "sEmptyTable": jQuery.i18n.map["common.table.no-data"],
                "sInfo": jQuery.i18n.map["common.showing"],
                "sInfoFiltered": jQuery.i18n.map["common.filtered"],
                "sSearch": jQuery.i18n.map["common.search"],
                "sLengthMenu": jQuery.i18n.map["common.show-items"] + "<input type='number' id='dataTables_length_input'/>"
            },
            "fnInitComplete": function(oSettings) {
                var dtable = this;
                var saveHTML = "<div class='save-table-data' data-help='help.datatables-export'><i class='fa fa-download'></i></div>",
                    searchHTML = "<div class='search-table-data'><i class='fa fa-search'></i></div>",
                    tableWrapper = $("#" + oSettings.sTableId + "_wrapper");

                countlyCommon.dtSettings = countlyCommon.dtSettings || [];
                tableWrapper.bind('page', function(e, _oSettings) {
                    var dataTable = $(e.target).dataTable();
                    saveCurrentPage(dataTable, _oSettings);
                });

                tableWrapper.bind('init', function(e, _oSettings) {
                    var dataTable = $(e.target).dataTable();
                    if (_oSettings.oFeatures.bServerSide) {
                        setTimeout(function() {
                            setCurrentPage(dataTable, _oSettings);
                            oSettings.isInitFinished = true;
                            tableWrapper.show();
                        }, 0);
                    }
                    else {
                        setCurrentPage(dataTable, _oSettings);
                        oSettings.isInitFinished = true;
                        tableWrapper.show();
                    }
                });

                var selectButton = "<div class='select-column-table-data' style='display:none;'><p class='ion-gear-a'></p></div>";
                $(selectButton).insertBefore(tableWrapper.find(".dataTables_filter"));

                $(saveHTML).insertBefore(tableWrapper.find(".DTTT_container"));
                $(searchHTML).insertBefore(tableWrapper.find(".dataTables_filter"));
                tableWrapper.find(".dataTables_filter").html(tableWrapper.find(".dataTables_filter").find("input").attr("Placeholder", jQuery.i18n.map["common.search"]).clone(true));

                tableWrapper.find(".search-table-data").on("click", function() {
                    $(this).next(".dataTables_filter").toggle();
                    $(this).next(".dataTables_filter").find("input").focus();
                });

                tableWrapper.find(".dataTables_length").show();
                tableWrapper.find('#dataTables_length_input').bind('change.DT', function(/*e, _oSettings*/) {
                    //store.set("iDisplayLength", $(this).val());
                    if ($(this).val() && $(this).val().length > 0) {
                        var pageSizeSettings = countlyCommon.getPersistentSettings().pageSizeSettings;
                        if (!pageSizeSettings) {
                            pageSizeSettings = [];
                        }

                        var tableId = oSettings.sTableId;

                        if (!tableId) {
                            return;
                        }

                        var previosTableStatus = pageSizeSettings.filter(function(item) {
                            return (item.viewId === app.activeView.cid && item.selector === tableId);
                        })[0];

                        if (previosTableStatus) {
                            previosTableStatus.pageSize = parseInt($(this).val());
                        }
                        else {
                            pageSizeSettings.push({
                                viewId: app.activeView.cid,
                                selector: tableId,
                                pageSize: parseInt($(this).val())
                            });
                        }

                        countlyCommon.setPersistentSettings({ pageSizeSettings: pageSizeSettings });
                    }
                });
                var exportDrop;
                if (oSettings.oFeatures.bServerSide) {
                    //slowdown serverside filtering
                    tableWrapper.find('.dataTables_filter input').unbind();
                    var timeout = null;
                    tableWrapper.find('.dataTables_filter input').bind('keyup', function() {
                        var $this = this;
                        if (timeout) {
                            clearTimeout(timeout);
                            timeout = null;
                        }
                        timeout = setTimeout(function() {
                            oSettings.oInstance.fnFilter($this.value);
                        }, 1000);
                    });
                    var exportView = $(dtable).data("view") || "activeView";
                    var exportAPIData = app[exportView].getExportAPI ? app[exportView].getExportAPI(oSettings.sTableId) : null;
                    var exportQueryData = app[exportView].getExportQuery ? app[exportView].getExportQuery(oSettings.sTableId) : null;

                    if (exportAPIData || exportQueryData) {
                        //create export dialog
                        var position = 'left middle';
                        if (oSettings.oInstance && oSettings.oInstance.addColumnExportSelector === true) {
                            position = 'left top';
                        }

                        exportDrop = new CountlyDrop({
                            target: tableWrapper.find('.save-table-data')[0],
                            content: "",
                            position: position,
                            classes: "server-export",
                            constrainToScrollParent: false,
                            remove: true,
                            openOn: "click"
                        });
                        exportDrop.on("open", function() {
                            if (exportAPIData) {
                                $(".server-export .countly-drop-content").empty().append(CountlyHelpers.export(oSettings._iRecordsDisplay, app[exportView].getExportAPI(oSettings.sTableId), null, true, oSettings.oInstance).removeClass("dialog"));
                            }
                            else if (exportQueryData) {
                                $(".server-export .countly-drop-content").empty().append(CountlyHelpers.export(oSettings._iRecordsDisplay, app[exportView].getExportQuery(oSettings.sTableId), null, null, oSettings.oInstance).removeClass("dialog"));
                            }
                            exportDrop.position();
                        });
                    }
                    else {
                        // tableWrapper.find(".dataTables_length").hide();
                        //create export dialog
                        var item = tableWrapper.find('.save-table-data')[0];
                        if (item) {
                            exportDrop = new CountlyDrop({
                                target: tableWrapper.find('.save-table-data')[0],
                                content: "",
                                position: 'left middle',
                                classes: "server-export",
                                constrainToScrollParent: false,
                                remove: true,
                                openOn: "click"
                            });
                            exportDrop.on("open", function() {
                                $(".server-export .countly-drop-content").empty().append(CountlyHelpers.tableExport(dtable, { api_key: countlyGlobal.member.api_key }, null, oSettings).removeClass("dialog"));
                                exportDrop.position();
                            });
                        }
                    }
                }
                else {
                    // tableWrapper.find(".dataTables_length").hide();
                    //create export dialog
                    var item2 = tableWrapper.find('.save-table-data')[0];
                    if (item2) {
                        exportDrop = new CountlyDrop({
                            target: tableWrapper.find('.save-table-data')[0],
                            content: "",
                            position: 'right middle',
                            classes: "server-export",
                            constrainToScrollParent: false,
                            remove: true,
                            openOn: "click"
                        });

                        exportDrop.on("open", function() {
                            $(".server-export .countly-drop-content").empty().append(CountlyHelpers.tableExport(dtable, { api_key: countlyGlobal.member.api_key }).removeClass("dialog"));
                            exportDrop.position();
                        });
                    }
                }

                //tableWrapper.css({"min-height": tableWrapper.height()});
            },
            fnPreDrawCallback: function(oSettings) {
                var tableWrapper = $("#" + oSettings.sTableId + "_wrapper");

                if (oSettings.isInitFinished) {
                    tableWrapper.show();
                }
                else {
                    var dtable = $(oSettings.nTable).dataTable();
                    oSettings._iDisplayLength = getPageSize(dtable, oSettings);
                    $('.dataTables_length').find('input[type=number]').val(oSettings._iDisplayLength);
                    tableWrapper.hide();
                }

                if (tableWrapper.find(".table-placeholder").length === 0) {
                    var $placeholder = $('<div class="table-placeholder"><div class="top"></div><div class="header"></div></div>');
                    tableWrapper.append($placeholder);
                }

                if (tableWrapper.find(".table-loader").length === 0) {
                    tableWrapper.append("<div class='table-loader'></div>");
                }
            },
            fnDrawCallback: function(oSettings) {

                var tableWrapper = $("#" + oSettings.sTableId + "_wrapper");
                tableWrapper.find(".dataTable-bottom").show();
                tableWrapper.find(".table-placeholder").remove();
                tableWrapper.find(".table-loader").remove();
            }
        });

        $.fn.dataTableExt.sErrMode = 'throw';
        $(document).ready(function() {
            setTimeout(function() {
                self.onAppSwitch(countlyCommon.ACTIVE_APP_ID, true, true);
            }, 1);
        });
    },
    /**
    * Localize all found html elements with data-localize and data-help-localize attributes
    * @param {jquery_object} el - jquery reference to parent element which contents to localize, by default all document is localized if not provided
    * @memberof app
    */
    localize: function(el) {
        var helpers = {
            onlyFirstUpper: function(str) {
                return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
            },
            upper: function(str) {
                return str.toUpperCase();
            }
        };

        // translate help module
        (el ? el.find('[data-help-localize]') : $("[data-help-localize]")).each(function() {
            var elem = $(this);
            if (typeof elem.data("help-localize") !== "undefined") {
                elem.data("help", jQuery.i18n.map[elem.data("help-localize")]);
            }
        });

        // translate dashboard
        (el ? el.find('[data-localize]') : $("[data-localize]")).each(function() {
            var elem = $(this),
                toLocal = elem.data("localize").split("!"),
                localizedValue = "";

            if (toLocal.length === 2) {
                if (helpers[toLocal[0]]) {
                    localizedValue = helpers[toLocal[0]](jQuery.i18n.map[toLocal[1]]);
                }
                else {
                    localizedValue = jQuery.i18n.prop(toLocal[0], (toLocal[1]) ? jQuery.i18n.map[toLocal[1]] : "");
                }
            }
            else {
                localizedValue = jQuery.i18n.map[elem.data("localize")];
            }

            if (elem.is("input[type=text]") || elem.is("input[type=password]") || elem.is("textarea")) {
                elem.attr("placeholder", localizedValue);
            }
            else if (elem.is("input[type=button]") || elem.is("input[type=submit]")) {
                elem.attr("value", localizedValue);
            }
            else {
                elem.html(localizedValue);
            }
        });
    },
    /**
    * Toggle showing tooltips, which are usually used in help mode for all elements containing css class help-zone-vs or help-zone-vb and having data-help attributes (which are generated automatically from data-help-localize attributes upon localization)
    * @param {boolean} enable - if true tooltips will be shown on hover, if false tooltips will be disabled
    * @param {jquery_object} el - jquery reference to parent element which contents to check for tooltips, by default all document is checked if not provided
    * @memberof app
    * @instance
    */
    tipsify: function(enable, el) {
        var vs = el ? el.find('.help-zone-vs') : $('.help-zone-vs'),
            vb = el ? el.find('.help-zone-vb') : $('.help-zone-vb'),
            both = el ? el.find('.help-zone-vs, .help-zone-vb') : $(".help-zone-vs, .help-zone-vb");

        vb.tipsy({
            gravity: $.fn.tipsy.autoNS,
            trigger: 'manual',
            title: function() {
                return $(this).data("help") || "";
            },
            fade: true,
            offset: 5,
            cssClass: 'yellow',
            opacity: 1,
            html: true
        });
        vs.tipsy({
            gravity: $.fn.tipsy.autoNS,
            trigger: 'manual',
            title: function() {
                return $(this).data("help") || "";
            },
            fade: true,
            offset: 5,
            cssClass: 'yellow narrow',
            opacity: 1,
            html: true
        });

        if (enable) {
            both.off('mouseenter mouseleave')
                .on('mouseenter', function() {
                    $(this).tipsy("show");
                })
                .on('mouseleave', function() {
                    $(this).tipsy("hide");
                });
        }
        else {
            both.off('mouseenter mouseleave');
        }
    },
    /**
    * Register new app type as mobile, web, desktop, etc. You can create new plugin to add new app type with its own dashboard
    * @param {string} name - name of the app type as mobile, web, desktop etc
    * @param {countlyView} view - instance of the countlyView to show as main dashboard for provided app type
    * @memberof app
    * @instance
    * @example
    * app.addAppType("mobile", MobileDashboardView);
    */
    addAppType: function(name, view) {
        this.appTypes[name] = new view();
        var menu = $("#default-type").clone();
        menu.attr("id", name + "-type");
        $("#sidebar-menu").append(menu);

        //run all queued type menus
        if (this._menuForTypes[name]) {
            for (var i = 0; i < this._menuForTypes[name].length; i++) {
                this.addMenuForType(name, this._menuForTypes[name][i].category, this._menuForTypes[name][i].node);
            }
            this._menuForTypes[name] = null;
        }

        //run all queued type submenus
        if (this._subMenuForTypes[name]) {
            for (i = 0; i < this._subMenuForTypes[name].length; i++) {
                this.addSubMenuForType(name, this._subMenuForTypes[name][i].parent_code, this._subMenuForTypes[name][i].node);
            }
            this._subMenuForTypes[name] = null;
        }

        //run all queued all type menus
        for (i = 0; i < this._menuForAllTypes.length; i++) {
            this.addMenuForType(name, this._menuForAllTypes[i].category, this._menuForAllTypes[i].node);
        }

        //run all queued all type submenus
        for (i = 0; i < this._subMenuForAllTypes.length; i++) {
            this.addSubMenuForType(name, this._subMenuForAllTypes[i].parent_code, this._subMenuForAllTypes[i].node);
        }
    },
    /**
    * Add callback to be called when user changes app in dashboard, which can be used globally, outside of the view
    * @param {function} callback - function receives app_id param which is app id of the new app to which user switched
    * @memberof app
    * @instance
    * @example
    * app.addAppSwitchCallback(function(appId){
    *    countlyCrashes.loadList(appId);
    * });
    */
    addAppSwitchCallback: function(callback) {
        this.appSwitchCallbacks.push(callback);
    },
    /**
    * Add callback to be called when user changes app in Managment -> Applications section, useful when providing custom input additions to app editing for different app types
    * @param {function} callback - function receives app_id param which is app id and type which is app type
    * @memberof app
    * @instance
    * @example
    * app.addAppManagementSwitchCallback(function(appId, type){
    *   if (type == "mobile") {
    *       addPushHTMLIfNeeded(type);
    *       $("#view-app .appmng-push").show();
    *   } else {
    *       $("#view-app .appmng-push").hide();
    *   }
    * });
    */
    addAppManagementSwitchCallback: function(callback) {
        this.appManagementSwitchCallbacks.push(callback);
    },
    /**
    * Modify app object on app create/update before submitting it to server
    * @param {function} callback - function args object with all data that will be submitted to server on app create/update
    * @memberof app
    * @instance
    * @example
    * app.addAppObjectModificatorfunction(args){
    *   if (args.type === "mobile") {
    *       //do something for mobile
    *   }
    * });
    */
    addAppObjectModificator: function(callback) {
        this.appObjectModificators.push(callback);
    },
    /**
     * Add a countlyManagementView-extending view which will be displayed in accordion tabs on Management->Applications screen
     * @memberof app
     * @param {string} plugin - plugin name
     * @param {string} title  - plugin title
     * @param {object} View - plugin view
     */
    addAppManagementView: function(plugin, title, View) {
        this.appManagementViews[plugin] = {title: title, view: View};
    },
    /**
    * Add additional settings to app management. Allows you to inject html with css classes app-read-settings, app-write-settings and using data-id attribute for the key to store in app collection. And if your value or input needs additional processing, you may add the callbacks here
    * @param {string} id - the same value on your input data-id attributes
    * @param {object} options - different callbacks for data modification
    * @param {function} options.toDisplay - function to be called when data is prepared for displaying, pases reference to html element with app-read-settings css class in which value should be displayed
    * @param {function} options.toInput - function to be called when data is prepared for input, pases reference to html input element with app-write-settings css class in which value should be placed for editing
    * @param {function} options.toSave - function to be called when data is prepared for saving, pases reference to object args that will be sent to server ad html input element with app-write-settings css class from which value should be taken and placed in args
     * @param {function} options.toInject - function to be called when to inject HTML into app management view
    * @memberof app
    * @instance
    * @example
    * app.addAppSetting("my_setting", {
    *     toDisplay: function(appId, elem){$(elem).text(process(countlyGlobal['apps'][appId]["my_setting"]));},
    *     toInput: function(appId, elem){$(elem).val(process(countlyGlobal['apps'][appId]["my_setting"]));},
    *     toSave: function(appId, args, elem){
    *         args.my_setting = process($(elem).val());
    *     },
    *     toInject: function(){
    *         var addApp = '<tr class="help-zone-vs" data-help-localize="manage-apps.app-my_setting">'+
    *             '<td>'+
    *                 '<span data-localize="management-applications.my_setting"></span>'+
    *             '</td>'+
    *             '<td>'+
    *                 '<input type="text" value="" class="app-write-settings" data-localize="placeholder.my_setting" data-id="my_setting">'+
    *             '</td>'+
    *         '</tr>';
    *
    *         $("#add-new-app table .table-add").before(addApp);
    *
    *         var editApp = '<tr class="help-zone-vs" data-help-localize="manage-apps.app-my_settingt">'+
    *             '<td>'+
    *                 '<span data-localize="management-applications.my_setting"></span>'+
    *             '</td>'+
    *             '<td>'+
    *                 '<div class="read app-read-settings" data-id="my_setting"></div>'+
    *                 '<div class="edit">'+
    *                     '<input type="text" value="" class="app-write-settings" data-id="my_setting" data-localize="placeholder.my_setting">'+
    *                 '</div>'+
    *             '</td>'+
    *         '</tr>';
    *
    *         $(".app-details table .table-edit").before(editApp);
    *     }
    * });
    */
    addAppSetting: function(id, options) {
        this.appSettings[id] = options;
    },
    /**
    * Add callback to be called when user changes app type in UI in Managment -> Applications section (even without saving app type, just chaning in UI), useful when providing custom input additions to app editing for different app types
    * @param {function} callback - function receives type which is app type
    * @memberof app
    * @instance
    * @example
    * app.addAppAddTypeCallback(function(type){
    *   if (type == "mobile") {
    *       $("#view-app .appmng-push").show();
    *   } else {
    *       $("#view-app .appmng-push").hide();
    *   }
    * });
    */
    addAppAddTypeCallback: function(callback) {
        this.appAddTypeCallbacks.push(callback);
    },
    /**
    * Add callback to be called when user open user edit UI in Managment -> Users section (even without saving, just opening), useful when providing custom input additions to user editing
    * @param {function} callback - function receives user object and paramm which can be true if saving data, false if opening data, string to modify data
    * @memberof app
    * @instance
    */
    addUserEditCallback: function(callback) {
        this.userEditCallbacks.push(callback);
    },
    /**
    * Add custom data export handler from datatables to csv/xls exporter. Provide exporter name and callback function.
    * Then add the same name as sExport attribute to the first datatables column.
    * Then when user will want to export data from this table, your callback function will be called to get the data.
    * You must perpare array of objects all with the same keys, where keys are columns and value are table data and return it from callback
    * to be processed by exporter.
    * @param {string} name - name of the export to expect in datatables sExport attribute
    * @param {function} callback - callback to call when getting data
    * @memberof app
    * @instance
    * @example
    * app.addDataExport("userinfo", function(){
    *    var ret = [];
    *    var elem;
    *    for(var i = 0; i < tableData.length; i++){
    *        //use same keys for each array element with different user data
    *        elem ={
    *            "fullname": tableData[i].firstname + " " + tableData[i].lastname,
    *            "job": tableData[i].company + ", " + tableData[i].jobtitle,
    *            "email": tableData[i].email
    *        };
    *        ret.push(elem);
    *    }
    *    //return array
    *    return ret;
    * });
    */
    addDataExport: function(name, callback) {
        this.dataExports[name] = callback;
    },
    /**
    * Add callback to be called everytime new view/page is loaded, so you can modify view with javascript after it has been loaded
    * @param {string} view - view url/hash or with possible # as wildcard or simply providing # for any view
    * @param {function} callback - function to be called when view loaded
    * @memberof app
    * @instance
    * @example <caption>Adding to single specific view with specific url</caption>
    * //this will work only for view bind to #/analytics/events
    * app.addPageScript("/analytics/events", function(){
    *   $("#event-nav-head").after(
    *       "<a href='#/analytics/events/compare'>" +
    *           "<div id='compare-events' class='event-container'>" +
    *               "<div class='icon'></div>" +
    *               "<div class='name'>" + jQuery.i18n.map["compare.button"] + "</div>" +
    *           "</div>" +
    *       "</a>"
    *   );
    * });

    * @example <caption>Add to all view subpages</caption>
    * //this will work /users/ and users/1 and users/abs etc
    * app.addPageScript("/users#", modifyUserDetailsForPush);

    * @example <caption>Adding script to any view</caption>
    * //this will work for any view
    * app.addPageScript("#", function(){
    *   alert("I am an annoying popup appearing on each view");
    * });
    */
    addPageScript: function(view, callback) {
        if (!this.pageScripts[view]) {
            this.pageScripts[view] = [];
        }
        this.pageScripts[view].push(callback);
    },
    /**
    * Add callback to be called everytime view is refreshed, because view may reset some html, and we may want to remodify it again. By default this happens every 10 seconds, so not cpu intensive tasks
    * @param {string} view - view url/hash or with possible # as wildcard or simply providing # for any view
    * @param {function} callback - function to be called when view refreshed
    * @memberof app
    * @instance
    * @example <caption>Adding to single specific view with specific url</caption>
    * //this will work only for view bind to #/analytics/events
    * app.addPageScript("/analytics/events", function(){
    *   $("#event-nav-head").after(
    *       "<a href='#/analytics/events/compare'>" +
    *           "<div id='compare-events' class='event-container'>" +
    *               "<div class='icon'></div>" +
    *               "<div class='name'>" + jQuery.i18n.map["compare.button"] + "</div>" +
    *           "</div>" +
    *       "</a>"
    *   );
    * });

    * @example <caption>Add to all view subpage refreshed</caption>
    * //this will work /users/ and users/1 and users/abs etc
    * app.addRefreshScript("/users#", modifyUserDetailsForPush);

    * @example <caption>Adding script to any view</caption>
    * //this will work for any view
    * app.addRefreshScript("#", function(){
    *   alert("I am an annoying popup appearing on each refresh of any view");
    * });
    */
    addRefreshScript: function(view, callback) {
        if (!this.refreshScripts[view]) {
            this.refreshScripts[view] = [];
        }
        this.refreshScripts[view].push(callback);
    },
    onAppSwitch: function(appId, refresh, firstLoad) {
        if (appId !== 0) {
            this._isFirstLoad = firstLoad;
            jQuery.i18n.map = JSON.parse(app.origLang);
            if (!refresh) {
                app.main(true);
                if (window.components && window.components.slider && window.components.slider.instance) {
                    window.components.slider.instance.close();
                }
                app.updateLongTaskViewsNofification(true);
            }
            $("#sidebar-menu .sidebar-menu").hide();
            var type = countlyGlobal.apps[appId].type;
            if ($("#sidebar-menu #" + type + "-type").length) {
                $("#sidebar-menu #" + type + "-type").show();
            }
            else {
                $("#sidebar-menu #default-type").show();
            }
            for (var i = 0; i < this.appSwitchCallbacks.length; i++) {
                this.appSwitchCallbacks[i](appId);
            }
            app.localize();
        }
    },
    onAppManagementSwitch: function(appId, type) {
        for (var i = 0; i < this.appManagementSwitchCallbacks.length; i++) {
            this.appManagementSwitchCallbacks[i](appId, type || countlyGlobal.apps[appId].type);
        }
        if ($("#app-add-name").length) {
            var newAppName = $("#app-add-name").val();
            $("#app-container-new .name").text(newAppName);
            $(".new-app-name").text(newAppName);
        }
    },
    onAppAddTypeSwitch: function(type) {
        for (var i = 0; i < this.appAddTypeCallbacks.length; i++) {
            this.appAddTypeCallbacks[i](type);
        }
    },
    onUserEdit: function(user, param) {
        for (var i = 0; i < this.userEditCallbacks.length; i++) {
            param = this.userEditCallbacks[i](user, param);
        }
        return param;
    },
    pageScript: function() { //scripts to be executed on each view change
        $("#month").text(moment().year());
        $("#day").text(moment().format("MMMM, YYYY"));
        $("#yesterday").text(moment().subtract(1, "days").format("Do"));

        var self = this;
        $(document).ready(function() {

            var selectedDateID = countlyCommon.getPeriod();

            if (Object.prototype.toString.call(selectedDateID) !== '[object Array]') {
                $("#" + selectedDateID).addClass("active");
            }
            var i = 0;
            var l = 0;
            if (self.pageScripts[Backbone.history.fragment]) {
                for (i = 0, l = self.pageScripts[Backbone.history.fragment].length; i < l; i++) {
                    self.pageScripts[Backbone.history.fragment][i]();
                }
            }
            for (var k in self.pageScripts) {
                if (k !== '#' && k.indexOf('#') !== -1 && Backbone.history.fragment.match("^" + k.replace(/#/g, '.*'))) {
                    for (i = 0, l = self.pageScripts[k].length; i < l; i++) {
                        self.pageScripts[k][i]();
                    }
                }
            }
            if (self.pageScripts["#"]) {
                for (i = 0, l = self.pageScripts["#"].length; i < l; i++) {
                    self.pageScripts["#"][i]();
                }
            }

            // Translate all elements with a data-help-localize or data-localize attribute
            self.localize();

            if ($("#help-toggle").hasClass("active")) {
                $('.help-zone-vb').tipsy({
                    gravity: $.fn.tipsy.autoNS,
                    trigger: 'manual',
                    title: function() {
                        return ($(this).data("help")) ? $(this).data("help") : "";
                    },
                    fade: true,
                    offset: 5,
                    cssClass: 'yellow',
                    opacity: 1,
                    html: true
                });
                $('.help-zone-vs').tipsy({
                    gravity: $.fn.tipsy.autoNS,
                    trigger: 'manual',
                    title: function() {
                        return ($(this).data("help")) ? $(this).data("help") : "";
                    },
                    fade: true,
                    offset: 5,
                    cssClass: 'yellow narrow',
                    opacity: 1,
                    html: true
                });

                $.idleTimer('destroy');
                clearInterval(self.refreshActiveView);
                $(".help-zone-vs, .help-zone-vb").hover(
                    function() {
                        $(this).tipsy("show");
                    },
                    function() {
                        $(this).tipsy("hide");
                    }
                );
            }

            $(document).off("chart:changed", ".usparkline").on("chart:changed", ".usparkline", function() {
                $(this).show();
            });
            $(document).off("chart:changed", ".dsparkline").on("chart:changed", ".dsparkline", function() {
                $(this).show();
            });
            $(".usparkline").peity("bar", { width: "100%", height: "30", colour: "#83C986", strokeColour: "#83C986", strokeWidth: 2 });
            $(".dsparkline").peity("bar", { width: "100%", height: "30", colour: "#DB6E6E", strokeColour: "#DB6E6E", strokeWidth: 2 });

            CountlyHelpers.setUpDateSelectors(self.activeView);

            $(window).click(function() {
                $("#date-picker").hide();
                $(".date-time-picker").hide();
                $(".cly-select").removeClass("active");
            });

            $("#date-picker").click(function(e) {
                e.stopPropagation();
            });

            $(".date-time-picker").click(function(e) {
                e.stopPropagation();
            });

            var dateTo;
            var dateFrom;
            $("#date-picker-button").click(function(e) {
                $("#date-picker").toggle();
                $("#date-picker-button").toggleClass("active");
                var date;
                if (self.dateToSelected) {
                    date = new Date(self.dateToSelected);
                    dateTo.datepicker("setDate", date);
                    dateFrom.datepicker("option", "maxDate", date);
                }
                else {
                    date = new Date();
                    date.setHours(0, 0, 0, 0);
                    self.dateToSelected = date.getTime();
                    dateTo.datepicker("setDate", new Date(self.dateToSelected));
                    dateFrom.datepicker("option", "maxDate", new Date(self.dateToSelected));
                }

                if (self.dateFromSelected) {
                    date = new Date(self.dateFromSelected);
                    dateFrom.datepicker("setDate", date);
                    dateTo.datepicker("option", "minDate", date);
                }
                else {
                    var extendDate = moment(dateTo.datepicker("getDate"), "MM-DD-YYYY").subtract(30, 'days').toDate();
                    extendDate.setHours(0, 0, 0, 0);
                    dateFrom.datepicker("setDate", extendDate);
                    self.dateFromSelected = extendDate.getTime();
                    dateTo.datepicker("option", "minDate", new Date(self.dateFromSelected));
                }

                $("#date-from-input").val(moment(dateFrom.datepicker("getDate"), "MM-DD-YYYY").format("MM/DD/YYYY"));
                $("#date-to-input").val(moment(dateTo.datepicker("getDate"), "MM-DD-YYYY").format("MM/DD/YYYY"));

                dateTo.datepicker("refresh");
                dateFrom.datepicker("refresh");
                //setSelectedDate();
                e.stopPropagation();
            });

            dateTo = $("#date-to").datepicker({
                numberOfMonths: 1,
                showOtherMonths: true,
                maxDate: moment().toDate(),
                onSelect: function(selectedDate) {
                    var instance = $(this).data("datepicker"),
                        date = $.datepicker.parseDate(instance.settings.dateFormat || $.datepicker._defaults.dateFormat, selectedDate, instance.settings);
                    date.setHours(0, 0, 0, 0);
                    if (date.getTime() < self.dateFromSelected) {
                        self.dateFromSelected = date.getTime();
                    }
                    $("#date-to-input").val(moment(date).format("MM/DD/YYYY"));
                    dateFrom.datepicker("option", "maxDate", date);
                    self.dateToSelected = date.getTime();
                },
                beforeShowDay: function(date) {
                    var ts = date.getTime();
                    if (ts < moment($("#date-to-input").val(), "MM/DD/YYYY") && ts >= moment($("#date-from-input").val(), "MM/DD/YYYY")) {
                        return [true, "in-range", ""];
                    }
                    else {
                        return [true, "", ""];
                    }
                }
            });

            dateFrom = $("#date-from").datepicker({
                numberOfMonths: 1,
                showOtherMonths: true,
                maxDate: moment().subtract(1, 'days').toDate(),
                onSelect: function(selectedDate) {
                    var instance = $(this).data("datepicker"),
                        date = $.datepicker.parseDate(instance.settings.dateFormat || $.datepicker._defaults.dateFormat, selectedDate, instance.settings);
                    date.setHours(0, 0, 0, 0);
                    if (date.getTime() > self.dateToSelected) {
                        self.dateToSelected = date.getTime();
                    }
                    $("#date-from-input").val(moment(date).format("MM/DD/YYYY"));
                    dateTo.datepicker("option", "minDate", date);
                    self.dateFromSelected = date.getTime();
                },
                beforeShowDay: function(date) {
                    var ts = date.getTime();
                    if (ts <= moment($("#date-to-input").val(), "MM/DD/YYYY") && ts > moment($("#date-from-input").val(), "MM/DD/YYYY")) {
                        return [true, "in-range", ""];
                    }
                    else {
                        return [true, "", ""];
                    }
                }
            });

            $("#date-from-input").keyup(function(event) {
                if (event.keyCode === 13) {
                    var date = moment($("#date-from-input").val(), "MM/DD/YYYY");

                    if (date.format("MM/DD/YYYY") !== $("#date-from-input").val()) {
                        var jsDate = $('#date-from').datepicker('getDate');
                        $("#date-from-input").val(moment(jsDate.getTime()).format("MM/DD/YYYY"));
                    }
                    else {
                        dateTo.datepicker("option", "minDate", date.toDate());
                        if (date.valueOf() > self.dateToSelected) {
                            date.startOf('day');
                            self.dateToSelected = date.valueOf();
                            dateFrom.datepicker("option", "maxDate", date.toDate());
                            dateTo.datepicker("setDate", date.toDate());
                            $("#date-to-input").val(date.format("MM/DD/YYYY"));

                        }
                        dateFrom.datepicker("setDate", date.toDate());
                    }
                }
            });


            $("#date-to-input").keyup(function(event) {
                if (event.keyCode === 13) {
                    var date = moment($("#date-to-input").val(), "MM/DD/YYYY");
                    if (date.format("MM/DD/YYYY") !== $("#date-to-input").val()) {
                        var jsDate = $('#date-to').datepicker('getDate');
                        $("#date-to-input").val(moment(jsDate.getTime()).format("MM/DD/YYYY"));
                    }
                    else {
                        dateFrom.datepicker("option", "maxDate", date.toDate());
                        if (date.toDate() < self.dateFromSelected) {
                            date.startOf('day');
                            self.dateFromSelected = date.valueOf();
                            dateTo.datepicker("option", "minDate", date.toDate());
                            dateFrom.datepicker("setDate", date.toDate());
                            $("#date-from-input").val(date.format("MM/DD/YYYY"));
                        }
                        dateTo.datepicker("setDate", date.toDate());
                    }
                }
            });
            /** function sets selected date */
            function setSelectedDate() {
                $("#selected-date").text(countlyCommon.getDateRangeForCalendar());
            }

            $.datepicker.setDefaults($.datepicker.regional[""]);
            $("#date-to").datepicker("option", $.datepicker.regional[countlyCommon.BROWSER_LANG]);


            $("#date-from").datepicker("option", $.datepicker.regional[countlyCommon.BROWSER_LANG]);

            $("#date-submit").click(function() {
                if (!self.dateFromSelected && !self.dateToSelected) {
                    return false;
                }

                countlyCommon.setPeriod([
                    self.dateFromSelected - countlyCommon.getOffsetCorrectionForTimestamp(self.dateFromSelected),
                    self.dateToSelected - countlyCommon.getOffsetCorrectionForTimestamp(self.dateToSelected) + 24 * 60 * 60 * 1000 - 1
                ]);

                self.activeView.dateChanged();
                app.runRefreshScripts();
                setSelectedDate();
                $("#date-selector .calendar").removeClass("active");
                $(".date-selector").removeClass("selected").removeClass("active");
                $("#date-picker").hide();
            });

            $("#date-cancel").click(function() {
                $("#date-selector .calendar").removeClass("selected").removeClass("active");
                $("#date-picker").hide();
            });

            $("#date-cancel").click(function() {
                $("#date-selector .calendar").removeClass("selected").removeClass("active");
                $("#date-picker").hide();
            });

            setSelectedDate();

            $('.scrollable').slimScroll({
                height: '100%',
                start: 'top',
                wheelStep: 10,
                position: 'right',
                disableFadeOut: true
            });

            $(".checkbox").on('click', function() {
                $(this).toggleClass("checked");
            });

            $(".resource-link").on('click', function() {
                if ($(this).data("link")) {
                    CountlyHelpers.openResource($(this).data("link"));
                }
            });

            $("#sidebar-menu").find(".item").each(function() {
                if ($(this).next().hasClass("sidebar-submenu") && $(this).find(".ion-chevron-right").length === 0) {
                    $(this).append("<span class='ion-chevron-right'></span>");
                }
            });

            $('.nav-search').on('input', "input", function() {
                var searchText = new RegExp($(this).val().toLowerCase().replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')),
                    searchInside = $(this).parent().next().find(".searchable");

                searchInside.filter(function() {
                    return !(searchText.test($(this).text().toLowerCase()));
                }).css('display', 'none').removeClass('filtered-app-item');

                searchInside.filter(function() {
                    return searchText.test($(this).text().toLowerCase());
                }).css('display', 'block').addClass('filtered-app-item');
            });

            $(document).on('input', "#listof-apps .search input", function() {
                var searchText = new RegExp($(this).val().toLowerCase()),
                    searchInside = $(this).parent().next().find(".searchable");

                searchInside.filter(function() {
                    return !(searchText.test($(this).text().toLowerCase()));
                }).css('display', 'none');

                searchInside.filter(function() {
                    return searchText.test($(this).text().toLowerCase());
                }).css('display', 'block');
            });

            $(document).on('mouseenter', ".bar-inner", function() {
                var number = $(this).parent().next();

                number.text($(this).data("item"));
                number.css({ "color": $(this).css("background-color") });
            });

            $(document).on('mouseleave', ".bar-inner", function() {
                var number = $(this).parent().next();

                number.text(number.data("item"));
                number.css({ "color": $(this).parent().find(".bar-inner:first-child").css("background-color") });
            });

            /*
                Auto expand left navigation (events, management > apps etc)
                if ellipsis is applied to children
             */
            var closeLeftNavExpand;
            var leftNavSelector = "#event-nav, #app-management-bar, #configs-title-bar";
            var $leftNav = $(leftNavSelector);

            $leftNav.hoverIntent({
                over: function() {
                    var parentLeftNav = $(this).parents(leftNavSelector);

                    if (leftNavNeedsExpand(parentLeftNav)) {
                        parentLeftNav.addClass("expand");
                    }
                },
                out: function() {
                    // Delay shrinking and allow movement towards the top section cancel it
                    closeLeftNavExpand = setTimeout(function() {
                        $(this).parents(leftNavSelector).removeClass("expand");
                    }, 500);
                },
                selector: ".slimScrollDiv"
            });

            $leftNav.on("mousemove", function() {
                if ($(this).hasClass("expand")) {
                    clearTimeout(closeLeftNavExpand);
                }
            });

            $leftNav.on("mouseleave", function() {
                $(this).removeClass("expand");
            });

            /** Checks if nav needs to expand
                @param {object} $nav html element
                @returns {boolean} true or false
            */
            function leftNavNeedsExpand($nav) {
                var makeExpandable = false;

                $nav.find(".event-container:not(#compare-events) .name, .app-container .name, .config-container .name").each(function(z, el) {
                    if (el.offsetWidth < el.scrollWidth) {
                        makeExpandable = true;
                        return false;
                    }
                });

                return makeExpandable;
            }
            /* End of auto expand code */
        });
    }
});

Backbone.history || (Backbone.history = new Backbone.History);
Backbone.history._checkUrl = Backbone.history.checkUrl;
Backbone.history.urlChecks = [];
Backbone.history.checkOthers = function() {
    var proceed = true;
    for (var i = 0; i < Backbone.history.urlChecks.length; i++) {
        if (!Backbone.history.urlChecks[i]()) {
            proceed = false;
        }
    }
    return proceed;
};
Backbone.history.checkUrl = function() {
    if (Backbone.history.checkOthers()) {
        Backbone.history._checkUrl();
    }
};

Backbone.history.noHistory = function(hash) {
    if (history && history.replaceState) {
        history.replaceState(undefined, undefined, hash);
    }
    else {
        location.replace(hash);
    }
};

Backbone.history.__checkUrl = Backbone.history.checkUrl;
Backbone.history._getFragment = Backbone.history.getFragment;
Backbone.history.appIds = [];
for (var i in countlyGlobal.apps) {
    Backbone.history.appIds.push(i);
}
Backbone.history.getFragment = function() {
    var fragment = Backbone.history._getFragment();
    if (fragment.indexOf("/" + countlyCommon.ACTIVE_APP_ID) === 0) {
        fragment = fragment.replace("/" + countlyCommon.ACTIVE_APP_ID, "");
    }
    return fragment;
};
Backbone.history.checkUrl = function() {
    store.set("countly_fragment_name", Backbone.history._getFragment());
    var app_id = Backbone.history._getFragment().split("/")[1] || "";
    if (countlyCommon.APP_NAMESPACE !== false && countlyCommon.ACTIVE_APP_ID !== 0 && countlyCommon.ACTIVE_APP_ID !== app_id && Backbone.history.appIds.indexOf(app_id) === -1) {
        Backbone.history.noHistory("#/" + countlyCommon.ACTIVE_APP_ID + Backbone.history._getFragment());
        app_id = countlyCommon.ACTIVE_APP_ID;
    }

    if (countlyCommon.ACTIVE_APP_ID !== 0 && countlyCommon.ACTIVE_APP_ID !== app_id && Backbone.history.appIds.indexOf(app_id) !== -1) {
        app.switchApp(app_id, function() {
            if (Backbone.history.checkOthers()) {
                Backbone.history.__checkUrl();
            }
        });
    }
    else {
        if (Backbone.history.checkOthers()) {
            Backbone.history.__checkUrl();
        }
    }
};

var checkGlobalAdminOnlyPermission = function() {
    var userCheckList = [
        "/manage/users",
        "/manage/apps"
    ];

    var adminCheckList = [
        "/manage/users"
    ];

    if (!countlyGlobal.member.global_admin && !countlyGlobal.config.autonomous) {

        var existed = false;
        var checkList = userCheckList;
        if (countlyGlobal.admin_apps && Object.keys(countlyGlobal.admin_apps).length) {
            checkList = adminCheckList;
        }
        checkList.forEach(function(item) {
            if (Backbone.history.getFragment().indexOf(item) > -1) {
                existed = true;
            }
        });

        if (existed === true) {

            window.location.hash = "/";
            return false;
        }
    }
    return true;
};
Backbone.history.urlChecks.push(checkGlobalAdminOnlyPermission);


//initial hash check
(function() {
    if (!Backbone.history.getFragment() && store.get("countly_fragment_name")) {
        Backbone.history.noHistory("#" + store.get("countly_fragment_name"));
    }
    else {
        var app_id = Backbone.history._getFragment().split("/")[1] || "";
        if (countlyCommon.ACTIVE_APP_ID === app_id || Backbone.history.appIds.indexOf(app_id) !== -1) {
            //we have app id
            if (app_id !== countlyCommon.ACTIVE_APP_ID) {
                // but it is not currently selected app, so let' switch
                countlyCommon.setActiveApp(app_id);
                $("#active-app-name").text(countlyGlobal.apps[app_id].name);
                $('#active-app-name').attr('title', countlyGlobal.apps[app_id].name);
                $("#active-app-icon").css("background-image", "url('" + countlyGlobal.path + "appimages/" + app_id + ".png')");
            }
        }
        else if (countlyCommon.APP_NAMESPACE !== false) {
            //add current app id
            Backbone.history.noHistory("#/" + countlyCommon.ACTIVE_APP_ID + Backbone.history._getFragment());
        }
    }
})();

var app = new AppRouter();

/**
* Navigate to another hash address programmatically, without trigering view route and without leaving trace in history, if possible
* @param {string} hash - url path (hash part) to change
* @memberof app
* @example
* //you are at #/manage/systemlogs
* app.noHistory("#/manage/systemlogs/query/{}");
* //now pressing back would not go to #/manage/systemlogs
*/
app.noHistory = function(hash) {
    if (countlyCommon.APP_NAMESPACE !== false) {
        hash = "#/" + countlyCommon.ACTIVE_APP_ID + hash.substr(1);
    }
    if (history && history.replaceState) {
        history.replaceState(undefined, undefined, hash);
    }
    else {
        location.replace(hash);
    }
};

//collects requests for active views to dscard them if views changed
$.ajaxPrefilter(function(options, originalOptions, jqXHR) {
    //add to options for independent!!!

    var myurl = "";
    var mydata = "";
    if (originalOptions && originalOptions.url) {
        myurl = originalOptions.url;
    }
    if (originalOptions && originalOptions.data) {
        mydata = JSON.stringify(originalOptions.data);
    }
    //request which is not killed on view change(only on app change)
    jqXHR.my_set_url = myurl;
    jqXHR.my_set_data = mydata;

    if (originalOptions && (originalOptions.type === 'GET' || originalOptions.type === 'get') && originalOptions.url.substr(0, 2) === '/o') {
        if (originalOptions.data && originalOptions.data.preventGlobalAbort && originalOptions.data.preventGlobalAbort === true) {
            return true;
        }

        if (originalOptions.data && originalOptions.data.preventRequestAbort && originalOptions.data.preventRequestAbort === true) {
            if (app._myRequests[myurl] && app._myRequests[myurl][mydata]) {
                jqXHR.abort(); //we already have same working request
            }
            else {
                jqXHR.always(function(data, textStatus, jqXHR1) {
                    //if success jqxhr object is third, errored jqxhr object is in first parameter.
                    if (jqXHR1 && jqXHR1.my_set_url && jqXHR1.my_set_data) {
                        if (app._myRequests[jqXHR1.my_set_url] && app._myRequests[jqXHR1.my_set_url][jqXHR1.my_set_data]) {
                            delete app._myRequests[jqXHR1.my_set_url][jqXHR1.my_set_data];
                        }
                    }
                    else if (data && data.my_set_url && data.my_set_data) {
                        if (app._myRequests[data.my_set_url] && app._myRequests[data.my_set_url][data.my_set_data]) {
                            delete app._myRequests[data.my_set_url][data.my_set_data];
                        }

                    }
                });
                //save request in our object
                if (!app._myRequests[myurl]) {
                    app._myRequests[myurl] = {};
                }
                app._myRequests[myurl][mydata] = jqXHR;
            }
        }
        else {
            if (app.activeView) {
                if (app.activeView._myRequests[myurl] && app.activeView._myRequests[myurl][mydata]) {
                    jqXHR.abort(); //we already have same working request
                }
                else {
                    jqXHR.always(function(data, textStatus, jqXHR1) {
                        //if success jqxhr object is third, errored jqxhr object is in first parameter.
                        if (jqXHR1 && jqXHR1.my_set_url && jqXHR1.my_set_data) {
                            if (app.activeView._myRequests[jqXHR1.my_set_url] && app.activeView._myRequests[jqXHR1.my_set_url][jqXHR1.my_set_data]) {
                                delete app.activeView._myRequests[jqXHR1.my_set_url][jqXHR1.my_set_data];
                            }
                        }
                        else if (data && data.my_set_url && data.my_set_data) {
                            if (app.activeView._myRequests[data.my_set_url] && app.activeView._myRequests[data.my_set_url][data.my_set_data]) {
                                delete app.activeView._myRequests[data.my_set_url][data.my_set_data];
                            }
                        }
                    });
                    //save request in our object
                    if (!app.activeView._myRequests[myurl]) {
                        app.activeView._myRequests[myurl] = {};
                    }
                    app.activeView._myRequests[myurl][mydata] = jqXHR;
                }
            }
        }
    }
});