diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/CurrentUserEntity.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/CurrentUserEntity.java index 7bb627040d..76a692e0cd 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/CurrentUserEntity.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-client-dto/src/main/java/org/apache/nifi/web/api/entity/CurrentUserEntity.java @@ -35,6 +35,7 @@ public class CurrentUserEntity extends Entity { private PermissionsDTO tenantsPermissions; private PermissionsDTO controllerPermissions; private PermissionsDTO policiesPermissions; + private PermissionsDTO systemPermissions; /** * @return the user identity being serialized @@ -119,4 +120,16 @@ public class CurrentUserEntity extends Entity { public void setPoliciesPermissions(PermissionsDTO policiesPermissions) { this.policiesPermissions = policiesPermissions; } + + /** + * @return permissions for accessing the system + */ + @ApiModelProperty("Permissions for accessing system.") + public PermissionsDTO getSystemPermissions() { + return systemPermissions; + } + + public void setSystemPermissions(PermissionsDTO systemPermissions) { + this.systemPermissions = systemPermissions; + } } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/AuthorizableLookup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/AuthorizableLookup.java index 350410ae97..6f3c01b527 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/AuthorizableLookup.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/AuthorizableLookup.java @@ -262,4 +262,13 @@ public interface AuthorizableLookup { * @return authorizable */ Authorizable getAuthorizableFromResource(final String resource); + + + /** + * Get the authorizable for access to the System resource. + * + * @return authorizable + */ + Authorizable getSystem(); + } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/StandardAuthorizableLookup.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/StandardAuthorizableLookup.java index 33c162a145..52af28579a 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/StandardAuthorizableLookup.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/authorization/StandardAuthorizableLookup.java @@ -98,6 +98,18 @@ class StandardAuthorizableLookup implements AuthorizableLookup { } }; + private static final Authorizable SYSTEM_AUTHORIZABLE = new Authorizable() { + @Override + public Authorizable getParentAuthorizable() { + return null; + } + + @Override + public Resource getResource() { + return ResourceFactory.getSystemResource(); + } + }; + // nifi core components private ControllerFacade controllerFacade; @@ -708,6 +720,11 @@ class StandardAuthorizableLookup implements AuthorizableLookup { return group.findConnectable(id); } + @Override + public Authorizable getSystem() { + return SYSTEM_AUTHORIZABLE; + } + public void setProcessorDAO(ProcessorDAO processorDAO) { this.processorDAO = processorDAO; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java index 1412db76f9..491d448ee4 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-api/src/main/java/org/apache/nifi/web/StandardNiFiServiceFacade.java @@ -2919,6 +2919,7 @@ public class StandardNiFiServiceFacade implements NiFiServiceFacade { entity.setTenantsPermissions(dtoFactory.createPermissionsDto(authorizableLookup.getTenant())); entity.setControllerPermissions(dtoFactory.createPermissionsDto(authorizableLookup.getController())); entity.setPoliciesPermissions(dtoFactory.createPermissionsDto(authorizableLookup.getPolicies())); + entity.setSystemPermissions(dtoFactory.createPermissionsDto(authorizableLookup.getSystem())); return entity; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/cluster.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/cluster.jsp index baa4c6a82b..dbb1ef56b8 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/cluster.jsp +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/pages/cluster.jsp @@ -23,6 +23,7 @@ ${nf.cluster.style.tags} + @@ -35,6 +36,7 @@ + diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/cluster/cluster-content.jsp b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/cluster/cluster-content.jsp index 3874e24dba..02bcf610db 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/cluster/cluster-content.jsp +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/cluster/cluster-content.jsp @@ -18,17 +18,34 @@
NiFi Cluster
-
+
+
+
+
- Displaying  of  + Displaying  of 
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/cluster.css b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/cluster.css index a4224be56d..c71077d0b5 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/cluster.css +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/css/cluster.css @@ -27,8 +27,8 @@ overflow: auto; } -#cluster-header-and-filter { - height: 98px; +#cluster-filter-controls { + z-index: 1; } #cluster-header-text { @@ -39,6 +39,10 @@ margin-bottom: 30px; } +#cluster-tabs-content { + top: 80px; +} + #cluster-refresh-container { position: absolute; bottom: 0px; @@ -63,17 +67,27 @@ padding-top: 10px; } -/* counters table */ +/* cluster table */ -#cluster-table { +.cluster-tabbed-table { position: absolute; - top: 98px; + top: 70px; left: 0px; bottom: 47px; right: 0px; min-height: 150px; } +#cluster-tabs-content .slick-cell.cell-right { + text-align: right; + padding-right: 16px; + padding-left: 2px; +} + +#cluster-tabs-content .slick-header-column.header-right { + text-align: right; +} + span.sorted { text-decoration: underline; } diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/cluster/nf-cluster-table.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/cluster/nf-cluster-table.js index e9a9e0e024..8bc169b302 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/cluster/nf-cluster-table.js +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/cluster/nf-cluster-table.js @@ -24,13 +24,261 @@ nf.ClusterTable = (function () { */ var config = { primaryNode: 'Primary Node', - clusterCoorindator: 'Cluster Coordinator', + clusterCoordinator: 'Cluster Coordinator', urls: { cluster: '../nifi-api/controller/cluster', - nodes: '../nifi-api/controller/cluster/nodes' - } + nodes: '../nifi-api/controller/cluster/nodes', + systemDiagnostics: '../nifi-api/system-diagnostics' + }, + data: [{ + name: 'cluster', + update: refreshClusterData, + isAuthorized: nf.Common.canAccessController + },{ + name: 'systemDiagnostics', + update: refreshSystemDiagnosticsData, + isAuthorized: nf.Common.canAccessSystem + } + ] }; + var commonTableOptions = { + forceFitColumns: true, + 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, + 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: [ + {id: 'node', field: 'node', name: 'Node Address', sortable: true, resizable: true}, + {id: 'heapMax', field: 'maxHeap', name: 'Heap Max', sortable: true, resizable: true, cssClass: 'cell-right', headerCssClass: 'header-right'}, + {id: 'heapTotal', field: 'totalHeap', name: 'Heap Total', sortable: true, resizable: true, cssClass: 'cell-right', headerCssClass: 'header-right'}, + {id: 'heapUsed', field: 'usedHeap', name: 'Heap Used', sortable: true, resizable: true, cssClass: 'cell-right', headerCssClass: 'header-right'}, + {id: 'heapUtilPct', field: 'heapUtilization', name: 'Heap Utilization', sortable: true, resizable: true, cssClass: 'cell-right', headerCssClass: 'header-right'}, + {id: 'nonHeapTotal', field: 'totalNonHeap', name: 'Non-Heap Total', sortable: true, resizable: true, cssClass: 'cell-right', headerCssClass: 'header-right'}, + {id: 'nonHeapUsed', field: 'usedNonHeap', name: 'Non-Heap Used', sortable: true, resizable: true, cssClass: 'cell-right', headerCssClass: 'header-right'}, + {id: 'gcOldGen', field: 'gcOldGen', name: 'G1 Old Generation', sortable: true, resizable: true, cssClass: 'cell-right', headerCssClass: 'header-right'}, + {id: 'gcNewGen', field: 'gcNewGen', name: 'G1 Young Generation', sortable: true, resizable: true, cssClass: 'cell-right', headerCssClass: 'header-right'} + ], + tableIdColumn: 'id', + tableOptions: commonTableOptions, + tableOnClick: null, + 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}, + {id: 'processors', field: 'processors', name: 'Processors', sortable: true, resizable: true, cssClass: 'cell-right', headerCssClass: 'header-right'}, + {id: 'processorLoadAverage', field: 'processorLoadAverage', name: 'Processor Load Average', sortable: true, resizable: true, cssClass: 'cell-right', headerCssClass: 'header-right'}, + {id: 'totalThreads', field: 'totalThreads', name: 'Total Threads', sortable: true, resizable: true, cssClass: 'cell-right', headerCssClass: 'header-right'}, + {id: 'daemonThreads', field: 'daemonThreads', name: 'Daemon Threads', sortable: true, resizable: true, cssClass: 'cell-right', headerCssClass: 'header-right'} + ], + tableIdColumn: 'id', + tableOptions: commonTableOptions, + tableOnClick: 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}, + {id: 'ffRepoTotal', field: 'ffRepoTotal', name: 'Total Space', sortable: true, resizable: true, cssClass: 'cell-right', headerCssClass: 'header-right'}, + {id: 'ffRepoUsed', field: 'ffRepoUsed', name: 'Used Space', sortable: true, resizable: true, cssClass: 'cell-right', headerCssClass: 'header-right'}, + {id: 'ffRepoFree', field: 'ffRepoFree', name: 'Free Space', sortable: true, resizable: true, cssClass: 'cell-right', headerCssClass: 'header-right'}, + {id: 'ffStoreUtil', field: 'ffRepoUtil', name: 'Utilization', sortable: true, resizable: true, cssClass: 'cell-right', headerCssClass: 'header-right'} + ], + tableIdColumn: 'id', + tableOptions: commonTableOptions, + tableOnClick: 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}, + {id: 'contentRepoId', field: 'contentRepoId', name: 'Content Repository', sortable: true, resizable: true}, + {id: 'contentRepoTotal', field: 'contentRepoTotal', name: 'Total Space', sortable: true, resizable: true, cssClass: 'cell-right', headerCssClass: 'header-right'}, + {id: 'contentRepoUsed', field: 'contentRepoUsed', name: 'Used Space', sortable: true, resizable: true, cssClass: 'cell-right', headerCssClass: 'header-right'}, + {id: 'contentRepoFree', field: 'contentRepoFree', name: 'Free Space', sortable: true, resizable: true, cssClass: 'cell-right', headerCssClass: 'header-right'}, + {id: 'contentRepoUtil', field: 'contentRepoUtil', name: 'Utilization', sortable: true, resizable: true, cssClass: 'cell-right', headerCssClass: 'header-right'} + ], + tableIdColumn: 'id', + tableOptions: commonTableOptions, + tableOnClick: null, + init: commonTableInit, + onSort: sort, + onTabSelected: onSelectTab, + filterOptions: [{ + text: 'by address', + value: 'node' + }, { + text: 'by repository', + value: 'contentRepoId' + }] + }; + + var clusterTabs = [nodesTab, systemTab, jvmTab, flowFileTab, contentTab]; + 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-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 '
'; + }; + + // 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 nf.Common.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: ' ', 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}, + {id: 'queued', field: 'queued', name: 'Queue / Size', resizable: true, sortable: true, defaultSortAsc: false}, + {id: 'status', field: 'status', name: 'Status', formatter: statusFormatter, resizable: true, sortable: true}, + {id: 'uptime', field: 'nodeStartTime', name: 'Uptime', 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 (nf.Common.canModifyController()) { + var actionFormatter = function (row, cell, value, columnDef, dataContext) { + var canDisconnect = false; + var canConnect = false; + + // determine if this node is already the primary + var isPrimary = dataContext.primary; + + // determine the current status + if (dataContext.status === 'CONNECTED' || dataContext.status === 'CONNECTING') { + canDisconnect = true; + } else if (dataContext.status === 'DISCONNECTED') { + canConnect = true; + } + + // return the appropriate markup + if (canConnect) { + return '
 
'; + } else if (canDisconnect) { + return '
'; + } else { + return '
 
'; + } + }; + + columnModel.push({id: 'actions', label: ' ', formatter: actionFormatter, resizable: false, sortable: false, width: 80, maxWidth: 80}); + } + + return columnModel; + } + var prevColumn, count; /** @@ -39,7 +287,7 @@ nf.ClusterTable = (function () { * @param {object} sortDetails * @param {object} data */ - var sort = function (sortDetails, data) { + function sort (sortDetails, dataView, tab) { // defines a function for sorting var comparer = function (a, b) { if (sortDetails.columnId === 'heartbeat' || sortDetails.columnId === 'uptime') { @@ -51,27 +299,40 @@ nf.ClusterTable = (function () { var bSplit = b[sortDetails.columnId].split(/ \/ /); var mod = count % 4; if (mod < 2) { - $('#cluster-table span.queued-title').addClass('sorted'); + $('#cluster-nodes-table span.queued-title').addClass('sorted'); var aCount = nf.Common.parseCount(aSplit[0]); var bCount = nf.Common.parseCount(bSplit[0]); return aCount - bCount; } else { - $('#cluster-table span.queued-size-title').addClass('sorted'); + $('#cluster-nodes-table span.queued-size-title').addClass('sorted'); var aSize = nf.Common.parseSize(aSplit[1]); var bSize = nf.Common.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 === 'ffRepoTotal' || sortDetails.columnId === 'ffRepoUsed' + || sortDetails.columnId === 'ffRepoFree' || sortDetails.columnId === 'contentRepoTotal' + || sortDetails.columnId === 'contentRepoUsed' || sortDetails.columnId === 'contentRepoFree') { + var aSize = nf.Common.parseSize(a[sortDetails.columnId]); + var bSize = nf.Common.parseSize(b[sortDetails.columnId]); + return aSize - bSize; + } else if (sortDetails.columnId === 'totalThreads' || sortDetails.columnId === 'daemonThreads' + || sortDetails.columnId === 'processors') { + var aCount = nf.Common.parseCount(a[sortDetails.columnId]); + var bCount = nf.Common.parseCount(b[sortDetails.columnId]); + return aCount - bCount; + } else if (sortDetails.columnId === 'gcOldGen' || sortDetails.columnId === 'gcNewGen') { + var aSplit = a[sortDetails.columnId].split(/ /); + var bSplit = b[sortDetails.columnId].split(/ /); + var aCount = nf.Common.parseCount(aSplit[0]); + var bCount = nf.Common.parseCount(bSplit[0]); + return aCount - bCount; } else if (sortDetails.columnId === 'status') { - var aString = nf.Common.isDefinedAndNotNull(a[sortDetails.columnId]) ? a[sortDetails.columnId] : ''; - if (a.roles.includes(config.primaryNode)) { - aString += ', PRIMARY'; - } - var bString = nf.Common.isDefinedAndNotNull(b[sortDetails.columnId]) ? b[sortDetails.columnId] : ''; - if (b.roles.includes(config.primaryNode)) { - bString += ', PRIMARY'; - } - return aString === bString ? 0 : aString > bString ? 1 : -1; - } else if (sortDetails.columnId === 'node') { + 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; @@ -83,8 +344,8 @@ nf.ClusterTable = (function () { }; // remove previous sort indicators - $('#cluster-table span.queued-title').removeClass('sorted'); - $('#cluster-table span.queued-size-title').removeClass('sorted'); + $('#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) { @@ -94,14 +355,14 @@ nf.ClusterTable = (function () { } // perform the sort - data.sort(comparer, sortDetails.sortAsc); + dataView.sort(comparer, sortDetails.sortAsc); // record the previous table and sorted column prevColumn = sortDetails.columnId; }; /** - * Formats the address for the specified noe. + * Formats the address for the specified node. * * @param {object} node * @returns {string} @@ -110,6 +371,23 @@ nf.ClusterTable = (function () { return nf.Common.escapeHtml(node.address) + ':' + nf.Common.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 markup; + } + /** * Prompts to verify node connection. * @@ -138,7 +416,6 @@ nf.ClusterTable = (function () { 'status': 'CONNECTING' } }; - $.ajax({ type: 'PUT', url: config.urls.nodes + '/' + encodeURIComponent(nodeId), @@ -149,7 +426,7 @@ nf.ClusterTable = (function () { var node = response.node; // update the node in the table - var clusterGrid = $('#cluster-table').data('gridInstance'); + var clusterGrid = $('#cluster-nodes-table').data('gridInstance'); var clusterData = clusterGrid.getData(); clusterData.updateItem(node.nodeId, node); }).fail(nf.Common.handleAjaxError); @@ -194,7 +471,7 @@ nf.ClusterTable = (function () { var node = response.node; // update the node in the table - var clusterGrid = $('#cluster-table').data('gridInstance'); + var clusterGrid = $('#cluster-nodes-table').data('gridInstance'); var clusterData = clusterGrid.getData(); clusterData.updateItem(node.nodeId, node); }).fail(nf.Common.handleAjaxError); @@ -228,7 +505,7 @@ nf.ClusterTable = (function () { dataType: 'json' }).done(function () { // get the table and update the row accordingly - var clusterGrid = $('#cluster-table').data('gridInstance'); + var clusterGrid = $('#cluster-nodes-table').data('gridInstance'); var clusterData = clusterGrid.getData(); clusterData.deleteItem(nodeId); }).fail(nf.Common.handleAjaxError); @@ -247,22 +524,36 @@ nf.ClusterTable = (function () { * Applies the filter found in the filter expression text field. */ var applyFilter = function () { - // get the dataview - var clusterGrid = $('#cluster-table').data('gridInstance'); + var visibleTab = getSelectedTab(); + if (!visibleTab) { + return; + } + + var grid = visibleTab.grid; // ensure the grid has been initialized - if (nf.Common.isDefinedAndNotNull(clusterGrid)) { - var clusterData = clusterGrid.getData(); + if (nf.Common.isDefinedAndNotNull(grid)) { + var gridData = grid.getData(); // update the search criteria - clusterData.setFilterArgs({ + gridData.setFilterArgs({ searchString: getFilterText(), property: $('#cluster-filter-type').combo('getSelectedOption').value }); - clusterData.refresh(); + gridData.refresh(); } }; + var getSelectedTab = function () { + var selectedTab = null; + clusterTabs.forEach(function (tab) { + if ($('#' + tab.tableId).is(':visible')) { + selectedTab = tab; + } + }); + return selectedTab; + } + /** * Performs the filtering. * @@ -283,10 +574,40 @@ nf.ClusterTable = (function () { 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 item[args.property].search(filterExp) >= 0; + 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. * @@ -320,7 +641,282 @@ nf.ClusterTable = (function () { $('#node-details-dialog').modal('show'); }).fail(nf.Common.handleAjaxError); }; - + + /** + * Applies system diagnostics data to the JVM tab. + */ + function updateJvmTableData (systemDiagnosticsResponse) { + if (nf.Common.isDefinedAndNotNull(systemDiagnosticsResponse.systemDiagnostics) + && nf.Common.isDefinedAndNotNull(systemDiagnosticsResponse.systemDiagnostics.nodeSnapshots)) { + + var jvmTableRows = []; + systemDiagnosticsResponse.systemDiagnostics.nodeSnapshots.forEach(function (nodeSnapshot) { + var snapshot = nodeSnapshot.snapshot; + 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, + gcOldGen: snapshot.garbageCollection[0].collectionCount + ' times (' + + snapshot.garbageCollection[0].collectionTime + ')', + gcNewGen: snapshot.garbageCollection[1].collectionCount + ' times (' + + snapshot.garbageCollection[1].collectionTime + ')' + }); + }); + 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 (nf.Common.isDefinedAndNotNull(systemDiagnosticsResponse.systemDiagnostics) + && nf.Common.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 (nf.Common.isDefinedAndNotNull(systemDiagnosticsResponse.systemDiagnostics) + && nf.Common.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 (nf.Common.isDefinedAndNotNull(systemDiagnosticsResponse.systemDiagnostics) + && nf.Common.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; + } + } + + /** + * 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(nf.Common.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 + $('#' + tab.tableId).data('gridInstance', grid); + 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 (nf.Common.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(nf.Common.handleAjaxError); + return clusterNodesDataPromise; + } + + /** + * Event handler triggered when the user switches tabs. + */ + function onSelectTab (tab) { + // Resize table + var tabGrid = tab.grid; + if (nf.Common.isDefinedAndNotNull(tabGrid)) { + tabGrid.resizeCanvas(); + } + + // Clear filter text + clearFilter(); + + // Reset filter options + $('#cluster-filter-type').combo({ + options: tab.filterOptions, + select: function (option) { + applyFilter(); + } + }); + + updateFilterStats(tab); + } + return { /** * Initializes the cluster list. @@ -358,17 +954,51 @@ nf.ClusterTable = (function () { applyFilter(); }); - // filter type - $('#cluster-filter-type').combo({ - options: [{ - text: 'by address', - value: 'address' - }, { - text: 'by status', - value: 'status' - }], - select: function (option) { - 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); + } } }); @@ -377,190 +1007,41 @@ nf.ClusterTable = (function () { nf.ClusterTable.resetTableSize(); }); - // define a custom formatter for the more details column - var moreDetailsFormatter = function (row, cell, value, columnDef, dataContext) { - return '
'; - }; - - // 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 nf.Common.formatValue(value); - }; - - // define a custom formatter for the status column - var statusFormatter = function (row, cell, value, columnDef, dataContext) { - var markup = value; - if (dataContext.roles.includes(config.primaryNode)) { - value += ', PRIMARY'; - } - if (dataContext.roles.includes(config.clusterCoorindator)) { - value += ', COORDINATOR'; - } - return value; - }; - - var columnModel = [ - {id: 'moreDetails', name: ' ', sortable: false, resizable: false, formatter: moreDetailsFormatter, width: 50, maxWidth: 50}, - {id: 'node', field: 'node', name: 'Node Address', formatter: nodeFormatter, resizable: true, sortable: true}, - {id: 'activeThreadCount', field: 'activeThreadCount', name: 'Active Thread Count', resizable: true, sortable: true, defaultSortAsc: false}, - {id: 'queued', field: 'queued', name: 'Queue / Size', resizable: true, sortable: true, defaultSortAsc: false}, - {id: 'status', field: 'status', name: 'Status', formatter: statusFormatter, resizable: true, sortable: true}, - {id: 'uptime', field: 'nodeStartTime', name: 'Uptime', 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 (nf.Common.canModifyController()) { - // function for formatting the actions column - var actionFormatter = function (row, cell, value, columnDef, dataContext) { - var canDisconnect = false; - var canConnect = false; - - // determine the current status - if (dataContext.status === 'CONNECTED' || dataContext.status === 'CONNECTING') { - canDisconnect = true; - } else if (dataContext.status === 'DISCONNECTED') { - canConnect = true; - } - - // return the appropriate markup - if (canConnect) { - return '
 
'; - } else if (canDisconnect) { - return '
'; - } else { - return '
 
'; - } - }; - - columnModel.push({id: 'actions', label: ' ', formatter: actionFormatter, resizable: false, sortable: false, width: 80, maxWidth: 80}); - } - - var clusterOptions = { - forceFitColumns: true, - enableTextSelectionOnCells: true, - enableCellNavigation: false, - enableColumnReorder: false, - autoEdit: false, - rowHeight: 24 - }; - - // initialize the dataview - var clusterData = new Slick.Data.DataView({ - inlineFilters: false - }); - clusterData.setItems([], 'nodeId'); - clusterData.setFilterArgs({ - searchString: getFilterText(), - property: $('#cluster-filter-type').combo('getSelectedOption').value - }); - clusterData.setFilter(filter); - - // initialize the sort - sort({ - columnId: 'node', - sortAsc: true - }, clusterData); - - // initialize the grid - var clusterGrid = new Slick.Grid('#cluster-table', clusterData, columnModel, clusterOptions); - clusterGrid.setSelectionModel(new Slick.RowSelectionModel()); - clusterGrid.setSortColumn('node', true); - clusterGrid.onSort.subscribe(function (e, args) { - sort({ - columnId: args.sortCol.id, - sortAsc: args.sortAsc - }, clusterData); - }); - - // configure a click listener - clusterGrid.onClick.subscribe(function (e, args) { - var target = $(e.target); - - // get the node at this row - var item = clusterData.getItem(args.row); - - // determine the desired action - if (clusterGrid.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-disconnect')) { - promptForDisconnect(item); - } - } else if (clusterGrid.getColumns()[args.cell].id === 'moreDetails') { - if (target.hasClass('show-node-details')) { - showNodeDetails(item); - } + // initialize tabs + clusterTabs.forEach(function (tab) { + try { + tab.init(tab); + } catch (ex) { + console.error('Failed to initialize tab', tab, ex); } }); - - // wire up the dataview to the grid - clusterData.onRowCountChanged.subscribe(function (e, args) { - clusterGrid.updateRowCount(); - clusterGrid.render(); - - // update the total number of displayed processors - $('#displayed-nodes').text(args.current); - }); - clusterData.onRowsChanged.subscribe(function (e, args) { - clusterGrid.invalidateRows(args.rows); - clusterGrid.render(); - }); - - // hold onto an instance of the grid - $('#cluster-table').data('gridInstance', clusterGrid); - - // initialize the number of displayed items - $('#displayed-nodes').text('0'); }, /** * Update the size of the grid based on its container's current size. */ resetTableSize: function () { - var clusterGrid = $('#cluster-table').data('gridInstance'); - if (nf.Common.isDefinedAndNotNull(clusterGrid)) { - clusterGrid.resizeCanvas(); - } + clusterTabs.forEach(function (tab) { + if (tab && tab.grid) { + tab.grid.resizeCanvas(); + } + }); }, /** * Load the processor cluster table. */ loadClusterTable: function () { - return $.ajax({ - type: 'GET', - url: config.urls.cluster, - dataType: 'json' - }).done(function (response) { - var cluster = response.cluster; - - // ensure there are groups specified - if (nf.Common.isDefinedAndNotNull(cluster.nodes)) { - var clusterGrid = $('#cluster-table').data('gridInstance'); - var clusterData = clusterGrid.getData(); - - // set the items - clusterData.setItems(cluster.nodes); - clusterData.reSort(); - clusterGrid.invalidate(); - - // update the stats last refreshed timestamp - $('#cluster-last-refreshed').text(cluster.generated); - - // update the total number of processors - $('#total-nodes').text(cluster.nodes.length); - } else { - $('#total-nodes').text('0'); - } - }).fail(nf.Common.handleAjaxError); + 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; } }; + }()); \ No newline at end of file diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/cluster/nf-cluster.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/cluster/nf-cluster.js index 8d4da554bb..2e988cbcf8 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/cluster/nf-cluster.js +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/cluster/nf-cluster.js @@ -128,17 +128,17 @@ nf.Cluster = (function () { $('#cluster-table').css('bottom', 127); $('#cluster-refresh-container').css('margin', 40); } - - // configure the initial grid height - nf.ClusterTable.resetTableSize(); }; // set the initial size setBodySize(); - // create the counters table + // create the cluster table nf.ClusterTable.init(); + // resize to fit + nf.ClusterTable.resetTableSize(); + // load the table nf.ClusterTable.loadClusterTable().done(function () { // once the table is initialized, finish initializing the page diff --git a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js index 96a3d681b0..898a5d85fb 100644 --- a/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js +++ b/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/nf-common.js @@ -337,6 +337,19 @@ nf.Common = (function () { } }, + /** + * Determines whether the current user can access system diagnostics. + * + * @returns {boolean} + */ + canAccessSystem: function () { + if (nf.Common.isDefinedAndNotNull(nf.Common.currentUser)) { + return nf.Common.currentUser.systemPermissions.canRead === true; + } else { + return false; + } + }, + /** * Adds a mouse over effect for the specified selector using * the specified styles.