mcgilman commented on code in PR #8273: URL: https://github.com/apache/nifi/pull/8273#discussion_r1563327796
########## nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/flow-analysis-drawer.jsp: ########## @@ -0,0 +1,84 @@ +<%-- + 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. +--%> +<%@ page contentType="text/html" pageEncoding="UTF-8" session="false" %> +<section id="flow-analysis-drawer"> + <div class="flow-analysis-header"> + <div id="flow-analysis-loading-container" class="flow-analysis-loading-container"></div> + <div id="flow-analysis-loading-message" class="flow-analysis-loading-message">Rules analysis pending...</div> + </div> + <div class="flow-analysis-flow-guide-container"> + <div class="flow-analysis-flow-guide"> + <div class="flow-analysis-flow-guide-title">Flow Guide</div> + <div> + <div class="flow-analysis-violations-options"> + <div class="nf-checkbox checkbox-unchecked" id="show-only-violations"></div> + <span class="nf-checkbox-label show-only-violations-label">Show enforced violations</span> + </div> + <div class="flow-analysis-warnings-options"> + <div class="nf-checkbox checkbox-unchecked" id="show-only-warnings"></div> + <span class="nf-checkbox-label show-only-warnings-label">Show warning violations</span> + </div> + </div> + </div> + <div class="flow-analysis-flow-guide-breadcrumb">NiFi Flow</div> + </div> + <div id="flow-analysis-rules-accordion" class="flow-analysis-rules-accordion"> + + <div id="required-rules" class="required-rules"> + <div> + <div>Enforced Rules <span id="required-rule-count" class="required-rule-count"></span></div> + </div> + <ul id="required-rules-list" class="required-rules-list"> + </ul> + </div> + + <div id="recommended-rules" class="recommended-rules"> + <div> + <div>Warning Rules <span id="recommended-rule-count" class="recommended-rule-count"></span></div> + </div> + <ul id="recommended-rules-list" class="recommended-rules-list"></ul> + </div> + + <div id="rule-violations" class="rule-violations"> + <div class="rules-violations-header"> + <div>Enforced Violations <span id="rule-violation-count" class="rule-violation-count"></span></div> + </div> + <ul id="rule-violations-list" class="rule-violations-list"></ul> + </div> + + <div id="rule-warnings" class="rule-warnings"> + <div class="rules-warnings-header"> + <div>Warning Violations <span id="rule-warning-count" class="rule-warning-count"></span></div> + </div> + <ul id="rule-warnings-list" class="rule-warnings-list"></ul> + </div> + + <div class="rule-menu" id="rule-menu"> + <ul> + <li class="rule-menu-option" id="rule-menu-view-documentation"><i class="fa fa-info-circle rule-menu-option-icon" aria-hidden="true"></i>View Documentation</li> + <li class="rule-menu-option" id="rule-menu-edit-rule"><i class="fa fa-pencil rule-menu-option-icon" aria-hidden="true"></i>Edit Rule</li> + </ul> + </div> + + <div class="violation-menu" id="violation-menu"> + <ul> + <li class="violation-menu-option" id="violation-menu-more-info"><i class="fa fa-info-circle violation-menu-option-icon" aria-hidden="true"></i>Violation details</li> + <li class="violation-menu-option" id="violation-menu-go-to"><i class="fa fa-pencil violation-menu-option-icon" aria-hidden="true"></i>Go to component</li> Review Comment: Minor but I noticed inconsistent enabled states on the icons in the context menu. <img width="352" alt="Screenshot 2024-04-12 at 6 57 04 PM" src="https://github.com/apache/nifi/assets/123395/460a95c2-c84b-46e0-9c1e-c9854efec757"> <img width="349" alt="Screenshot 2024-04-12 at 6 57 15 PM" src="https://github.com/apache/nifi/assets/123395/2ca6f9bd-ef6c-4426-ae4a-0aed7b2a02d9"> ########## nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-flow-status-controller.js: ########## @@ -400,6 +406,562 @@ } } + /** + * The flow analysis controller. + */ + + this.flowAnalysis = { + + /** + * Create the list of rule violations + */ + buildRuleViolationsList: function(rules, violationsAndRecs) { + var ruleViolationCountEl = $('#rule-violation-count'); + var ruleViolationListEl = $('#rule-violations-list'); + var ruleWarningCountEl = $('#rule-warning-count'); + var ruleWarningListEl = $('#rule-warnings-list'); + var violations = violationsAndRecs.filter(function (violation) { + return violation.enforcementPolicy === 'ENFORCE' + }); + var warnings = violationsAndRecs.filter(function (violation) { + return violation.enforcementPolicy === 'WARN' + }); + ruleViolationCountEl.empty().text('(' + violations.length + ')'); + ruleWarningCountEl.empty().text('(' + warnings.length + ')'); + ruleViolationListEl.empty(); + ruleWarningListEl.empty(); + violations.forEach(function(violation) { + var rule = rules.find(function(rule) { + return rule.id === violation.ruleId; + }); + // create DOM elements + var violationListItemEl = $('<li></li>'); + var violationEl = $('<div class="violation-list-item"></div>'); + var violationListItemWrapperEl = $('<div class="violation-list-item-wrapper"></div>'); + var violationRuleEl = $('<div class="rule-violations-list-item-name"></div>'); + var violationListItemNameEl = $('<div class="violation-list-item-name"></div>'); + var violationListItemIdEl = $('<span class="violation-list-item-id"></span>'); + var violationInfoButtonEl = $('<button class="violation-menu-btn"><i class="fa fa-ellipsis-v rules-list-item-menu-target" aria-hidden="true"></i></button>'); + + // add text content and button data + $(violationRuleEl).text(rule.name); + violation.subjectPermissionDto.canRead ? $(violationListItemNameEl).text(violation.subjectDisplayName) : $(violationListItemNameEl).text('Unauthorized').addClass('unauthorized'); + $(violationListItemIdEl).text(violation.subjectId); + $(violationListItemEl).append(violationRuleEl).append(violationListItemWrapperEl); + $(violationInfoButtonEl).data('violationInfo', violation); + + // build list DOM structure + violationListItemWrapperEl.append(violationListItemNameEl).append(violationListItemIdEl); + violationEl.append(violationListItemWrapperEl).append(violationInfoButtonEl); + violationListItemEl.append(violationRuleEl).append(violationEl) + ruleViolationListEl.append(violationListItemEl); + }); + + warnings.forEach(function(warning) { + var rule = rules.find(function(rule) { + return rule.id === warning.ruleId; + }); + // create DOM elements + var warningListItemEl = $('<li></li>'); + var warningEl = $('<div class="warning-list-item"></div>'); + var warningListItemWrapperEl = $('<div class="warning-list-item-wrapper"></div>'); + var warningRuleEl = $('<div class="rule-warnings-list-item-name"></div>'); + var warningListItemNameEl = $('<div class="warning-list-item-name"></div>'); + var warningListItemIdEl = $('<span class="warning-list-item-id"></span>'); + var warningInfoButtonEl = $('<button class="violation-menu-btn"><i class="fa fa-ellipsis-v rules-list-item-menu-target" aria-hidden="true"></i></button>'); + + // add text content and button data + $(warningRuleEl).text(rule.name); + warning.subjectPermissionDto.canRead ? $(warningListItemNameEl).text(warning.subjectDisplayName) : $(warningListItemNameEl).text('Unauthorized').addClass('unauthorized'); + $(warningListItemIdEl).text(warning.subjectId); + $(warningListItemEl).append(warningRuleEl).append(warningListItemWrapperEl); + $(warningInfoButtonEl).data('violationInfo', warning); + + // build list DOM structure + warningListItemWrapperEl.append(warningListItemNameEl).append(warningListItemIdEl); + warningEl.append(warningListItemWrapperEl).append(warningInfoButtonEl); + warningListItemEl.append(warningRuleEl).append(warningEl) + ruleWarningListEl.append(warningListItemEl); + }); + }, + + /** + * + * Render a new list when it differs from the previous violations response + */ + buildRuleViolations: function(rules, violations) { + if (Object.keys(previousRulesResponse).length !== 0) { + var previousRulesResponseSorted = _.sortBy(previousRulesResponse.ruleViolations, 'subjectId'); + var violationsSorted = _.sortBy(violations, 'subjectId'); + if (!_.isEqual(previousRulesResponseSorted, violationsSorted)) { + this.buildRuleViolationsList(rules, violations); + } + } else { + this.buildRuleViolationsList(rules, violations); + } + }, + + /** + * Create the list of flow policy rules + */ + buildRuleList: function(ruleType, violationsMap, rec) { + var requiredRulesListEl = $('#required-rules-list'); + var recommendedRulesListEl = $('#recommended-rules-list'); + var rule = $('<li class="rules-list-item"></li>').append($(rec.requirement).append(rec.requirementInfoButton)) + var violationsListEl = ''; + var violationCountEl = ''; + + var violations = violationsMap.get(rec.id); + if (!!violations) { + if (violations.length === 1) { + violationCountEl = '<div class="rule-' + ruleType + 's-count">' + violations.length + ' ' + ruleType + '</div>'; + } else { + violationCountEl = '<div class="rule-' + ruleType + 's-count">' + violations.length + ' ' + ruleType + 's</div>'; + } + violationsListEl = $('<ul class="rule-' + ruleType + 's-list"></ul>'); + violations.forEach(function(violation) { + // create DOM elements + var violationListItemEl = $('<li class="' + ruleType + '-list-item"></li>'); + var violationWrapperEl = $('<div class="' + ruleType + '-list-item-wrapper"></div>'); + var violationNameEl = $('<div class="' + ruleType + '-list-item-name"></div>'); + var violationIdEl = $('<span class="' + ruleType + '-list-item-id"></span>'); + var violationInfoButtonEl = $('<button class="violation-menu-btn"><i class="fa fa-ellipsis-v rules-list-item-menu-target" aria-hidden="true"></i></button>'); + + // add text content and button data + violation.subjectPermissionDto.canRead ? violationNameEl.text(violation.subjectDisplayName) : violationNameEl.text('Unauthorized'); + violationIdEl.text(violation.subjectId); + + // build list DOM structure + violationListItemEl.append(violationWrapperEl); + violationWrapperEl.append(violationNameEl).append(violationIdEl) + violationInfoButtonEl.data('violationInfo', violation); + (violationsListEl).append(violationListItemEl.append(violationInfoButtonEl)); + }); + rule.append(violationCountEl).append(violationsListEl); + } + ruleType === 'violation' ? requiredRulesListEl.append(rule) : recommendedRulesListEl.append(rule); + }, + + /** + * Loads the current status of the flow. + */ + loadFlowPolicies: function () { + var flowAnalysisCtrl = this; + var requiredRulesListEl = $('#required-rules-list'); + var recommendedRulesListEl = $('#recommended-rules-list'); + var requiredRuleCountEl = $('#required-rule-count'); + var recommendedRuleCountEl = $('#recommended-rule-count'); + var flowAnalysisLoader = $('#flow-analysis-loading-container'); + var flowAnalysisLoadMessage = $('#flow-analysis-loading-message'); + + var groupId = nfCanvasUtils.getGroupId(); + if (groupId !== 'root') { + $.ajax({ + type: 'GET', + url: '../nifi-api/flow/flow-analysis/results/' + groupId, + dataType: 'json', + context: this + }).done(function (response) { + var recommendations = []; + var requirements = []; + var requirementsTotal = 0; + var recommendationsTotal = 0; + + if (!_.isEqual(previousRulesResponse, response)) { + // clear previous accordion content + requiredRulesListEl.empty(); + recommendedRulesListEl.empty(); + flowAnalysisCtrl.buildRuleViolations(response.rules, response.ruleViolations); + + if (response.flowAnalysisPending) { + flowAnalysisLoader.addClass('ajax-loading'); + flowAnalysisLoadMessage.show(); + } else { + flowAnalysisLoader.removeClass('ajax-loading'); + flowAnalysisLoadMessage.hide(); + } + + // For each ruleViolations: + // * group violations by ruleId + // * build DOM elements + // * get the ruleId and find the matching rule id + // * append violation list to matching rule list item + var violationsMap = new Map(); + response.ruleViolations.forEach(function(violation) { + if (violationsMap.has(violation.ruleId)){ + violationsMap.get(violation.ruleId).push(violation); + } else { + violationsMap.set(violation.ruleId, [violation]); + } + }); + + // build list of recommendations + response.rules.forEach(function(rule) { + if (rule.enforcementPolicy === 'WARN') { + var requirement = '<div class="rules-list-rule-info"></div>'; + var requirementName = $('<div></div>').text(rule.name); + var requirementInfoButton = '<button class="rule-menu-btn"><i class="fa fa-ellipsis-v rules-list-item-menu-target" aria-hidden="true"></i></button>'; + recommendations.push( + { + 'requirement': $(requirement).append(requirementName), + 'requirementInfoButton': $(requirementInfoButton).data('ruleInfo', rule), + 'id': rule.id + } + ) + recommendationsTotal++; + } + }); + + // add class to notification icon for recommended rules + var hasRecommendations = response.ruleViolations.findIndex(function(violation) { + return violation.enforcementPolicy === 'WARN'; + }); + if (hasRecommendations !== -1) { + $('#flow-analysis .flow-analysis-notification-icon ').addClass('recommendations'); + } else { + $('#flow-analysis .flow-analysis-notification-icon ').removeClass('recommendations'); + } + + // build list of requirements + recommendedRuleCountEl.empty().append('(' + recommendationsTotal + ')'); + recommendations.forEach(function(rec) { + flowAnalysisCtrl.buildRuleList('recommendation', violationsMap, rec); + }); + + response.rules.forEach(function(rule) { + if (rule.enforcementPolicy === 'ENFORCE') { + var requirement = '<div class="rules-list-rule-info"></div>'; + var requirementName = $('<div></div>').text(rule.name); + var requirementInfoButton = '<button class="rule-menu-btn"><i class="fa fa-ellipsis-v rules-list-item-menu-target" aria-hidden="true"></i></button>'; + requirements.push( + { + 'requirement': $(requirement).append(requirementName), + 'requirementInfoButton': $(requirementInfoButton).data('ruleInfo', rule), + 'id': rule.id + } + ) + requirementsTotal++; + } + }); + + // add class to notification icon for required rules + var hasViolations = response.ruleViolations.findIndex(function(violation) { + return violation.enforcementPolicy === 'ENFORCE'; + }) + if (hasViolations !== -1) { + $('#flow-analysis .flow-analysis-notification-icon ').addClass('violations'); + } else { + $('#flow-analysis .flow-analysis-notification-icon ').removeClass('violations'); + } + + requiredRuleCountEl.empty().append('(' + requirementsTotal + ')'); + + // build violations + requirements.forEach(function(rec) { + flowAnalysisCtrl.buildRuleList('violation', violationsMap, rec); + }); + + $('#required-rules').accordion('refresh'); + $('#recommended-rules').accordion('refresh'); + // report the updated status + previousRulesResponse = response; + + // setup rule menu handling + flowAnalysisCtrl.setRuleMenuHandling(); + + // setup violation menu handling + flowAnalysisCtrl.setViolationMenuHandling(response.rules); + } + }).fail(nfErrorHandler.handleAjaxError); + } + }, + + /** + * Set event bindings for rule menus + */ + setRuleMenuHandling: function() { + $('.rule-menu-btn').click(function(event) { + // stop event from immediately bubbling up to document and triggering closeRuleWindow + event.stopPropagation(); + // unbind previously bound rule data that may still exist + unbindRuleMenuHandling(); + + var ruleInfo = $(this).data('ruleInfo'); + $('#violation-menu').hide(); + $('#rule-menu').show(); + $('#rule-menu').position({ + my: "left top", + at: "left top", + of: event + }); + + // rule menu bindings + if (nfCommon.canAccessController()) { + $('#rule-menu-edit-rule').removeClass('disabled'); + $('#rule-menu-edit-rule').on('click', openRuleDetailsDialog); + } else { + $('#rule-menu-edit-rule').addClass('disabled'); + } + $('#rule-menu-view-documentation').on('click', viewRuleDocumentation); + $(document).on('click', closeRuleWindow); + + function viewRuleDocumentation(e) { + nfShell.showPage('../nifi-docs/documentation?' + $.param({ + select: ruleInfo.type, + group: ruleInfo.bundle.group, + artifact: ruleInfo.bundle.artifact, + version: ruleInfo.bundle.version + })).done(function () {}); + $("#rule-menu").hide(); + unbindRuleMenuHandling(); + } + + function closeRuleWindow(e) { + if ($(e.target).parents("#rule-menu").length === 0) { + $("#rule-menu").hide(); + unbindRuleMenuHandling(); + } + } + + function openRuleDetailsDialog() { + $('#rule-menu').hide(); + nfSettings.showSettings().done(function() { + nfSettings.selectFlowAnalysisRule(ruleInfo.id); + }); + unbindRuleMenuHandling(); + } + + function unbindRuleMenuHandling() { + $('#rule-menu-edit-rule').off("click"); + $('#rule-menu-view-documentation').off("click"); + $(document).unbind('click', closeRuleWindow); + } + + }); + }, + + /** + * Set event bindings for violation menus + */ + setViolationMenuHandling: function(rules) { + $('.violation-menu-btn').click(function(event) { + // stop event from immediately bubbling up to document and triggering closeViolationWindow + event.stopPropagation(); + var violationInfo = $(this).data('violationInfo'); + $('#rule-menu').hide(); + $('#violation-menu').show(); + $('#violation-menu').position({ + my: "left top", + at: "left top", + of: event + }); + + // violation menu bindings + if (violationInfo.subjectPermissionDto.canRead) { + $('#violation-menu-more-info').removeClass('disabled'); + $('#violation-menu-more-info .violation-menu-option-icon').removeClass('disabled'); + $('#violation-menu-more-info').on( "click", openRuleViolationMoreInfoDialog); + } else { + $('#violation-menu-more-info').addClass('disabled'); + $('#violation-menu-more-info .violation-menu-option-icon').addClass('disabled'); + } + + var isProcessor = violationInfo.subjectComponentType === 'PROCESSOR'; + if (violationInfo.groupId && isProcessor) { + $('#violation-menu-go-to').removeClass('disabled'); + $('#violation-menu-go-to .violation-menu-option-icon').removeClass('disabled'); + $('#violation-menu-go-to').on('click', goToComponent); + } else { + $('#violation-menu-go-to').addClass('disabled'); + $('#violation-menu-go-to .violation-menu-option-icon').addClass('disabled'); Review Comment: I know we often go back and forth with hidden vs disabled but in this case but in this case, we're dealing with something that isn't a Processor and will not support Go To. Should we consider hiding this menu item? There is a class `hidden` that is `display: none;` that should work. I think disabling makes sense for scenarios when the user lacks permissions (like Edit Rule) but am interested in your thoughts on this one. ########## nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-flow-status-controller.js: ########## @@ -400,6 +406,562 @@ } } + /** + * The flow analysis controller. + */ + + this.flowAnalysis = { + + /** + * Create the list of rule violations + */ + buildRuleViolationsList: function(rules, violationsAndRecs) { + var ruleViolationCountEl = $('#rule-violation-count'); + var ruleViolationListEl = $('#rule-violations-list'); + var ruleWarningCountEl = $('#rule-warning-count'); + var ruleWarningListEl = $('#rule-warnings-list'); + var violations = violationsAndRecs.filter(function (violation) { + return violation.enforcementPolicy === 'ENFORCE' + }); + var warnings = violationsAndRecs.filter(function (violation) { + return violation.enforcementPolicy === 'WARN' + }); + ruleViolationCountEl.empty().text('(' + violations.length + ')'); + ruleWarningCountEl.empty().text('(' + warnings.length + ')'); + ruleViolationListEl.empty(); + ruleWarningListEl.empty(); + violations.forEach(function(violation) { + var rule = rules.find(function(rule) { + return rule.id === violation.ruleId; + }); + // create DOM elements + var violationListItemEl = $('<li></li>'); + var violationEl = $('<div class="violation-list-item"></div>'); + var violationListItemWrapperEl = $('<div class="violation-list-item-wrapper"></div>'); + var violationRuleEl = $('<div class="rule-violations-list-item-name"></div>'); + var violationListItemNameEl = $('<div class="violation-list-item-name"></div>'); + var violationListItemIdEl = $('<span class="violation-list-item-id"></span>'); + var violationInfoButtonEl = $('<button class="violation-menu-btn"><i class="fa fa-ellipsis-v rules-list-item-menu-target" aria-hidden="true"></i></button>'); + + // add text content and button data + $(violationRuleEl).text(rule.name); + violation.subjectPermissionDto.canRead ? $(violationListItemNameEl).text(violation.subjectDisplayName) : $(violationListItemNameEl).text('Unauthorized').addClass('unauthorized'); + $(violationListItemIdEl).text(violation.subjectId); + $(violationListItemEl).append(violationRuleEl).append(violationListItemWrapperEl); + $(violationInfoButtonEl).data('violationInfo', violation); + + // build list DOM structure + violationListItemWrapperEl.append(violationListItemNameEl).append(violationListItemIdEl); + violationEl.append(violationListItemWrapperEl).append(violationInfoButtonEl); + violationListItemEl.append(violationRuleEl).append(violationEl) + ruleViolationListEl.append(violationListItemEl); + }); + + warnings.forEach(function(warning) { + var rule = rules.find(function(rule) { + return rule.id === warning.ruleId; + }); + // create DOM elements + var warningListItemEl = $('<li></li>'); + var warningEl = $('<div class="warning-list-item"></div>'); + var warningListItemWrapperEl = $('<div class="warning-list-item-wrapper"></div>'); + var warningRuleEl = $('<div class="rule-warnings-list-item-name"></div>'); + var warningListItemNameEl = $('<div class="warning-list-item-name"></div>'); + var warningListItemIdEl = $('<span class="warning-list-item-id"></span>'); + var warningInfoButtonEl = $('<button class="violation-menu-btn"><i class="fa fa-ellipsis-v rules-list-item-menu-target" aria-hidden="true"></i></button>'); + + // add text content and button data + $(warningRuleEl).text(rule.name); + warning.subjectPermissionDto.canRead ? $(warningListItemNameEl).text(warning.subjectDisplayName) : $(warningListItemNameEl).text('Unauthorized').addClass('unauthorized'); + $(warningListItemIdEl).text(warning.subjectId); + $(warningListItemEl).append(warningRuleEl).append(warningListItemWrapperEl); + $(warningInfoButtonEl).data('violationInfo', warning); + + // build list DOM structure + warningListItemWrapperEl.append(warningListItemNameEl).append(warningListItemIdEl); + warningEl.append(warningListItemWrapperEl).append(warningInfoButtonEl); + warningListItemEl.append(warningRuleEl).append(warningEl) + ruleWarningListEl.append(warningListItemEl); + }); + }, + + /** + * + * Render a new list when it differs from the previous violations response + */ + buildRuleViolations: function(rules, violations) { + if (Object.keys(previousRulesResponse).length !== 0) { + var previousRulesResponseSorted = _.sortBy(previousRulesResponse.ruleViolations, 'subjectId'); + var violationsSorted = _.sortBy(violations, 'subjectId'); + if (!_.isEqual(previousRulesResponseSorted, violationsSorted)) { + this.buildRuleViolationsList(rules, violations); + } + } else { + this.buildRuleViolationsList(rules, violations); + } + }, + + /** + * Create the list of flow policy rules + */ + buildRuleList: function(ruleType, violationsMap, rec) { + var requiredRulesListEl = $('#required-rules-list'); + var recommendedRulesListEl = $('#recommended-rules-list'); + var rule = $('<li class="rules-list-item"></li>').append($(rec.requirement).append(rec.requirementInfoButton)) + var violationsListEl = ''; + var violationCountEl = ''; + + var violations = violationsMap.get(rec.id); + if (!!violations) { + if (violations.length === 1) { + violationCountEl = '<div class="rule-' + ruleType + 's-count">' + violations.length + ' ' + ruleType + '</div>'; + } else { + violationCountEl = '<div class="rule-' + ruleType + 's-count">' + violations.length + ' ' + ruleType + 's</div>'; + } + violationsListEl = $('<ul class="rule-' + ruleType + 's-list"></ul>'); + violations.forEach(function(violation) { + // create DOM elements + var violationListItemEl = $('<li class="' + ruleType + '-list-item"></li>'); + var violationWrapperEl = $('<div class="' + ruleType + '-list-item-wrapper"></div>'); + var violationNameEl = $('<div class="' + ruleType + '-list-item-name"></div>'); + var violationIdEl = $('<span class="' + ruleType + '-list-item-id"></span>'); + var violationInfoButtonEl = $('<button class="violation-menu-btn"><i class="fa fa-ellipsis-v rules-list-item-menu-target" aria-hidden="true"></i></button>'); + + // add text content and button data + violation.subjectPermissionDto.canRead ? violationNameEl.text(violation.subjectDisplayName) : violationNameEl.text('Unauthorized'); + violationIdEl.text(violation.subjectId); + + // build list DOM structure + violationListItemEl.append(violationWrapperEl); + violationWrapperEl.append(violationNameEl).append(violationIdEl) + violationInfoButtonEl.data('violationInfo', violation); + (violationsListEl).append(violationListItemEl.append(violationInfoButtonEl)); + }); + rule.append(violationCountEl).append(violationsListEl); + } + ruleType === 'violation' ? requiredRulesListEl.append(rule) : recommendedRulesListEl.append(rule); + }, + + /** + * Loads the current status of the flow. + */ + loadFlowPolicies: function () { + var flowAnalysisCtrl = this; + var requiredRulesListEl = $('#required-rules-list'); + var recommendedRulesListEl = $('#recommended-rules-list'); + var requiredRuleCountEl = $('#required-rule-count'); + var recommendedRuleCountEl = $('#recommended-rule-count'); + var flowAnalysisLoader = $('#flow-analysis-loading-container'); + var flowAnalysisLoadMessage = $('#flow-analysis-loading-message'); + + var groupId = nfCanvasUtils.getGroupId(); + if (groupId !== 'root') { + $.ajax({ + type: 'GET', + url: '../nifi-api/flow/flow-analysis/results/' + groupId, + dataType: 'json', + context: this + }).done(function (response) { + var recommendations = []; + var requirements = []; + var requirementsTotal = 0; + var recommendationsTotal = 0; + + if (!_.isEqual(previousRulesResponse, response)) { + // clear previous accordion content + requiredRulesListEl.empty(); + recommendedRulesListEl.empty(); + flowAnalysisCtrl.buildRuleViolations(response.rules, response.ruleViolations); + + if (response.flowAnalysisPending) { + flowAnalysisLoader.addClass('ajax-loading'); + flowAnalysisLoadMessage.show(); + } else { + flowAnalysisLoader.removeClass('ajax-loading'); + flowAnalysisLoadMessage.hide(); + } + + // For each ruleViolations: + // * group violations by ruleId + // * build DOM elements + // * get the ruleId and find the matching rule id + // * append violation list to matching rule list item + var violationsMap = new Map(); + response.ruleViolations.forEach(function(violation) { + if (violationsMap.has(violation.ruleId)){ + violationsMap.get(violation.ruleId).push(violation); + } else { + violationsMap.set(violation.ruleId, [violation]); + } + }); + + // build list of recommendations + response.rules.forEach(function(rule) { + if (rule.enforcementPolicy === 'WARN') { + var requirement = '<div class="rules-list-rule-info"></div>'; + var requirementName = $('<div></div>').text(rule.name); + var requirementInfoButton = '<button class="rule-menu-btn"><i class="fa fa-ellipsis-v rules-list-item-menu-target" aria-hidden="true"></i></button>'; + recommendations.push( + { + 'requirement': $(requirement).append(requirementName), + 'requirementInfoButton': $(requirementInfoButton).data('ruleInfo', rule), + 'id': rule.id + } + ) + recommendationsTotal++; + } + }); + + // add class to notification icon for recommended rules + var hasRecommendations = response.ruleViolations.findIndex(function(violation) { + return violation.enforcementPolicy === 'WARN'; + }); + if (hasRecommendations !== -1) { + $('#flow-analysis .flow-analysis-notification-icon ').addClass('recommendations'); + } else { + $('#flow-analysis .flow-analysis-notification-icon ').removeClass('recommendations'); + } + + // build list of requirements + recommendedRuleCountEl.empty().append('(' + recommendationsTotal + ')'); + recommendations.forEach(function(rec) { + flowAnalysisCtrl.buildRuleList('recommendation', violationsMap, rec); + }); + + response.rules.forEach(function(rule) { + if (rule.enforcementPolicy === 'ENFORCE') { + var requirement = '<div class="rules-list-rule-info"></div>'; + var requirementName = $('<div></div>').text(rule.name); + var requirementInfoButton = '<button class="rule-menu-btn"><i class="fa fa-ellipsis-v rules-list-item-menu-target" aria-hidden="true"></i></button>'; + requirements.push( + { + 'requirement': $(requirement).append(requirementName), + 'requirementInfoButton': $(requirementInfoButton).data('ruleInfo', rule), + 'id': rule.id + } + ) + requirementsTotal++; + } + }); + + // add class to notification icon for required rules + var hasViolations = response.ruleViolations.findIndex(function(violation) { + return violation.enforcementPolicy === 'ENFORCE'; + }) + if (hasViolations !== -1) { + $('#flow-analysis .flow-analysis-notification-icon ').addClass('violations'); + } else { + $('#flow-analysis .flow-analysis-notification-icon ').removeClass('violations'); + } + + requiredRuleCountEl.empty().append('(' + requirementsTotal + ')'); + + // build violations + requirements.forEach(function(rec) { + flowAnalysisCtrl.buildRuleList('violation', violationsMap, rec); + }); + + $('#required-rules').accordion('refresh'); + $('#recommended-rules').accordion('refresh'); + // report the updated status + previousRulesResponse = response; + + // setup rule menu handling + flowAnalysisCtrl.setRuleMenuHandling(); + + // setup violation menu handling + flowAnalysisCtrl.setViolationMenuHandling(response.rules); + } + }).fail(nfErrorHandler.handleAjaxError); + } + }, + + /** + * Set event bindings for rule menus + */ + setRuleMenuHandling: function() { + $('.rule-menu-btn').click(function(event) { + // stop event from immediately bubbling up to document and triggering closeRuleWindow + event.stopPropagation(); + // unbind previously bound rule data that may still exist + unbindRuleMenuHandling(); + + var ruleInfo = $(this).data('ruleInfo'); + $('#violation-menu').hide(); + $('#rule-menu').show(); + $('#rule-menu').position({ + my: "left top", + at: "left top", + of: event + }); + + // rule menu bindings + if (nfCommon.canAccessController()) { + $('#rule-menu-edit-rule').removeClass('disabled'); + $('#rule-menu-edit-rule').on('click', openRuleDetailsDialog); + } else { + $('#rule-menu-edit-rule').addClass('disabled'); + } + $('#rule-menu-view-documentation').on('click', viewRuleDocumentation); + $(document).on('click', closeRuleWindow); + + function viewRuleDocumentation(e) { + nfShell.showPage('../nifi-docs/documentation?' + $.param({ + select: ruleInfo.type, + group: ruleInfo.bundle.group, + artifact: ruleInfo.bundle.artifact, + version: ruleInfo.bundle.version + })).done(function () {}); + $("#rule-menu").hide(); + unbindRuleMenuHandling(); + } + + function closeRuleWindow(e) { + if ($(e.target).parents("#rule-menu").length === 0) { + $("#rule-menu").hide(); + unbindRuleMenuHandling(); + } + } + + function openRuleDetailsDialog() { + $('#rule-menu').hide(); + nfSettings.showSettings().done(function() { + nfSettings.selectFlowAnalysisRule(ruleInfo.id); + }); + unbindRuleMenuHandling(); + } + + function unbindRuleMenuHandling() { + $('#rule-menu-edit-rule').off("click"); + $('#rule-menu-view-documentation').off("click"); + $(document).unbind('click', closeRuleWindow); + } + + }); + }, + + /** + * Set event bindings for violation menus + */ + setViolationMenuHandling: function(rules) { + $('.violation-menu-btn').click(function(event) { + // stop event from immediately bubbling up to document and triggering closeViolationWindow + event.stopPropagation(); + var violationInfo = $(this).data('violationInfo'); + $('#rule-menu').hide(); + $('#violation-menu').show(); + $('#violation-menu').position({ + my: "left top", + at: "left top", + of: event + }); + + // violation menu bindings + if (violationInfo.subjectPermissionDto.canRead) { + $('#violation-menu-more-info').removeClass('disabled'); + $('#violation-menu-more-info .violation-menu-option-icon').removeClass('disabled'); + $('#violation-menu-more-info').on( "click", openRuleViolationMoreInfoDialog); + } else { + $('#violation-menu-more-info').addClass('disabled'); + $('#violation-menu-more-info .violation-menu-option-icon').addClass('disabled'); + } + + var isProcessor = violationInfo.subjectComponentType === 'PROCESSOR'; + if (violationInfo.groupId && isProcessor) { Review Comment: Thanks for updating the `isRootGroup` logic here but I'm still unsure of this condition. A Processor will always have a parent group. Other extension types will not have a parent group or will conditionally have a parent group. If the intent is to only support Go To for Processors then I think we only need to check if it's a Processor. ########## nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/controllers/nf-ng-canvas-flow-status-controller.js: ########## @@ -400,6 +406,562 @@ } } + /** + * The flow analysis controller. + */ + + this.flowAnalysis = { + + /** + * Create the list of rule violations + */ + buildRuleViolationsList: function(rules, violationsAndRecs) { + var ruleViolationCountEl = $('#rule-violation-count'); + var ruleViolationListEl = $('#rule-violations-list'); + var ruleWarningCountEl = $('#rule-warning-count'); + var ruleWarningListEl = $('#rule-warnings-list'); + var violations = violationsAndRecs.filter(function (violation) { + return violation.enforcementPolicy === 'ENFORCE' + }); + var warnings = violationsAndRecs.filter(function (violation) { + return violation.enforcementPolicy === 'WARN' + }); + ruleViolationCountEl.empty().text('(' + violations.length + ')'); + ruleWarningCountEl.empty().text('(' + warnings.length + ')'); + ruleViolationListEl.empty(); + ruleWarningListEl.empty(); + violations.forEach(function(violation) { + var rule = rules.find(function(rule) { + return rule.id === violation.ruleId; + }); + // create DOM elements + var violationListItemEl = $('<li></li>'); + var violationEl = $('<div class="violation-list-item"></div>'); + var violationListItemWrapperEl = $('<div class="violation-list-item-wrapper"></div>'); + var violationRuleEl = $('<div class="rule-violations-list-item-name"></div>'); + var violationListItemNameEl = $('<div class="violation-list-item-name"></div>'); + var violationListItemIdEl = $('<span class="violation-list-item-id"></span>'); + var violationInfoButtonEl = $('<button class="violation-menu-btn"><i class="fa fa-ellipsis-v rules-list-item-menu-target" aria-hidden="true"></i></button>'); + + // add text content and button data + $(violationRuleEl).text(rule.name); + violation.subjectPermissionDto.canRead ? $(violationListItemNameEl).text(violation.subjectDisplayName) : $(violationListItemNameEl).text('Unauthorized').addClass('unauthorized'); + $(violationListItemIdEl).text(violation.subjectId); + $(violationListItemEl).append(violationRuleEl).append(violationListItemWrapperEl); + $(violationInfoButtonEl).data('violationInfo', violation); + + // build list DOM structure + violationListItemWrapperEl.append(violationListItemNameEl).append(violationListItemIdEl); + violationEl.append(violationListItemWrapperEl).append(violationInfoButtonEl); + violationListItemEl.append(violationRuleEl).append(violationEl) + ruleViolationListEl.append(violationListItemEl); + }); + + warnings.forEach(function(warning) { + var rule = rules.find(function(rule) { + return rule.id === warning.ruleId; + }); + // create DOM elements + var warningListItemEl = $('<li></li>'); + var warningEl = $('<div class="warning-list-item"></div>'); + var warningListItemWrapperEl = $('<div class="warning-list-item-wrapper"></div>'); + var warningRuleEl = $('<div class="rule-warnings-list-item-name"></div>'); + var warningListItemNameEl = $('<div class="warning-list-item-name"></div>'); + var warningListItemIdEl = $('<span class="warning-list-item-id"></span>'); + var warningInfoButtonEl = $('<button class="violation-menu-btn"><i class="fa fa-ellipsis-v rules-list-item-menu-target" aria-hidden="true"></i></button>'); + + // add text content and button data + $(warningRuleEl).text(rule.name); + warning.subjectPermissionDto.canRead ? $(warningListItemNameEl).text(warning.subjectDisplayName) : $(warningListItemNameEl).text('Unauthorized').addClass('unauthorized'); + $(warningListItemIdEl).text(warning.subjectId); + $(warningListItemEl).append(warningRuleEl).append(warningListItemWrapperEl); + $(warningInfoButtonEl).data('violationInfo', warning); + + // build list DOM structure + warningListItemWrapperEl.append(warningListItemNameEl).append(warningListItemIdEl); + warningEl.append(warningListItemWrapperEl).append(warningInfoButtonEl); + warningListItemEl.append(warningRuleEl).append(warningEl) + ruleWarningListEl.append(warningListItemEl); + }); + }, + + /** + * + * Render a new list when it differs from the previous violations response + */ + buildRuleViolations: function(rules, violations) { + if (Object.keys(previousRulesResponse).length !== 0) { + var previousRulesResponseSorted = _.sortBy(previousRulesResponse.ruleViolations, 'subjectId'); + var violationsSorted = _.sortBy(violations, 'subjectId'); + if (!_.isEqual(previousRulesResponseSorted, violationsSorted)) { + this.buildRuleViolationsList(rules, violations); + } + } else { + this.buildRuleViolationsList(rules, violations); + } + }, + + /** + * Create the list of flow policy rules + */ + buildRuleList: function(ruleType, violationsMap, rec) { + var requiredRulesListEl = $('#required-rules-list'); + var recommendedRulesListEl = $('#recommended-rules-list'); + var rule = $('<li class="rules-list-item"></li>').append($(rec.requirement).append(rec.requirementInfoButton)) + var violationsListEl = ''; + var violationCountEl = ''; + + var violations = violationsMap.get(rec.id); + if (!!violations) { + if (violations.length === 1) { + violationCountEl = '<div class="rule-' + ruleType + 's-count">' + violations.length + ' ' + ruleType + '</div>'; + } else { + violationCountEl = '<div class="rule-' + ruleType + 's-count">' + violations.length + ' ' + ruleType + 's</div>'; + } + violationsListEl = $('<ul class="rule-' + ruleType + 's-list"></ul>'); + violations.forEach(function(violation) { + // create DOM elements + var violationListItemEl = $('<li class="' + ruleType + '-list-item"></li>'); + var violationWrapperEl = $('<div class="' + ruleType + '-list-item-wrapper"></div>'); + var violationNameEl = $('<div class="' + ruleType + '-list-item-name"></div>'); + var violationIdEl = $('<span class="' + ruleType + '-list-item-id"></span>'); + var violationInfoButtonEl = $('<button class="violation-menu-btn"><i class="fa fa-ellipsis-v rules-list-item-menu-target" aria-hidden="true"></i></button>'); + + // add text content and button data + violation.subjectPermissionDto.canRead ? violationNameEl.text(violation.subjectDisplayName) : violationNameEl.text('Unauthorized'); + violationIdEl.text(violation.subjectId); + + // build list DOM structure + violationListItemEl.append(violationWrapperEl); + violationWrapperEl.append(violationNameEl).append(violationIdEl) + violationInfoButtonEl.data('violationInfo', violation); + (violationsListEl).append(violationListItemEl.append(violationInfoButtonEl)); + }); + rule.append(violationCountEl).append(violationsListEl); + } + ruleType === 'violation' ? requiredRulesListEl.append(rule) : recommendedRulesListEl.append(rule); + }, + + /** + * Loads the current status of the flow. + */ + loadFlowPolicies: function () { + var flowAnalysisCtrl = this; + var requiredRulesListEl = $('#required-rules-list'); + var recommendedRulesListEl = $('#recommended-rules-list'); + var requiredRuleCountEl = $('#required-rule-count'); + var recommendedRuleCountEl = $('#recommended-rule-count'); + var flowAnalysisLoader = $('#flow-analysis-loading-container'); + var flowAnalysisLoadMessage = $('#flow-analysis-loading-message'); + + var groupId = nfCanvasUtils.getGroupId(); + if (groupId !== 'root') { + $.ajax({ + type: 'GET', + url: '../nifi-api/flow/flow-analysis/results/' + groupId, + dataType: 'json', + context: this + }).done(function (response) { + var recommendations = []; + var requirements = []; + var requirementsTotal = 0; + var recommendationsTotal = 0; + + if (!_.isEqual(previousRulesResponse, response)) { + // clear previous accordion content + requiredRulesListEl.empty(); + recommendedRulesListEl.empty(); + flowAnalysisCtrl.buildRuleViolations(response.rules, response.ruleViolations); + + if (response.flowAnalysisPending) { + flowAnalysisLoader.addClass('ajax-loading'); + flowAnalysisLoadMessage.show(); + } else { + flowAnalysisLoader.removeClass('ajax-loading'); + flowAnalysisLoadMessage.hide(); + } + + // For each ruleViolations: + // * group violations by ruleId + // * build DOM elements + // * get the ruleId and find the matching rule id + // * append violation list to matching rule list item + var violationsMap = new Map(); + response.ruleViolations.forEach(function(violation) { + if (violationsMap.has(violation.ruleId)){ + violationsMap.get(violation.ruleId).push(violation); + } else { + violationsMap.set(violation.ruleId, [violation]); + } + }); + + // build list of recommendations + response.rules.forEach(function(rule) { + if (rule.enforcementPolicy === 'WARN') { + var requirement = '<div class="rules-list-rule-info"></div>'; + var requirementName = $('<div></div>').text(rule.name); + var requirementInfoButton = '<button class="rule-menu-btn"><i class="fa fa-ellipsis-v rules-list-item-menu-target" aria-hidden="true"></i></button>'; + recommendations.push( + { + 'requirement': $(requirement).append(requirementName), + 'requirementInfoButton': $(requirementInfoButton).data('ruleInfo', rule), + 'id': rule.id + } + ) + recommendationsTotal++; + } + }); + + // add class to notification icon for recommended rules + var hasRecommendations = response.ruleViolations.findIndex(function(violation) { + return violation.enforcementPolicy === 'WARN'; + }); + if (hasRecommendations !== -1) { + $('#flow-analysis .flow-analysis-notification-icon ').addClass('recommendations'); + } else { + $('#flow-analysis .flow-analysis-notification-icon ').removeClass('recommendations'); + } + + // build list of requirements + recommendedRuleCountEl.empty().append('(' + recommendationsTotal + ')'); + recommendations.forEach(function(rec) { + flowAnalysisCtrl.buildRuleList('recommendation', violationsMap, rec); + }); + + response.rules.forEach(function(rule) { + if (rule.enforcementPolicy === 'ENFORCE') { + var requirement = '<div class="rules-list-rule-info"></div>'; + var requirementName = $('<div></div>').text(rule.name); + var requirementInfoButton = '<button class="rule-menu-btn"><i class="fa fa-ellipsis-v rules-list-item-menu-target" aria-hidden="true"></i></button>'; + requirements.push( + { + 'requirement': $(requirement).append(requirementName), + 'requirementInfoButton': $(requirementInfoButton).data('ruleInfo', rule), + 'id': rule.id + } + ) + requirementsTotal++; + } + }); + + // add class to notification icon for required rules + var hasViolations = response.ruleViolations.findIndex(function(violation) { + return violation.enforcementPolicy === 'ENFORCE'; + }) + if (hasViolations !== -1) { + $('#flow-analysis .flow-analysis-notification-icon ').addClass('violations'); + } else { + $('#flow-analysis .flow-analysis-notification-icon ').removeClass('violations'); + } + + requiredRuleCountEl.empty().append('(' + requirementsTotal + ')'); + + // build violations + requirements.forEach(function(rec) { + flowAnalysisCtrl.buildRuleList('violation', violationsMap, rec); + }); + + $('#required-rules').accordion('refresh'); + $('#recommended-rules').accordion('refresh'); + // report the updated status + previousRulesResponse = response; + + // setup rule menu handling + flowAnalysisCtrl.setRuleMenuHandling(); + + // setup violation menu handling + flowAnalysisCtrl.setViolationMenuHandling(response.rules); + } + }).fail(nfErrorHandler.handleAjaxError); + } + }, + + /** + * Set event bindings for rule menus + */ + setRuleMenuHandling: function() { + $('.rule-menu-btn').click(function(event) { + // stop event from immediately bubbling up to document and triggering closeRuleWindow + event.stopPropagation(); + // unbind previously bound rule data that may still exist + unbindRuleMenuHandling(); + + var ruleInfo = $(this).data('ruleInfo'); + $('#violation-menu').hide(); + $('#rule-menu').show(); + $('#rule-menu').position({ + my: "left top", + at: "left top", + of: event + }); + + // rule menu bindings + if (nfCommon.canAccessController()) { + $('#rule-menu-edit-rule').removeClass('disabled'); + $('#rule-menu-edit-rule').on('click', openRuleDetailsDialog); + } else { + $('#rule-menu-edit-rule').addClass('disabled'); + } + $('#rule-menu-view-documentation').on('click', viewRuleDocumentation); + $(document).on('click', closeRuleWindow); + + function viewRuleDocumentation(e) { + nfShell.showPage('../nifi-docs/documentation?' + $.param({ + select: ruleInfo.type, + group: ruleInfo.bundle.group, + artifact: ruleInfo.bundle.artifact, + version: ruleInfo.bundle.version + })).done(function () {}); + $("#rule-menu").hide(); + unbindRuleMenuHandling(); + } + + function closeRuleWindow(e) { + if ($(e.target).parents("#rule-menu").length === 0) { + $("#rule-menu").hide(); + unbindRuleMenuHandling(); + } + } + + function openRuleDetailsDialog() { + $('#rule-menu').hide(); + nfSettings.showSettings().done(function() { + nfSettings.selectFlowAnalysisRule(ruleInfo.id); + }); + unbindRuleMenuHandling(); + } + + function unbindRuleMenuHandling() { + $('#rule-menu-edit-rule').off("click"); + $('#rule-menu-view-documentation').off("click"); + $(document).unbind('click', closeRuleWindow); + } + + }); + }, + + /** + * Set event bindings for violation menus + */ + setViolationMenuHandling: function(rules) { + $('.violation-menu-btn').click(function(event) { + // stop event from immediately bubbling up to document and triggering closeViolationWindow + event.stopPropagation(); + var violationInfo = $(this).data('violationInfo'); + $('#rule-menu').hide(); + $('#violation-menu').show(); + $('#violation-menu').position({ + my: "left top", + at: "left top", + of: event + }); + + // violation menu bindings + if (violationInfo.subjectPermissionDto.canRead) { + $('#violation-menu-more-info').removeClass('disabled'); + $('#violation-menu-more-info .violation-menu-option-icon').removeClass('disabled'); + $('#violation-menu-more-info').on( "click", openRuleViolationMoreInfoDialog); + } else { + $('#violation-menu-more-info').addClass('disabled'); + $('#violation-menu-more-info .violation-menu-option-icon').addClass('disabled'); + } Review Comment: I'm not sure we need to prevent navigation to a component when the user lacks permissions. If you compare this to other Referencing Component scenarios we allow the user to navigate to them. We just hide the component configuration and details. Also, in scenario like this we've disabled Violation Details in one menu but not the other even though they are from the same Rule. <img width="355" alt="Screenshot 2024-04-12 at 7 25 08 PM" src="https://github.com/apache/nifi/assets/123395/3a4e4d33-52db-4608-894c-68e46c585bb2"> -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
