/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/* global define, module, require, exports */

(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        define(['jquery',
                'Slick',
                'nf.Common',
                'nf.Dialog',
                'nf.ErrorHandler'],
            function ($, Slick, nfCommon, nfDialog, nfErrorHandler) {
                return (nf.ClusterTable = factory($, Slick, nfCommon, nfDialog, nfErrorHandler));
            });
    } else if (typeof exports === 'object' && typeof module === 'object') {
        module.exports = (nf.ClusterTable =
            factory(require('jquery'),
                require('Slick'),
                require('nf.Common'),
                require('nf.Dialog'),
                require('nf.ErrorHandler')));
    } else {
        nf.ClusterTable = factory(root.$,
            root.Slick,
            root.nf.Common,
            root.nf.Dialog,
            root.nf.ErrorHandler);
    }
}(this, function ($, Slick, nfCommon, nfDialog, nfErrorHandler) {
    'use strict';

    /**
     * Configuration object used to hold a number of configuration items.
     */
    var config = {
        primaryNode: 'Primary Node',
        clusterCoordinator: 'Cluster Coordinator',
        urls: {
            cluster: '../nifi-api/controller/cluster',
            nodes: '../nifi-api/controller/cluster/nodes',
            systemDiagnostics: '../nifi-api/system-diagnostics'
        },
        data: [{
            name: 'cluster',
            update: refreshClusterData,
            isAuthorized: nfCommon.canAccessController
        }, {
            name: 'systemDiagnostics',
            update: refreshSystemDiagnosticsData,
            isAuthorized: nfCommon.canAccessSystem
        }
        ]
    };

    var commonTableOptions = {
        autosizeColsMode: Slick.GridAutosizeColsMode.LegacyForceFit,
        enableTextSelectionOnCells: true,
        enableCellNavigation: false,
        enableColumnReorder: false,
        autoEdit: false,
        rowHeight: 24
    };

    var nodesTab = {
        name: 'Nodes',
        data: {
            dataSet: 'cluster',
            update: updateNodesTableData
        },
        tabContentId: 'cluster-nodes-tab-content',
        tableId: 'cluster-nodes-table',
        tableColumnModel: createNodeTableColumnModel,
        tableIdColumn: 'nodeId',
        tableOptions: commonTableOptions,
        tableOnClick: nodesTableOnClick,
        createTableOnEnter: null,
        cleanUpTable: null,
        init: commonTableInit,
        onSort: sort,
        onTabSelected: onSelectTab,
        filterOptions: [{
            text: 'by address',
            value: 'address'
        }, {
            text: 'by status',
            value: 'status'
        }]
    };

    var jvmTab = {
        name: 'JVM',
        data: {
            dataSet: 'systemDiagnostics',
            update: updateJvmTableData
        },
        tabContentId: 'cluster-jvm-tab-content',
        tableId: 'cluster-jvm-table',
        tableColumnModel: createJvmTableColumnModel,
        tableIdColumn: 'id',
        tableOptions: commonTableOptions,
        tableOnClick: null,
        createTableOnEnter: createJvmTableOnEnterHandler,
        cleanUpTable: cleanUpJvmTable,
        init: commonTableInit,
        onSort: sort,
        onTabSelected: onSelectTab,
        filterOptions: [{
            text: 'by address',
            value: 'node'
        }]
    };

    var systemTab = {
        name: 'System',
        data: {
            dataSet: 'systemDiagnostics',
            update: updateSystemTableData
        },
        tabContentId: 'cluster-system-tab-content',
        tableId: 'cluster-system-table',
        tableColumnModel: [
            {
                id: 'node',
                field: 'node',
                name: 'Node Address',
                sortable: true,
                resizable: true,
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'processors',
                field: 'processors',
                name: 'Cores',
                sortable: true,
                resizable: true,
                cssClass: 'cell-right',
                headerCssClass: 'header-right',
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'processorLoadAverage',
                field: 'processorLoadAverage',
                name: 'Core Load Average',
                sortable: true,
                resizable: true,
                cssClass: 'cell-right',
                headerCssClass: 'header-right',
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'totalThreads',
                field: 'totalThreads',
                name: 'Total Threads',
                sortable: true,
                resizable: true,
                cssClass: 'cell-right',
                headerCssClass: 'header-right',
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'daemonThreads',
                field: 'daemonThreads',
                name: 'Daemon Threads',
                sortable: true,
                resizable: true,
                cssClass: 'cell-right',
                headerCssClass: 'header-right',
                formatter: nfCommon.genericValueFormatter
            }
        ],
        tableIdColumn: 'id',
        tableOptions: commonTableOptions,
        tableOnClick: null,
        createTableOnEnter: null,
        cleanUpTable: null,
        init: commonTableInit,
        onSort: sort,
        onTabSelected: onSelectTab,
        filterOptions: [{
            text: 'by address',
            value: 'node'
        }]
    };

    var flowFileTab = {
        name: 'FlowFile Storage',
        data: {
            dataSet: 'systemDiagnostics',
            update: updateFlowFileTableData
        },
        tabContentId: 'cluster-flowfile-tab-content',
        tableId: 'cluster-flowfile-table',
        tableColumnModel: [
            {
                id: 'node',
                field: 'node',
                name: 'Node Address',
                sortable: true,
                resizable: true,
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'ffRepoTotal',
                field: 'ffRepoTotal',
                name: 'Total Space',
                sortable: true,
                resizable: true,
                cssClass: 'cell-right',
                headerCssClass: 'header-right',
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'ffRepoUsed',
                field: 'ffRepoUsed',
                name: 'Used Space',
                sortable: true,
                resizable: true,
                cssClass: 'cell-right',
                headerCssClass: 'header-right',
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'ffRepoFree',
                field: 'ffRepoFree',
                name: 'Free Space',
                sortable: true,
                resizable: true,
                cssClass: 'cell-right',
                headerCssClass: 'header-right',
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'ffStoreUtil',
                field: 'ffRepoUtil',
                name: 'Utilization',
                sortable: true,
                resizable: true,
                cssClass: 'cell-right',
                headerCssClass: 'header-right',
                formatter: nfCommon.genericValueFormatter
            }
        ],
        tableIdColumn: 'id',
        tableOptions: commonTableOptions,
        tableOnClick: null,
        createTableOnEnter: null,
        cleanUpTable: null,
        init: commonTableInit,
        onSort: sort,
        onTabSelected: onSelectTab,
        filterOptions: [{
            text: 'by address',
            value: 'node'
        }]
    };

    var contentTab = {
        name: 'Content Storage',
        data: {
            dataSet: 'systemDiagnostics',
            update: updateContentTableData
        },
        tabContentId: 'cluster-content-tab-content',
        tableId: 'cluster-content-table',
        tableColumnModel: [
            {
                id: 'node',
                field: 'node',
                name: 'Node Address',
                sortable: true,
                resizable: true,
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'contentRepoId',
                field: 'contentRepoId',
                name: 'Content Repository',
                sortable: true,
                resizable: true,
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'contentRepoTotal',
                field: 'contentRepoTotal',
                name: 'Total Space',
                sortable: true,
                resizable: true,
                cssClass: 'cell-right',
                headerCssClass: 'header-right',
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'contentRepoUsed',
                field: 'contentRepoUsed',
                name: 'Used Space',
                sortable: true,
                resizable: true,
                cssClass: 'cell-right',
                headerCssClass: 'header-right',
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'contentRepoFree',
                field: 'contentRepoFree',
                name: 'Free Space',
                sortable: true,
                resizable: true,
                cssClass: 'cell-right',
                headerCssClass: 'header-right',
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'contentRepoUtil',
                field: 'contentRepoUtil',
                name: 'Utilization',
                sortable: true,
                resizable: true,
                cssClass: 'cell-right',
                headerCssClass: 'header-right',
                formatter: nfCommon.genericValueFormatter
            }
        ],
        tableIdColumn: 'id',
        tableOptions: commonTableOptions,
        tableOnClick: null,
        createTableOnEnter: null,
        cleanUpTable: null,
        init: commonTableInit,
        onSort: sort,
        onTabSelected: onSelectTab,
        filterOptions: [{
            text: 'by address',
            value: 'node'
        }, {
            text: 'by repository',
            value: 'contentRepoId'
        }]
    };

    var provenanceTab = {
        name: 'Provenance Storage',
        data: {
            dataSet: 'systemDiagnostics',
            update: updateProvenanceTableData
        },
        tabContentId: 'cluster-provenance-tab-content',
        tableId: 'cluster-provenance-table',
        tableColumnModel: [
            {
                id: 'node',
                field: 'node',
                name: 'Node Address',
                sortable: true,
                resizable: true,
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'provenanceRepoId',
                field: 'provenanceRepoId',
                name: 'Provenance Repository',
                sortable: true,
                resizable: true,
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'provenanceRepoTotal',
                field: 'provenanceRepoTotal',
                name: 'Total Space',
                sortable: true,
                resizable: true,
                cssClass: 'cell-right',
                headerCssClass: 'header-right',
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'provenanceRepoUsed',
                field: 'provenanceRepoUsed',
                name: 'Used Space',
                sortable: true,
                resizable: true,
                cssClass: 'cell-right',
                headerCssClass: 'header-right',
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'provenanceRepoFree',
                field: 'provenanceRepoFree',
                name: 'Free Space',
                sortable: true,
                resizable: true,
                cssClass: 'cell-right',
                headerCssClass: 'header-right',
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'provenanceRepoUtil',
                field: 'provenanceRepoUtil',
                name: 'Utilization',
                sortable: true,
                resizable: true,
                cssClass: 'cell-right',
                headerCssClass: 'header-right',
                formatter: nfCommon.genericValueFormatter
            }
        ],
        tableIdColumn: 'id',
        tableOptions: commonTableOptions,
        tableOnClick: null,
        createTableOnEnter: null,
        cleanUpTable: null,
        init: commonTableInit,
        onSort: sort,
        onTabSelected: onSelectTab,
        filterOptions: [{
            text: 'by address',
            value: 'node'
        }, {
            text: 'by repository',
            value: 'contentRepoId'
        }]
    };

    var versionTab = {
        name: 'Versions',
        data: {
            dataSet: 'systemDiagnostics',
            update: updateVersionTableData
        },
        tabContentId: 'cluster-version-tab-content',
        tableId: 'cluster-version-table',
        tableColumnModel: [
            {
                id: 'node',
                field: 'node',
                name: 'Node Address',
                sortable: true,
                resizable: true,
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'version',
                field: 'version',
                name: 'NiFi Version',
                sortable: true,
                resizable: true,
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'javavendor',
                field: 'javaVendor',
                name: 'Java Vendor',
                sortable: true,
                resizable: true,
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'javaversion',
                field: 'javaVersion',
                name: 'Java Version',
                sortable: true,
                resizable: true,
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'osname',
                field: 'osName',
                name: 'OS Name',
                sortable: true,
                resizable: true,
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'osversion',
                field: 'osVersion',
                name: 'OS Version',
                sortable: true,
                resizable: true,
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'osarch',
                field: 'osArchitecture',
                name: 'OS Architecture',
                sortable: true,
                resizable: true,
                formatter: nfCommon.genericValueFormatter
            }
        ],
        tableIdColumn: 'id',
        tableOptions: commonTableOptions,
        tableOnClick: null,
        createTableOnEnter: null,
        cleanUpTable: null,
        init: commonTableInit,
        onSort: sort,
        onTabSelected: onSelectTab,
        filterOptions: [{
            text: 'by address',
            value: 'address'
        }]
    };

    var clusterTabs = [nodesTab, systemTab, jvmTab, flowFileTab, contentTab, provenanceTab, versionTab];
    var tabsByName = {};
    var dataSetHandlers = {};

    /**
     * Click handler for the Nodes table options.
     */
    function nodesTableOnClick(e, args, target, item) {
        if (nodesTab.grid.getColumns()[args.cell].id === 'actions') {
            if (target.hasClass('prompt-for-connect')) {
                promptForConnect(item);
            } else if (target.hasClass('prompt-for-removal')) {
                promptForRemoval(item);
            } else if (target.hasClass('prompt-for-offload')) {
                promptForOffload(item);
            } else if (target.hasClass('prompt-for-disconnect')) {
                promptForDisconnect(item);
            }
        } else if (nodesTab.grid.getColumns()[args.cell].id === 'moreDetails') {
            if (target.hasClass('show-node-details')) {
                showNodeDetails(item);
            }
        }
    }

    /**
     * Creates the Slick Grid column model for the Nodes table.
     */
    function createNodeTableColumnModel() {
        var moreDetailsFormatter = function (row, cell, value, columnDef, dataContext) {
            return '<div title="View Details" class="pointer show-node-details fa fa-info-circle"></div>';
        };

        // define a custom formatter for the run status column
        var nodeFormatter = function (row, cell, value, columnDef, dataContext) {
            return formatNodeAddress(dataContext);
        };

        // function for formatting the last accessed time
        var valueFormatter = function (row, cell, value, columnDef, dataContext) {
            return nfCommon.formatValue(value);
        };

        // define a custom formatter for the status column
        var statusFormatter = function (row, cell, value, columnDef, dataContext) {
            return formatNodeStatus(dataContext);
        };

        var columnModel = [
            {
                id: 'moreDetails',
                name: '&nbsp;',
                sortable: false,
                resizable: false,
                formatter: moreDetailsFormatter,
                width: 50,
                maxWidth: 50
            },
            {
                id: 'formattedNodeAddress',
                field: 'formattedNodeAddress',
                name: 'Node Address',
                formatter: nodeFormatter,
                resizable: true,
                sortable: true
            },
            {
                id: 'activeThreadCount',
                field: 'activeThreadCount',
                name: 'Active Thread Count',
                resizable: true,
                sortable: true,
                defaultSortAsc: false,
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'queued',
                field: 'queued',
                name: '<span class="queued-title">Queue</span>&nbsp;/&nbsp;<span class="queued-size-title">Size</span>',
                resizable: true,
                sortable: true,
                defaultSortAsc: false,
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'status',
                field: 'status',
                name: 'Status',
                formatter: statusFormatter,
                resizable: true,
                sortable: true
            },
            {
                id: 'uptime',
                field: 'nodeStartTime',
                name: 'Started At',
                formatter: valueFormatter,
                resizable: true,
                sortable: true,
                defaultSortAsc: false
            },
            {
                id: 'heartbeat',
                field: 'heartbeat',
                name: 'Last Heartbeat',
                formatter: valueFormatter,
                resizable: true,
                sortable: true,
                defaultSortAsc: false
            }
        ];

        // only allow the admin to modify the cluster
        if (nfCommon.canModifyController()) {
            var actionFormatter = function (row, cell, value, columnDef, dataContext) {
                var connectDiv = '<div title="Connect" class="pointer prompt-for-connect fa fa-plug"></div>';
                var deleteDiv = '<div title="Delete" class="pointer prompt-for-removal fa fa-trash"></div>';
                var disconnectDiv = '<div title="Disconnect" class="pointer prompt-for-disconnect fa fa-power-off"></div>';
                var offloadDiv = '<div title="Offload" class="pointer prompt-for-offload fa fa-rotate-90 fa-upload" ' +
                    'style="margin-top: 5px;margin-left: 5px;margin-right: -2px;"></div>';
                var markup = '';

                // determine the current status and create the appropriate markup
                if (dataContext.status === 'CONNECTED' || dataContext.status === 'CONNECTING') {
                    markup += disconnectDiv;
                } else if (dataContext.status === 'DISCONNECTED') {
                    markup += connectDiv + offloadDiv + deleteDiv;
                } else if (dataContext.status === 'OFFLOADED') {
                    markup += connectDiv + deleteDiv;
                } else {
                    markup += '<div style="width: 16px; height: 16px;">&nbsp;</div>';
                }

                return markup;
            };

            columnModel.push({
                id: 'actions',
                label: '&nbsp;',
                formatter: actionFormatter,
                resizable: false,
                sortable: false,
                width: 80,
                maxWidth: 80
            });
        }

        return columnModel;
    }

    /**
     * Click handler for the Nodes table options.
     */
    function createJvmTableOnEnterHandler(grid) {
        var data = grid.getData();

        return function (e) {
            var jvmGc = $(this).find('div.show-jvm-gc');
            if (jvmGc.length && !jvmGc.data('qtip')) {
                var rowId = $(this).find('span.row-id').text();

                // get the status item
                var item = data.getItemById(rowId);

                // generate the markup
                var garbageCollections = nfCommon.getFormattedGarbageCollections(item.garbageCollection);
                var tooltip = nfCommon.formatUnorderedList(garbageCollections);

                // show the tooltip
                if (nfCommon.isDefinedAndNotNull(tooltip)) {
                    jvmGc.qtip($.extend({}, nfCommon.config.tooltipConfig, {
                        content: tooltip,
                        position: {
                            container: $('#cluster'),
                            at: 'bottom left',
                            my: 'top right',
                            adjust: {
                                x: 4,
                                y: 4
                            }
                        }
                    }));
                }
            }
        };
    }

    function cleanUpJvmTable(table) {
        nfCommon.cleanUpTooltips(table, 'div.show-jvm-gc');
    }

    function createJvmTableColumnModel() {
        var gcFormatter = function (row, cell, value, columnDef, dataContext) {
            return '<div class="pointer show-jvm-gc fa fa-question-circle"></div><span class="hidden row-id">' + nfCommon.escapeHtml(dataContext.id) + '</span>';
        };

        return [
            {
                id: 'node',
                field: 'node',
                name: 'Node Address',
                sortable: true,
                resizable: true,
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'heapMax',
                field: 'maxHeap',
                name: 'Heap Max',
                sortable: true,
                resizable: true,
                cssClass: 'cell-right',
                headerCssClass: 'header-right',
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'heapTotal',
                field: 'totalHeap',
                name: 'Heap Total',
                sortable: true,
                resizable: true,
                cssClass: 'cell-right',
                headerCssClass: 'header-right',
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'heapUsed',
                field: 'usedHeap',
                name: 'Heap Used',
                sortable: true,
                resizable: true,
                cssClass: 'cell-right',
                headerCssClass: 'header-right',
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'heapUtilPct',
                field: 'heapUtilization',
                name: 'Heap Utilization',
                sortable: true,
                resizable: true,
                cssClass: 'cell-right',
                headerCssClass: 'header-right',
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'nonHeapTotal',
                field: 'totalNonHeap',
                name: 'Non-Heap Total',
                sortable: true,
                resizable: true,
                cssClass: 'cell-right',
                headerCssClass: 'header-right',
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'nonHeapUsed',
                field: 'usedNonHeap',
                name: 'Non-Heap Used',
                sortable: true,
                resizable: true,
                cssClass: 'cell-right',
                headerCssClass: 'header-right',
                formatter: nfCommon.genericValueFormatter
            },
            {
                id: 'gc',
                name: 'GC',
                sortable: false,
                resizable: true,
                formatter: gcFormatter,
                width: 50,
                maxWidth: 50
            },
            {
                id: 'uptime',
                field: 'uptime',
                name: 'Uptime',
                sortable: true,
                resizable: true,
                cssClass: 'cell-right',
                headerCssClass: 'header-right',
                formatter: nfCommon.genericValueFormatter
            }
        ];
    }

    var prevColumn, count;

    /**
     * Sorts the specified data using the specified sort details.
     *
     * @param {object} sortDetails
     * @param {object} data
     */
    function sort(sortDetails, dataView, tab) {
        // defines a function for sorting
        var comparer = function (a, b) {
            if (sortDetails.columnId === 'heartbeat' || sortDetails.columnId === 'nodeStartTime') {
                var aDate = nfCommon.parseDateTime(a[sortDetails.columnId]);
                var bDate = nfCommon.parseDateTime(b[sortDetails.columnId]);
                return aDate.getTime() - bDate.getTime();
            } else if (sortDetails.columnId === 'queued') {
                var aSplit = a[sortDetails.columnId].split(/ \/ /);
                var bSplit = b[sortDetails.columnId].split(/ \/ /);
                var mod = count % 4;
                if (mod < 2) {
                    $('#cluster-nodes-table span.queued-title').addClass('sorted');
                    var aCount = nfCommon.parseCount(aSplit[0]);
                    var bCount = nfCommon.parseCount(bSplit[0]);
                    return aCount - bCount;
                } else {
                    $('#cluster-nodes-table span.queued-size-title').addClass('sorted');
                    var aSize = nfCommon.parseSize(aSplit[1]);
                    var bSize = nfCommon.parseSize(bSplit[1]);
                    return aSize - bSize;
                }
            } else if (sortDetails.columnId === 'maxHeap' || sortDetails.columnId === 'totalHeap' || sortDetails.columnId === 'usedHeap'
                || sortDetails.columnId === 'totalNonHeap' || sortDetails.columnId === 'usedNonHeap'
                || sortDetails.columnId.includes('RepoTotal') || sortDetails.columnId.includes('RepoUsed')
                || sortDetails.columnId.includes('RepoFree')) {
                var aSize = nfCommon.parseSize(a[sortDetails.columnId]);
                var bSize = nfCommon.parseSize(b[sortDetails.columnId]);
                return aSize - bSize;
            } else if (sortDetails.columnId === 'totalThreads' || sortDetails.columnId === 'daemonThreads'
                || sortDetails.columnId === 'processors' || sortDetails.columnId === 'processorLoadAverage') {
                var aCount = a[sortDetails.columnId];
                var bCount = b[sortDetails.columnId];
                return aCount - bCount;
            } else if (sortDetails.columnId === 'status') {
                var aStatus = formatNodeStatus(a);
                var bStatus = formatNodeStatus(b);
                return aStatus === bStatus ? 0 : aStatus > bStatus ? 1 : -1;
            } else if (sortDetails.columnId === 'formattedNodeAddress') {
                var aNode = formatNodeAddress(a);
                var bNode = formatNodeAddress(b);
                return aNode === bNode ? 0 : aNode > bNode ? 1 : -1;
            } else if (sortDetails.columnId === 'version' || sortDetails.columnId === 'javaVersion') {
                return nfCommon.sortVersion(a[sortDetails.columnId], b[sortDetails.columnId]);
            } else if (sortDetails.columnId === 'uptime') {
                var aDuration = nfCommon.parseDuration(a[sortDetails.columnId]);
                var bDuration = nfCommon.parseDuration(b[sortDetails.columnId]);
                return aDuration - bDuration;
            } else if (sortDetails.columnId.includes('Util')) {
                return parseFloat(a[sortDetails.columnId]) - parseFloat(b[sortDetails.columnId]);
            } else {
                var aString = nfCommon.isDefinedAndNotNull(a[sortDetails.columnId]) ? a[sortDetails.columnId] : '';
                var bString = nfCommon.isDefinedAndNotNull(b[sortDetails.columnId]) ? b[sortDetails.columnId] : '';
                return aString === bString ? 0 : aString > bString ? 1 : -1;
            }
        };

        // remove previous sort indicators
        $('#cluster-nodes-table span.queued-title').removeClass('sorted');
        $('#cluster-nodes-table span.queued-size-title').removeClass('sorted');

        // update/reset the count as appropriate
        if (prevColumn !== sortDetails.columnId) {
            count = 0;
        } else {
            count++;
        }

        // perform the sort
        dataView.sort(comparer, sortDetails.sortAsc);

        // record the previous table and sorted column
        prevColumn = sortDetails.columnId;
    };

    /**
     * Formats the address for the specified node.
     *
     * @param {object} node
     * @returns {string}
     */
    var formatNodeAddress = function (node) {
        return nfCommon.escapeHtml(node.address) + ':' + nfCommon.escapeHtml(node.apiPort);
    };

    /**
     * Formats the status for the specified node.
     *
     * @param {object} node
     * @returns {string}
     */
    var formatNodeStatus = function (node) {
        var markup = node.status;
        if (node.roles.includes(config.primaryNode)) {
            markup += ', PRIMARY';
        }
        if (node.roles.includes(config.clusterCoordinator)) {
            markup += ', COORDINATOR';
        }
        return nfCommon.escapeHtml(markup);
    }

    /**
     * Prompts to verify node connection.
     *
     * @argument {object} node     The node
     */
    var promptForConnect = function (node) {
        // prompt to connect
        nfDialog.showYesNoDialog({
            headerText: 'Connect Node',
            dialogContent: 'Connect \'' + formatNodeAddress(node) + '\' to this cluster?',
            yesHandler: function () {
                connect(node.nodeId);
            }
        });
    };

    /**
     * Connects the node in the specified row.
     *
     * @argument {string} nodeId     The node id
     */
    var connect = function (nodeId) {
        var entity = {
            'node': {
                'nodeId': nodeId,
                'status': 'CONNECTING'
            }
        };
        $.ajax({
            type: 'PUT',
            url: config.urls.nodes + '/' + encodeURIComponent(nodeId),
            data: JSON.stringify(entity),
            dataType: 'json',
            contentType: 'application/json'
        }).done(function (response) {
            var node = response.node;

            // update the node in the table
            var clusterGrid = $('#cluster-nodes-table').data('gridInstance');
            var clusterData = clusterGrid.getData();
            clusterData.updateItem(node.nodeId, node);
        }).fail(nfErrorHandler.handleAjaxError);
    };

    /**
     * Prompts to verify node offload.
     *
     * @argument {object} node     The node
     */
    var promptForOffload = function (node) {
        nfDialog.showYesNoDialog({
            headerText: 'Offload Node',
            dialogContent: 'Offload \'' + formatNodeAddress(node) + '\'?',
            yesHandler: function () {
                offload(node.nodeId);
            }
        });
    };

    /**
     * Offloads the node in the specified row.
     *
     * @argument {string} nodeId     The node id
     */
    var offload = function (nodeId) {
        var entity = {
            'node': {
                'nodeId': nodeId,
                'status': 'OFFLOADING'
            }
        };

        $.ajax({
            type: 'PUT',
            url: config.urls.nodes + '/' + encodeURIComponent(nodeId),
            data: JSON.stringify(entity),
            dataType: 'json',
            contentType: 'application/json'
        }).done(function (response) {
            var node = response.node;

            // update the node in the table
            var clusterGrid = $('#cluster-nodes-table').data('gridInstance');
            var clusterData = clusterGrid.getData();
            clusterData.updateItem(node.nodeId, node);
        }).fail(nfErrorHandler.handleAjaxError);
    };

    /**
     * Prompts to verify node disconnection.
     *
     * @argument {object} node     The node
     */
    var promptForDisconnect = function (node) {
        // prompt for disconnect
        nfDialog.showYesNoDialog({
            headerText: 'Disconnect Node',
            dialogContent: 'Disconnect \'' + formatNodeAddress(node) + '\' from the cluster?',
            yesHandler: function () {
                disconnect(node.nodeId);
            }
        });
    };

    /**
     * Disconnects the node in the specified row.
     *
     * @argument {string} nodeId     The node id
     */
    var disconnect = function (nodeId) {
        var entity = {
            'node': {
                'nodeId': nodeId,
                'status': 'DISCONNECTING'
            }
        };

        $.ajax({
            type: 'PUT',
            url: config.urls.nodes + '/' + encodeURIComponent(nodeId),
            data: JSON.stringify(entity),
            dataType: 'json',
            contentType: 'application/json'
        }).done(function (response) {
            var node = response.node;

            // update the node in the table
            var clusterGrid = $('#cluster-nodes-table').data('gridInstance');
            var clusterData = clusterGrid.getData();
            clusterData.updateItem(node.nodeId, node);
        }).fail(nfErrorHandler.handleAjaxError);
    };

    /**
     * Prompts to verify node disconnection.
     *
     * @argument {object} node     The node
     */
    var promptForRemoval = function (node) {
        // prompt for disconnect
        nfDialog.showYesNoDialog({
            headerText: 'Remove Node',
            dialogContent: 'Remove \'' + formatNodeAddress(node) + '\' from the cluster?',
            yesHandler: function () {
                remove(node.nodeId);
            }
        });
    };

    /**
     * Disconnects the node in the specified row.
     *
     * @argument {string} nodeId     The node id
     */
    var remove = function (nodeId) {
        $.ajax({
            type: 'DELETE',
            url: config.urls.nodes + '/' + encodeURIComponent(nodeId),
            dataType: 'json'
        }).done(function () {
            // get the table and update the row accordingly
            var clusterGrid = $('#cluster-nodes-table').data('gridInstance');
            var clusterData = clusterGrid.getData();
            clusterData.deleteItem(nodeId);
        }).fail(nfErrorHandler.handleAjaxError);
    };

    /**
     * Get the text out of the filter field. If the filter field doesn't
     * have any text it will contain the text 'filter list' so this method
     * accounts for that.
     */
    var getFilterText = function () {
        return $('#cluster-filter').val();
    };

    /**
     * Applies the filter found in the filter expression text field.
     */
    var applyFilter = function () {
        var visibleTab = getSelectedTab();
        if (!visibleTab) {
            return;
        }

        var grid = visibleTab.grid;

        // ensure the grid has been initialized
        if (nfCommon.isDefinedAndNotNull(grid)) {
            var gridData = grid.getData();

            // update the search criteria
            gridData.setFilterArgs({
                searchString: getFilterText(),
                property: $('#cluster-filter-type').combo('getSelectedOption').value
            });
            gridData.refresh();
        }
    };

    var getSelectedTab = function () {
        var selectedTab = null;
        clusterTabs.forEach(function (tab) {
            if ($('#' + tab.tableId).is(':visible')) {
                selectedTab = tab;
            }
        });
        return selectedTab;
    }

    /**
     * Performs the filtering.
     *
     * @param {object} item     The item subject to filtering
     * @param {object} args     Filter arguments
     * @returns {Boolean}       Whether or not to include the item
     */
    var filter = function (item, args) {
        if (args.searchString === '') {
            return true;
        }

        try {
            // perform the row filtering
            var filterExp = new RegExp(args.searchString, 'i');
        } catch (e) {
            // invalid regex
            return false;
        }

        var searchText = item[args.property];
        if (args.property === 'address') {
            searchText = formatNodeAddress(item);
        } else if (args.property === 'status') {
            searchText = formatNodeStatus(item);
        }

        // perform the filter
        return searchText.search(filterExp) >= 0;
    };

    /**
     * Updates count of displayed and total rows.
     */
    function updateFilterStats(selectedTab) {
        if (!selectedTab) {
            selectedTab = getSelectedTab();
        }
        if (selectedTab.dataView) {
            var displayedRows = selectedTab.dataView.getLength();
            var totalRows = selectedTab.rowCount;
            $('#displayed-rows').text(displayedRows);
            $('#total-rows').text(totalRows);
        }
    }

    /**
     * Clears any existing table filter.
     */
    var clearFilter = function () {
        $('#cluster-filter').val('');
        applyFilter();
    }

    /**
     * Show the node details.
     *
     * @argument {object} item     The item
     */
    var showNodeDetails = function (item) {
        $.ajax({
            type: 'GET',
            url: config.urls.nodes + '/' + encodeURIComponent(item.nodeId),
            dataType: 'json'
        }).done(function (response) {
            var node = response.node;

            // update the dialog fields
            $('#node-id').text(node.nodeId);
            $('#node-address').text(formatNodeAddress(node));

            // format the events
            var events = $('#node-events');
            if ($.isArray(node.events) && node.events.length > 0) {
                var eventMessages = [];
                $.each(node.events, function (i, event) {
                    eventMessages.push(event.timestamp + ": " + event.message);
                });
                $('<div></div>').append(nfCommon.formatUnorderedList(eventMessages)).appendTo(events);
            } else {
                events.append('<div><span class="unset">None</span></div>');
            }

            // show the dialog
            $('#node-details-dialog').modal('show');
        }).fail(nfErrorHandler.handleAjaxError);
    };

    /**
     * Applies system diagnostics data to the JVM tab.
     */
    function updateJvmTableData(systemDiagnosticsResponse) {
        if (nfCommon.isDefinedAndNotNull(systemDiagnosticsResponse.systemDiagnostics)
            && nfCommon.isDefinedAndNotNull(systemDiagnosticsResponse.systemDiagnostics.nodeSnapshots)) {

            var jvmTableRows = [];
            systemDiagnosticsResponse.systemDiagnostics.nodeSnapshots.forEach(function (nodeSnapshot) {
                var snapshot = nodeSnapshot.snapshot;

                // add the node jvm details
                jvmTableRows.push({
                    id: nodeSnapshot.nodeId,
                    node: nodeSnapshot.address + ':' + nodeSnapshot.apiPort,
                    address: nodeSnapshot.address,
                    maxHeap: snapshot.maxHeap,
                    totalHeap: snapshot.totalHeap,
                    usedHeap: snapshot.usedHeap,
                    heapUtilization: snapshot.heapUtilization,
                    maxNonHeap: snapshot.maxNonHeap,
                    totalNonHeap: snapshot.totalNonHeap,
                    usedNonHeap: snapshot.usedNonHeap,
                    garbageCollection: snapshot.garbageCollection,
                    uptime: snapshot.uptime
                });
            });
            jvmTab.rowCount = jvmTableRows.length;
            jvmTab.dataView.setItems(jvmTableRows);
            jvmTab.dataView.reSort();
            jvmTab.grid.invalidate();
        } else {
            jvmTab.rowCount = 0;
        }
    }

    /**
     * Applies system diagnostics data to the System tab.
     */
    function updateSystemTableData(systemDiagnosticsResponse) {
        if (nfCommon.isDefinedAndNotNull(systemDiagnosticsResponse.systemDiagnostics)
            && nfCommon.isDefinedAndNotNull(systemDiagnosticsResponse.systemDiagnostics.nodeSnapshots)) {

            var systemTableRows = [];
            systemDiagnosticsResponse.systemDiagnostics.nodeSnapshots.forEach(function (nodeSnapshot) {
                var snapshot = nodeSnapshot.snapshot;
                systemTableRows.push({
                    id: nodeSnapshot.nodeId,
                    node: nodeSnapshot.address + ':' + nodeSnapshot.apiPort,
                    address: nodeSnapshot.address,
                    processors: snapshot.availableProcessors,
                    processorLoadAverage: snapshot.processorLoadAverage,
                    totalThreads: snapshot.totalThreads,
                    daemonThreads: snapshot.daemonThreads
                });
            });
            systemTab.rowCount = systemTableRows.length;
            systemTab.dataView.setItems(systemTableRows);
            systemTab.dataView.reSort();
            systemTab.grid.invalidate();
        } else {
            systemTab.rowCount = 0;
        }
    }

    /**
     * Applies system diagnostics data to the FlowFile Storage tab.
     */
    function updateFlowFileTableData(systemDiagnosticsResponse) {
        if (nfCommon.isDefinedAndNotNull(systemDiagnosticsResponse.systemDiagnostics)
            && nfCommon.isDefinedAndNotNull(systemDiagnosticsResponse.systemDiagnostics.nodeSnapshots)) {

            var flowFileTableRows = [];
            systemDiagnosticsResponse.systemDiagnostics.nodeSnapshots.forEach(function (nodeSnapshot) {
                var snapshot = nodeSnapshot.snapshot;
                flowFileTableRows.push({
                    id: nodeSnapshot.nodeId,
                    node: nodeSnapshot.address + ':' + nodeSnapshot.apiPort,
                    address: nodeSnapshot.address,
                    ffRepoTotal: snapshot.flowFileRepositoryStorageUsage.totalSpace,
                    ffRepoUsed: snapshot.flowFileRepositoryStorageUsage.usedSpace,
                    ffRepoFree: snapshot.flowFileRepositoryStorageUsage.freeSpace,
                    ffRepoUtil: snapshot.flowFileRepositoryStorageUsage.utilization
                });
            });
            flowFileTab.rowCount = flowFileTableRows.length;
            flowFileTab.dataView.setItems(flowFileTableRows);
            flowFileTab.dataView.reSort();
            flowFileTab.grid.invalidate();
        } else {
            flowFileTab.rowCount = 0;
        }
    }

    /**
     * Applies system diagnostics data to the Content Storage tab.
     */
    function updateContentTableData(systemDiagnosticsResponse) {
        if (nfCommon.isDefinedAndNotNull(systemDiagnosticsResponse.systemDiagnostics)
            && nfCommon.isDefinedAndNotNull(systemDiagnosticsResponse.systemDiagnostics.nodeSnapshots)) {

            var contentStorageTableRows = [];
            systemDiagnosticsResponse.systemDiagnostics.nodeSnapshots.forEach(function (nodeSnapshot) {
                var snapshot = nodeSnapshot.snapshot;
                snapshot.contentRepositoryStorageUsage.forEach(function (contentRepoUsage) {
                    contentStorageTableRows.push({
                        id: nodeSnapshot.nodeId + ':' + contentRepoUsage.identifier,
                        address: nodeSnapshot.address,
                        node: nodeSnapshot.address + ':' + nodeSnapshot.apiPort,
                        contentRepoId: contentRepoUsage.identifier,
                        contentRepoTotal: contentRepoUsage.totalSpace,
                        contentRepoUsed: contentRepoUsage.usedSpace,
                        contentRepoFree: contentRepoUsage.freeSpace,
                        contentRepoUtil: contentRepoUsage.utilization
                    });
                });
            });

            contentTab.rowCount = contentStorageTableRows.length;
            contentTab.dataView.setItems(contentStorageTableRows);
            contentTab.dataView.reSort();
            contentTab.grid.invalidate();
        } else {
            contentTab.rowCount = 0;
        }
    }

    /**
     * Applies system diagnostics data to the Provenance Storage tab.
     */
    function updateProvenanceTableData(systemDiagnosticsResponse) {
        if (nfCommon.isDefinedAndNotNull(systemDiagnosticsResponse.systemDiagnostics)
            && nfCommon.isDefinedAndNotNull(systemDiagnosticsResponse.systemDiagnostics.nodeSnapshots)) {

            var provenanceStorageTableRows = [];
            systemDiagnosticsResponse.systemDiagnostics.nodeSnapshots.forEach(function (nodeSnapshot) {
                var snapshot = nodeSnapshot.snapshot;
                snapshot.provenanceRepositoryStorageUsage.forEach(function (provenanceRepoUsage) {
                	provenanceStorageTableRows.push({
                        id: nodeSnapshot.nodeId + ':' + provenanceRepoUsage.identifier,
                        address: nodeSnapshot.address,
                        node: nodeSnapshot.address + ':' + nodeSnapshot.apiPort,
                        provenanceRepoId: provenanceRepoUsage.identifier,
                        provenanceRepoTotal: provenanceRepoUsage.totalSpace,
                        provenanceRepoUsed: provenanceRepoUsage.usedSpace,
                        provenanceRepoFree: provenanceRepoUsage.freeSpace,
                        provenanceRepoUtil: provenanceRepoUsage.utilization
                    });
                });
            });

            provenanceTab.rowCount = provenanceStorageTableRows.length;
            provenanceTab.dataView.setItems(provenanceStorageTableRows);
            provenanceTab.dataView.reSort();
            provenanceTab.grid.invalidate();
        } else {
        	provenanceTab.rowCount = 0;
        }
    }

    /**
     * Applies system diagnostics data to the Versions tab.
     */
    function updateVersionTableData(systemDiagnosticsResponse) {
        if (nfCommon.isDefinedAndNotNull(systemDiagnosticsResponse.systemDiagnostics)
            && nfCommon.isDefinedAndNotNull(systemDiagnosticsResponse.systemDiagnostics.nodeSnapshots)) {

            var versionTableRows = [];
            systemDiagnosticsResponse.systemDiagnostics.nodeSnapshots.forEach(function (nodeSnapshot) {
                var snapshot = nodeSnapshot.snapshot;
                versionTableRows.push({
                    id: nodeSnapshot.nodeId,
                    address: nodeSnapshot.address,
                    node: nodeSnapshot.address + ':' + nodeSnapshot.apiPort,
                    version: snapshot.versionInfo.niFiVersion,
                    javaVendor: snapshot.versionInfo.javaVendor,
                    javaVersion: snapshot.versionInfo.javaVersion,
                    osName: snapshot.versionInfo.osName,
                    osVersion: snapshot.versionInfo.osVersion,
                    osArchitecture: snapshot.versionInfo.osArchitecture
                });
            });

            versionTab.dataView.setItems(versionTableRows);
            versionTab.dataView.reSort();
            versionTab.grid.invalidate();
        }
    }

    /**
     * Loads system diagnostics data for the cluster.
     */
    function refreshSystemDiagnosticsData() {
        var systemDiagnosticsUri = config.urls.systemDiagnostics
        var loadPromise = $.ajax({
            type: 'GET',
            url: systemDiagnosticsUri,
            data: {
                nodewise: true
            },
            dataType: 'json'
        }).done(function (systemDiagnosticsResponse) {
            var handlers = dataSetHandlers['systemDiagnostics'];
            handlers.forEach(function (handler) {
                handler(systemDiagnosticsResponse);
            });
        }).fail(nfErrorHandler.handleAjaxError);
        return loadPromise;
    };

    /**
     * Generic initialization for Slick Grid tables
     */
    function commonTableInit(tab) {
        var dataView = new Slick.Data.DataView({
            inlineFilters: false
        });
        dataView.setItems([], tab.tableIdColumn);

        dataView.setFilterArgs({
            searchString: getFilterText(),
            property: $('#cluster-filter-type').combo('getSelectedOption').value
        });
        dataView.setFilter(filter);

        // initialize the sort
        tab.onSort({
            columnId: tab.tableIdColumn,
            sortAsc: true
        }, dataView);

        // initialize the grid
        var columnModel = tab.tableColumnModel;
        if (typeof columnModel === 'function') {
            columnModel = columnModel();
        }
        var grid = new Slick.Grid('#' + tab.tableId, dataView, columnModel, tab.tableOptions);
        grid.setSelectionModel(new Slick.RowSelectionModel());
        grid.setSortColumn(tab.tableIdColumn, true);
        grid.onSort.subscribe(function (e, args) {
            tab.onSort({
                columnId: args.sortCol.field,
                sortAsc: args.sortAsc
            }, dataView, tab);
        });

        // wire up the dataview to the grid
        dataView.onRowCountChanged.subscribe(function (e, args) {
            grid.updateRowCount();
            grid.render();
            updateFilterStats(tab);
        });
        dataView.onRowsChanged.subscribe(function (e, args) {
            grid.invalidateRows(args.rows);
            grid.render();
        });

        // click events
        if (tab.tableOnClick) {
            grid.onClick.subscribe(function (e, args) {
                var target = $(e.target);
                var item = dataView.getItem(args.row);
                tab.tableOnClick(e, args, target, item);
            });
        }

        // hold onto an instance of the grid
        var table = $('#' + tab.tableId).data('gridInstance', grid);

        // enter events
        if (typeof tab.createTableOnEnter === 'function') {
            table.on('mouseenter', 'div.slick-cell', tab.createTableOnEnter(grid));
        }

        // clean up
        if (typeof tab.cleanUpTable === 'function') {
            grid.onViewportChanged.subscribe(function (e, args) {
                tab.cleanUpTable(table);
            });
        }

        tab.dataView = dataView;
        tab.grid = grid;
    };

    /**
     * Apply the cluster nodes data set to the table.
     */
    function updateNodesTableData(clusterResponse) {
        var cluster = clusterResponse.cluster;

        // ensure there are groups specified
        if (nfCommon.isDefinedAndNotNull(cluster.nodes)) {
            var clusterGrid = nodesTab.grid;
            var clusterData = clusterGrid.getData();

            // set the items
            nodesTab.rowCount = cluster.nodes.length;
            clusterData.setItems(cluster.nodes);
            clusterData.reSort();
            clusterGrid.invalidate();

            // update the stats last refreshed timestamp
            $('#cluster-last-refreshed').text(cluster.generated);
        } else {
            $('#total-nodes').text('0');
        }
    }

    /**
     * Refreshes cluster data sets from the server.
     */
    function refreshClusterData() {
        var clusterNodesDataPromise = $.ajax({
            type: 'GET',
            url: config.urls.cluster,
            dataType: 'json'
        }).done(function (response) {
            var handlers = dataSetHandlers['cluster'];
            handlers.forEach(function (handler) {
                handler(response);
            });
        }).fail(nfErrorHandler.handleAjaxError);
        return clusterNodesDataPromise;
    }

    /**
     * Event handler triggered when the user switches tabs.
     */
    function onSelectTab(tab) {
        // Resize table
        var tabGrid = tab.grid;
        if (nfCommon.isDefinedAndNotNull(tabGrid)) {
            tabGrid.resizeCanvas();
        }

        // Clear filter text
        clearFilter();

        // Reset filter options
        $('#cluster-filter-type').combo({
            options: tab.filterOptions,
            select: function (option) {
                applyFilter();
            }
        });

        updateFilterStats(tab);
    }

    var nfClusterTable = {
        /**
         * Initializes the cluster list.
         */
        init: function () {
            // initialize the user details dialog
            $('#node-details-dialog').modal({
                scrollableContentStyle: 'scrollable',
                headerText: 'Node Details',
                buttons: [{
                    buttonText: 'Ok',
                    color: {
                        base: '#728E9B',
                        hover: '#004849',
                        text: '#ffffff'
                    },
                    handler: {
                        click: function () {
                            $('#node-details-dialog').modal('hide');
                        }
                    }
                }],
                handler: {
                    close: function () {
                        // clear the details
                        $('#node-address').text('');
                        $('#node-id').text('');
                        $('#node-events').empty();
                    }
                }
            });

            // define the function for filtering the list
            $('#cluster-filter').keyup(function () {
                applyFilter();
            });

            // Authorize data sets
            var dataSetAuthorized = {};
            config.data = config.data.filter(function (dataSetConfig) {
                dataSetConfig.authorized = dataSetConfig.isAuthorized();
                dataSetAuthorized[dataSetConfig.name] = dataSetConfig.authorized;
                if (dataSetConfig.authorized) {
                    return true;
                } else {
                    return false;
                }
            });

            // Filter tabs to authorized data sets
            clusterTabs = clusterTabs.filter(function (tab) {
                var tabDataSet = tab.data.dataSet;
                if (dataSetAuthorized[tabDataSet]) {
                    return true;
                } else {
                    return false;
                }
            });
            clusterTabs.forEach(function (tab) {
                tabsByName[tab.name] = tab;
                var dataSetHandlerList = dataSetHandlers[tab.data.dataSet];
                if (dataSetHandlerList) {
                    dataSetHandlers[tab.data.dataSet] = dataSetHandlerList.concat([tab.data.update]);
                } else {
                    dataSetHandlers[tab.data.dataSet] = [tab.data.update];
                }
            });

            // Initialize tab set
            $('#cluster-tabs').tabbs({
                tabStyle: 'tab',
                selectedTabStyle: 'selected-tab',
                scrollableTabContentStyle: 'scrollable',
                tabs: clusterTabs,
                select: function () {
                    var tab = $(this).text();
                    var selectedTab = tabsByName[tab];
                    if (selectedTab) {
                        selectedTab.onTabSelected(selectedTab);
                    } else {
                        console.error('Failed to match tab: ', tab, tabsByName);
                    }
                }
            });

            // listen for browser resize events to update the page size
            $(window).resize(function () {
                nfClusterTable.resetTableSize();
            });

            // initialize tabs
            clusterTabs.forEach(function (tab) {
                try {
                    tab.init(tab);
                } catch (ex) {
                    console.error('Failed to initialize tab', tab, ex);
                }
            });
        },

        /**
         * Update the size of the grid based on its container's current size.
         */
        resetTableSize: function () {
            clusterTabs.forEach(function (tab) {
                if (tab && tab.grid) {
                    tab.grid.resizeCanvas();
                }
            });
        },

        /**
         * Load the processor cluster table.
         */
        loadClusterTable: function () {
            var updateDataDeferreds = config.data.map(function (dataSetSpec) {
                var dataSetDeferred = dataSetSpec.update();
                return dataSetDeferred;
            });
            var aggregateDeferred = $.when.apply($, updateDataDeferreds);
            aggregateDeferred = aggregateDeferred.done(function (aggregateResult) {
                updateFilterStats(nodesTab);
            });
            return aggregateDeferred;
        }
    };

    return nfClusterTable;
}));
