You are viewing a plain text version of this content. The canonical link for it is here.
Posted to issues@nifi.apache.org by "sardell (via GitHub)" <gi...@apache.org> on 2024/01/19 17:01:05 UTC

[PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

sardell opened a new pull request, #8273:
URL: https://github.com/apache/nifi/pull/8273

   <!-- 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. -->
   
   # Summary
   
   [NIFI-11520](https://issues.apache.org/jira/browse/NIFI-11520)
   
   This PR introduces a menu to the canvas that displays the results of a Flow Analysis report. This will enable UI users to see a list of their established rules and violations (scoped to the currently selected Process Group in the canvas). I'm not sure how this will translate over to the new UI work that's currently in-progress, but I'll gladly take responsibility for adding it there as well.
   
   # Tracking
   
   Please complete the following tracking steps prior to pull request creation.
   
   ### Issue Tracking
   
   - [ ] [Apache NiFi Jira](https://issues.apache.org/jira/browse/NIFI) issue created
   
   ### Pull Request Tracking
   
   - [ ] Pull Request title starts with Apache NiFi Jira issue number, such as `NIFI-00000`
   - [ ] Pull Request commit message starts with Apache NiFi Jira issue number, as such `NIFI-00000`
   
   ### Pull Request Formatting
   
   - [ ] Pull Request based on current revision of the `main` branch
   - [ ] Pull Request refers to a feature branch with one commit containing changes
   
   # Verification
   
   Please indicate the verification steps performed prior to pull request creation.
   
   ### Build
   
   - [ ] Build completed using `mvn clean install -P contrib-check`
     - [ ] JDK 21
   
   ### Licensing
   
   - [ ] New dependencies are compatible with the [Apache License 2.0](https://apache.org/licenses/LICENSE-2.0) according to the [License Policy](https://www.apache.org/legal/resolved.html)
   - [ ] New dependencies are documented in applicable `LICENSE` and `NOTICE` files
   
   ### Documentation
   
   - [ ] Documentation formatting appears as expected in rendered files
   


-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "sardell (via GitHub)" <gi...@apache.org>.
sardell commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1463913231


##########
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,554 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);
+                        $(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);
+                        $(warningListItemNameEl).text(warning.subjectDisplayName);
+                        $(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
+                            violationNameEl.text(violation.subjectDisplayName);
+                            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 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);
+
+                                // 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(response);
+
+                                // setup violation menu handling
+                                flowAnalysisCtrl.setViolationMenuHandling(response, groupId);
+                            }
+                        }).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();
+                        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
+                        $('#rule-menu-more-info').on( "click", openRuleMoreInfoDialog);
+                        $('#rule-menu-edit-rule').on('click', openRuleDetailsDialog);
+                        $(document).on('click', closeRuleWindow);
+
+                        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 openRuleMoreInfoDialog() {
+                            $('#rule-menu').hide();
+                            $('#rule-type-pill').empty()
+                                                .removeClass()
+                                                .addClass(ruleInfo.enforcementPolicy.toLowerCase() + ' rule-type-pill')
+                                                .append(ruleInfo.enforcementPolicy);
+                            $('#rule-display-name').empty().append(ruleInfo.name);
+                            $('#rule-description').empty().append(ruleInfo.descriptors['component-type'].description);

Review Comment:
   Interesting. If descriptions for rules are optional, I'm wondering how valuable that dialog is. The only other information displayed in the dialog is the rule name, enforcement type and documentation link. The former two are already available in the side menu list. Maybe I should remove the dialog and just add another link to the menu for the documentation.



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "sardell (via GitHub)" <gi...@apache.org>.
sardell commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1508127197


##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/flow-status.jsp:
##########
@@ -71,6 +71,7 @@
             <button id="search-button" ng-click="appCtrl.serviceProvider.headerCtrl.flowStatusCtrl.search.toggleSearchField();"><i class="fa fa-search"></i></button>
             <input id="search-field" type="text" placeholder="Search"/>
         </div>
+        <button id="flow-analysis" class="flow-analysis"><i class="fa fa-lightbulb-o flow-analysis-notification-icon"></i></button>

Review Comment:
   Good catch. I'll update the background color styling to match the hover/focus states of the search icon.



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "mcgilman (via GitHub)" <gi...@apache.org>.
mcgilman merged PR #8273:
URL: https://github.com/apache/nifi/pull/8273


-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "tpalfy (via GitHub)" <gi...@apache.org>.
tpalfy commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1463631993


##########
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,554 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);
+                        $(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);
+                        $(warningListItemNameEl).text(warning.subjectDisplayName);
+                        $(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
+                            violationNameEl.text(violation.subjectDisplayName);
+                            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 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);
+
+                                // 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(response);
+
+                                // setup violation menu handling
+                                flowAnalysisCtrl.setViolationMenuHandling(response, groupId);
+                            }
+                        }).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();
+                        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
+                        $('#rule-menu-more-info').on( "click", openRuleMoreInfoDialog);
+                        $('#rule-menu-edit-rule').on('click', openRuleDetailsDialog);
+                        $(document).on('click', closeRuleWindow);
+
+                        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 openRuleMoreInfoDialog() {
+                            $('#rule-menu').hide();
+                            $('#rule-type-pill').empty()
+                                                .removeClass()
+                                                .addClass(ruleInfo.enforcementPolicy.toLowerCase() + ' rule-type-pill')
+                                                .append(ruleInfo.enforcementPolicy);
+                            $('#rule-display-name').empty().append(ruleInfo.name);
+                            $('#rule-description').empty().append(ruleInfo.descriptors['component-type'].description);
+                            $( "#rule-menu-more-info-dialog" ).modal( "show" );
+                            $('.rule-docs-link').click(function () {
+                                // open the documentation for this flow analysis rule
+                                nfShell.showPage('../nifi-docs/documentation?' + $.param({
+                                    select: ruleInfo.type,
+                                    group: ruleInfo.bundle.group,
+                                    artifact: ruleInfo.bundle.artifact,
+                                    version: ruleInfo.bundle.version
+                                })).done(function () {});
+                            });
+                            unbindRuleMenuHandling();
+                        };
+
+                        function unbindRuleMenuHandling() {
+                            $('#rule-menu-more-info').unbind('click', openRuleMoreInfoDialog);
+                            $('#rule-menu-edit-rule').unbind('click', openRuleDetailsDialog);
+                            $(document).unbind('click', closeRuleWindow);
+                        }
+
+                    });
+                },
+
+                /**
+                 * Set event bindings for violation menus
+                 */
+                setViolationMenuHandling: function(response, groupId) {
+                    $('.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
+                        $('#violation-menu-more-info').on( "click", openRuleMoreInfoDialog);
+                        $('#violation-menu-go-to').on('click', goToComponent);

Review Comment:
   The "Go to component" seem to work only if the violating component is in the currently viewed Process Group.



##########
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,554 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);
+                        $(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);
+                        $(warningListItemNameEl).text(warning.subjectDisplayName);
+                        $(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
+                            violationNameEl.text(violation.subjectDisplayName);
+                            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 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);
+
+                                // 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(response);
+
+                                // setup violation menu handling
+                                flowAnalysisCtrl.setViolationMenuHandling(response, groupId);
+                            }
+                        }).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();
+                        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
+                        $('#rule-menu-more-info').on( "click", openRuleMoreInfoDialog);
+                        $('#rule-menu-edit-rule').on('click', openRuleDetailsDialog);
+                        $(document).on('click', closeRuleWindow);
+
+                        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 openRuleMoreInfoDialog() {
+                            $('#rule-menu').hide();
+                            $('#rule-type-pill').empty()
+                                                .removeClass()
+                                                .addClass(ruleInfo.enforcementPolicy.toLowerCase() + ' rule-type-pill')
+                                                .append(ruleInfo.enforcementPolicy);
+                            $('#rule-display-name').empty().append(ruleInfo.name);
+                            $('#rule-description').empty().append(ruleInfo.descriptors['component-type'].description);

Review Comment:
   For now I'd go with directing the user to the rule documentation.
   



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "sardell (via GitHub)" <gi...@apache.org>.
sardell commented on PR #8273:
URL: https://github.com/apache/nifi/pull/8273#issuecomment-1984539412

   @mcgilman I believe I addressed all your comments. Thanks for the quick assist, @tpalfy!


-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "mcgilman (via GitHub)" <gi...@apache.org>.
mcgilman commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1526743150


##########
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 +407,602 @@
                 }
             }
 
+            /**
+             * 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 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);
+
+                                // 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.currentUser.controllerPermissions.canRead) {
+                            $('#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');
+                        }
+                        
+                        // If the groupId and subjectId are not the same, we can select the component
+                        var isRootGroup = violationInfo.groupId === violationInfo.subjectId;
+                        var isProcessor = violationInfo.subjectComponentType === 'PROCESSOR';
+                        if (!isRootGroup && isProcessor) {

Review Comment:
   Can you provide an example or explain when the `groupId` is the same as the `subjectId`? I'm a little unsure how that would mean the root group. I thought that was usually conveyed with the `groupId` being null/unset like Controller Services.



##########
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 +407,602 @@
                 }
             }
 
+            /**
+             * 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 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);
+
+                                // 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.currentUser.controllerPermissions.canRead) {

Review Comment:
   ```suggestion
                           if (nfCommon.canAccessController()) {
   ```
   This should be equivalent and protect against the scenario when the `currentUser` hasn't loaded yet.



##########
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,556 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);
+                        $(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);
+                        $(warningListItemNameEl).text(warning.subjectDisplayName);
+                        $(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
+                            violationNameEl.text(violation.subjectDisplayName);
+                            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 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);
+
+                                // 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
+                        $('#rule-menu-edit-rule').on('click', openRuleDetailsDialog);
+                        $('#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
+                        $('#violation-menu-more-info').on( "click", openRuleViolationMoreInfoDialog);
+                        // If the groupId and subjectId are not the same, we can select the component
+                        console.log(violationInfo.groupId !== violationInfo.subjectId);
+                        console.log(violationInfo);
+                        if (violationInfo.groupId !== violationInfo.subjectId) {
+                            $('#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');
+                        }
+                        $(document).on('click', closeViolationWindow);
+
+                        function closeViolationWindow(e) {
+                            if ($(e.target).parents("#violation-menu").length === 0) {
+                                $("#violation-menu").hide();
+                                unbindViolationMenuHandling();
+                            }
+                        }
+
+                        function openRuleViolationMoreInfoDialog() {
+                            var rule = rules.find(function(rule){ 
+                                return rule.id === violationInfo.ruleId;
+                            });
+                            $('#violation-menu').hide();
+                            $('#violation-type-pill').empty()
+                                                    .removeClass()
+                                                    .addClass(violationInfo.enforcementPolicy.toLowerCase() + ' violation-type-pill')
+                                                    .append(violationInfo.enforcementPolicy);
+                            $('#violation-description').empty().append(violationInfo.violationMessage);
+                            $('#violation-menu-more-info-dialog').modal( "show" );
+                            $('.violation-docs-link').click(function () {
+                                // open the documentation for this flow analysis rule
+                                nfShell.showPage('../nifi-docs/documentation?' + $.param({
+                                    select: rule.type,
+                                    group: rule.bundle.group,
+                                    artifact: rule.bundle.artifact,
+                                    version: rule.bundle.version
+                                })).done(function () {});
+                            });
+                            unbindViolationMenuHandling();
+                        }
+
+                        function goToComponent() {
+                            $('#violation-menu').hide();
+                            nfCanvasUtils.showComponent(violationInfo.groupId, violationInfo.subjectId);
+                            unbindViolationMenuHandling();
+                        }

Review Comment:
   I just tried this out again and there appears to be a case where `violoationInfo.groupId` is undefined. It looks like the field is missing from the back-end response. Because this field is missing, the menu item is available but when the user clicks on it nothing happens. 



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "sardell (via GitHub)" <gi...@apache.org>.
sardell commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1476626561


##########
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,554 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);
+                        $(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);
+                        $(warningListItemNameEl).text(warning.subjectDisplayName);
+                        $(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
+                            violationNameEl.text(violation.subjectDisplayName);
+                            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 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);
+
+                                // 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(response);
+
+                                // setup violation menu handling
+                                flowAnalysisCtrl.setViolationMenuHandling(response, groupId);
+                            }
+                        }).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();
+                        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
+                        $('#rule-menu-more-info').on( "click", openRuleMoreInfoDialog);
+                        $('#rule-menu-edit-rule').on('click', openRuleDetailsDialog);
+                        $(document).on('click', closeRuleWindow);
+
+                        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 openRuleMoreInfoDialog() {
+                            $('#rule-menu').hide();
+                            $('#rule-type-pill').empty()
+                                                .removeClass()
+                                                .addClass(ruleInfo.enforcementPolicy.toLowerCase() + ' rule-type-pill')
+                                                .append(ruleInfo.enforcementPolicy);
+                            $('#rule-display-name').empty().append(ruleInfo.name);
+                            $('#rule-description').empty().append(ruleInfo.descriptors['component-type'].description);
+                            $( "#rule-menu-more-info-dialog" ).modal( "show" );
+                            $('.rule-docs-link').click(function () {
+                                // open the documentation for this flow analysis rule

Review Comment:
   Thanks for pointing this out, @tpalfy. Seems like the issue was an event handler that wasn't removed properly. I've since pushed a fix for the issue. 



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "sardell (via GitHub)" <gi...@apache.org>.
sardell commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1565769286


##########
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:
   Really great point. I think your assessment is correct. Since we are disabling functionality based on not supporting that functionality vs. a permissions scenario, for example, it's probably better to just hide that option.



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "mcgilman (via GitHub)" <gi...@apache.org>.
mcgilman commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1567741800


##########
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 see. The `violationMessage` is subject dependent. Thanks for clarifying.



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "sardell (via GitHub)" <gi...@apache.org>.
sardell commented on PR #8273:
URL: https://github.com/apache/nifi/pull/8273#issuecomment-1907023978

   > Hey @sardell 👋 Thanks for the PR! Would love to have you help contribute this into the new UI work. That work is still in progress and is being tracked here [1]. Please add a line item there and file a sub-task. Please see [2] for a high-level overview of layout, structure, and dataflow throughout the application. These details will be formalized in a README soon, but wanted to pass along a pointer in the meantime.
   > 
   > The UX in this new effort is largely unchanged from the previous UI. So any designs for this PR should be directly applicable to the new UI. The only real difference is moving away from the large full-screen modal (though that may still be used for component documentation or custom UIs yet to be developed) to separate pages.
   > 
   > [1] https://issues.apache.org/jira/browse/NIFI-12400 [2] #8053
   
   @mcgilman Thanks for that quick rundown. I'm very excited to see this UI framework update coming together so fast! I've added a line item and sub-task to NIFI-12400 and will begin work shortly.


-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "tpalfy (via GitHub)" <gi...@apache.org>.
tpalfy commented on PR #8273:
URL: https://github.com/apache/nifi/pull/8273#issuecomment-1971265091

   LGTM
   Tested with built-in and some custom rules. All issues raised by me seem to be addressed.


-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "tpalfy (via GitHub)" <gi...@apache.org>.
tpalfy commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1509031907


##########
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,556 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);

Review Comment:
   @mcgilman this is a good observation. I'm trying to figure out how to solve this. In cluster mode usually the Merger (and in this case the FlowAnalysisResultEntityMerger) actually filters the result based on authorization. But in non-cluster mode I don't see what the usual practice is.
   Do you have a suggestion?



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "mcgilman (via GitHub)" <gi...@apache.org>.
mcgilman commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1531038999


##########
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 +407,602 @@
                 }
             }
 
+            /**
+             * 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 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);
+
+                                // 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.currentUser.controllerPermissions.canRead) {
+                            $('#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');
+                        }
+                        
+                        // If the groupId and subjectId are not the same, we can select the component
+                        var isRootGroup = violationInfo.groupId === violationInfo.subjectId;
+                        var isProcessor = violationInfo.subjectComponentType === 'PROCESSOR';
+                        if (!isRootGroup && isProcessor) {

Review Comment:
   What type of components could be the subject? Since the `subjectId` is being compared to the `groupId` it suggested that a Process Group could be the subject. If a child Process Group were the subject wouldn't the same condition be `true` even though it isn't the root group?



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "sardell (via GitHub)" <gi...@apache.org>.
sardell commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1516140292


##########
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,85 @@
+<%--
+ 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 class="flow-analysis-refresh-container">
+            <button id="flow-analysis-check-now-btn" class="flow-analysis-check-now-btn">Start a new analysis</button>

Review Comment:
   I updated my work to disable this button and show a spinner while a report is being run.



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "mcgilman (via GitHub)" <gi...@apache.org>.
mcgilman commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1507846158


##########
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,554 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);
+                        $(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);
+                        $(warningListItemNameEl).text(warning.subjectDisplayName);
+                        $(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
+                            violationNameEl.text(violation.subjectDisplayName);
+                            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 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);
+
+                                // 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(response);
+
+                                // setup violation menu handling
+                                flowAnalysisCtrl.setViolationMenuHandling(response, groupId);
+                            }
+                        }).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();
+                        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
+                        $('#rule-menu-more-info').on( "click", openRuleMoreInfoDialog);
+                        $('#rule-menu-edit-rule').on('click', openRuleDetailsDialog);
+                        $(document).on('click', closeRuleWindow);
+
+                        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 openRuleMoreInfoDialog() {
+                            $('#rule-menu').hide();
+                            $('#rule-type-pill').empty()
+                                                .removeClass()
+                                                .addClass(ruleInfo.enforcementPolicy.toLowerCase() + ' rule-type-pill')
+                                                .append(ruleInfo.enforcementPolicy);
+                            $('#rule-display-name').empty().append(ruleInfo.name);
+                            $('#rule-description').empty().append(ruleInfo.descriptors['component-type'].description);
+                            $( "#rule-menu-more-info-dialog" ).modal( "show" );
+                            $('.rule-docs-link').click(function () {
+                                // open the documentation for this flow analysis rule
+                                nfShell.showPage('../nifi-docs/documentation?' + $.param({
+                                    select: ruleInfo.type,
+                                    group: ruleInfo.bundle.group,
+                                    artifact: ruleInfo.bundle.artifact,
+                                    version: ruleInfo.bundle.version
+                                })).done(function () {});
+                            });
+                            unbindRuleMenuHandling();
+                        };
+
+                        function unbindRuleMenuHandling() {
+                            $('#rule-menu-more-info').unbind('click', openRuleMoreInfoDialog);
+                            $('#rule-menu-edit-rule').unbind('click', openRuleDetailsDialog);
+                            $(document).unbind('click', closeRuleWindow);
+                        }
+
+                    });
+                },
+
+                /**
+                 * Set event bindings for violation menus
+                 */
+                setViolationMenuHandling: function(response, groupId) {
+                    $('.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
+                        $('#violation-menu-more-info').on( "click", openRuleMoreInfoDialog);
+                        $('#violation-menu-go-to').on('click', goToComponent);
+                        $(document).on('click', closeViolationWindow);
+
+                        function closeViolationWindow(e) {
+                            if ($(e.target).parents("#violation-menu").length === 0) {
+                                $("#violation-menu").hide();
+                                unbindViolationMenuHandling();
+                            }
+                        }
+
+                        function openRuleMoreInfoDialog() {
+                            var rule = response.rules.find(function(rule){ 
+                                return rule.id === violationInfo.ruleId;
+                            })
+                            $('#violation-menu').hide();
+                            $('#violation-type-pill').empty()
+                                                    .removeClass()
+                                                    .addClass(violationInfo.enforcementPolicy.toLowerCase() + ' violation-type-pill')
+                                                    .append(violationInfo.enforcementPolicy);
+                            $('#violation-display-name').empty().append(violationInfo.violationMessage);
+                            $('#violation-description').empty().append(rule.descriptors['component-type'].description);
+                            $('#violation-menu-more-info-dialog').modal( "show" );
+                            $('.violation-docs-link').click(function () {
+                                // open the documentation for this flow analysis rule
+                                nfShell.showPage('../nifi-docs/documentation?' + $.param({
+                                    select: rule.type,
+                                    group: rule.bundle.group,
+                                    artifact: rule.bundle.artifact,
+                                    version: rule.bundle.version
+                                })).done(function () {});
+                            });
+                            unbindViolationMenuHandling();
+                        }
+
+                        function goToComponent() {
+                            $('#violation-menu').hide();
+                            nfCanvasUtils.showComponent(groupId, violationInfo.subjectId);
+                            unbindViolationMenuHandling();
+                        }
+
+                        function unbindViolationMenuHandling() {
+                            $('#violation-menu-go-to').unbind('click', goToComponent);
+                            $('#violation-menu-more-info').unbind('click', openRuleMoreInfoDialog);
+                            $(document).unbind('click', closeViolationWindow);
+                        }
+                    });
+                },
+
+                /**
+                 * Initialize the flow analysis controller.
+                 */
+                init: function () {
+                    var flowAnalysisCtrl = this;
+                    var drawer = $('#flow-analysis-drawer');
+                    var requiredRulesEl = $('#required-rules');
+                    var recommendedRulesEl = $('#recommended-rules');
+                    var newFlowAnalsysisBtnEl = $('#flow-analysis-check-now-btn');
+                    var flowAnalysisRefreshIntervalSeconds = 5;

Review Comment:
   The change is fine. But I believe it could have been passed down through the `init` functions.



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "sardell (via GitHub)" <gi...@apache.org>.
sardell commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1530355434


##########
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 +407,602 @@
                 }
             }
 
+            /**
+             * 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 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);
+
+                                // 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.currentUser.controllerPermissions.canRead) {
+                            $('#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');
+                        }
+                        
+                        // If the groupId and subjectId are not the same, we can select the component
+                        var isRootGroup = violationInfo.groupId === violationInfo.subjectId;
+                        var isProcessor = violationInfo.subjectComponentType === 'PROCESSOR';
+                        if (!isRootGroup && isProcessor) {

Review Comment:
   I'm also used to null being returned for the root group in other areas of the application. This logic is guidance I received from @tpalfy based on offline discussions we had, but I can't remember the exact reasoning why he suggested this approach. I'll gladly change it to evaluating if null/unset like we're used to, but I'm wondering if @tpalfy remembers why we went this direction.



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "mcgilman (via GitHub)" <gi...@apache.org>.
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "sardell (via GitHub)" <gi...@apache.org>.
sardell commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1459356177


##########
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,554 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);
+                        $(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);
+                        $(warningListItemNameEl).text(warning.subjectDisplayName);
+                        $(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
+                            violationNameEl.text(violation.subjectDisplayName);
+                            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 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);
+
+                                // 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(response);
+
+                                // setup violation menu handling
+                                flowAnalysisCtrl.setViolationMenuHandling(response, groupId);
+                            }
+                        }).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();
+                        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
+                        $('#rule-menu-more-info').on( "click", openRuleMoreInfoDialog);
+                        $('#rule-menu-edit-rule').on('click', openRuleDetailsDialog);
+                        $(document).on('click', closeRuleWindow);
+
+                        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 openRuleMoreInfoDialog() {
+                            $('#rule-menu').hide();
+                            $('#rule-type-pill').empty()
+                                                .removeClass()
+                                                .addClass(ruleInfo.enforcementPolicy.toLowerCase() + ' rule-type-pill')
+                                                .append(ruleInfo.enforcementPolicy);
+                            $('#rule-display-name').empty().append(ruleInfo.name);
+                            $('#rule-description').empty().append(ruleInfo.descriptors['component-type'].description);
+                            $( "#rule-menu-more-info-dialog" ).modal( "show" );
+                            $('.rule-docs-link').click(function () {
+                                // open the documentation for this flow analysis rule
+                                nfShell.showPage('../nifi-docs/documentation?' + $.param({
+                                    select: ruleInfo.type,
+                                    group: ruleInfo.bundle.group,
+                                    artifact: ruleInfo.bundle.artifact,
+                                    version: ruleInfo.bundle.version
+                                })).done(function () {});
+                            });
+                            unbindRuleMenuHandling();
+                        };
+
+                        function unbindRuleMenuHandling() {
+                            $('#rule-menu-more-info').unbind('click', openRuleMoreInfoDialog);
+                            $('#rule-menu-edit-rule').unbind('click', openRuleDetailsDialog);
+                            $(document).unbind('click', closeRuleWindow);
+                        }
+
+                    });
+                },
+
+                /**
+                 * Set event bindings for violation menus
+                 */
+                setViolationMenuHandling: function(response, groupId) {
+                    $('.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
+                        $('#violation-menu-more-info').on( "click", openRuleMoreInfoDialog);
+                        $('#violation-menu-go-to').on('click', goToComponent);
+                        $(document).on('click', closeViolationWindow);
+
+                        function closeViolationWindow(e) {
+                            if ($(e.target).parents("#violation-menu").length === 0) {
+                                $("#violation-menu").hide();
+                                unbindViolationMenuHandling();
+                            }
+                        }
+
+                        function openRuleMoreInfoDialog() {
+                            var rule = response.rules.find(function(rule){ 
+                                return rule.id === violationInfo.ruleId;
+                            })
+                            $('#violation-menu').hide();
+                            $('#violation-type-pill').empty()
+                                                    .removeClass()
+                                                    .addClass(violationInfo.enforcementPolicy.toLowerCase() + ' violation-type-pill')
+                                                    .append(violationInfo.enforcementPolicy);
+                            $('#violation-display-name').empty().append(violationInfo.violationMessage);
+                            $('#violation-description').empty().append(rule.descriptors['component-type'].description);
+                            $('#violation-menu-more-info-dialog').modal( "show" );
+                            $('.violation-docs-link').click(function () {
+                                // open the documentation for this flow analysis rule
+                                nfShell.showPage('../nifi-docs/documentation?' + $.param({
+                                    select: rule.type,
+                                    group: rule.bundle.group,
+                                    artifact: rule.bundle.artifact,
+                                    version: rule.bundle.version
+                                })).done(function () {});
+                            });
+                            unbindViolationMenuHandling();
+                        }
+
+                        function goToComponent() {
+                            $('#violation-menu').hide();
+                            nfCanvasUtils.showComponent(groupId, violationInfo.subjectId);
+                            unbindViolationMenuHandling();
+                        }
+
+                        function unbindViolationMenuHandling() {
+                            $('#violation-menu-go-to').unbind('click', goToComponent);
+                            $('#violation-menu-more-info').unbind('click', openRuleMoreInfoDialog);
+                            $(document).unbind('click', closeViolationWindow);
+                        }
+                    });
+                },
+
+                /**
+                 * Initialize the flow analysis controller.
+                 */
+                init: function () {
+                    var flowAnalysisCtrl = this;
+                    var drawer = $('#flow-analysis-drawer');
+                    var requiredRulesEl = $('#required-rules');
+                    var recommendedRulesEl = $('#recommended-rules');
+                    var newFlowAnalsysisBtnEl = $('#flow-analysis-check-now-btn');
+                    var flowAnalysisRefreshIntervalSeconds = 5;

Review Comment:
   Is there a constant elsewhere used for polling that I should be using here?  `5` is an arbitrary value used for development. I'm guessing we don't want polling that often.



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "tpalfy (via GitHub)" <gi...@apache.org>.
tpalfy commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1466662453


##########
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,554 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);
+                        $(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);
+                        $(warningListItemNameEl).text(warning.subjectDisplayName);
+                        $(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
+                            violationNameEl.text(violation.subjectDisplayName);
+                            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 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);
+
+                                // 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(response);
+
+                                // setup violation menu handling
+                                flowAnalysisCtrl.setViolationMenuHandling(response, groupId);
+                            }
+                        }).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();
+                        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
+                        $('#rule-menu-more-info').on( "click", openRuleMoreInfoDialog);
+                        $('#rule-menu-edit-rule').on('click', openRuleDetailsDialog);
+                        $(document).on('click', closeRuleWindow);
+
+                        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 openRuleMoreInfoDialog() {
+                            $('#rule-menu').hide();
+                            $('#rule-type-pill').empty()
+                                                .removeClass()
+                                                .addClass(ruleInfo.enforcementPolicy.toLowerCase() + ' rule-type-pill')
+                                                .append(ruleInfo.enforcementPolicy);
+                            $('#rule-display-name').empty().append(ruleInfo.name);
+                            $('#rule-description').empty().append(ruleInfo.descriptors['component-type'].description);
+                            $( "#rule-menu-more-info-dialog" ).modal( "show" );
+                            $('.rule-docs-link').click(function () {
+                                // open the documentation for this flow analysis rule

Review Comment:
   This "View documentation" is has some error in it.
   2 symptoms (I assume both require multiple enable rules of different types):
   1. Many time the documentation of a wrong rule is shown.
   2. When debugging this function runs multiple times.



##########
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,554 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);
+                        $(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);
+                        $(warningListItemNameEl).text(warning.subjectDisplayName);
+                        $(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
+                            violationNameEl.text(violation.subjectDisplayName);
+                            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 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);
+
+                                // 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(response);
+
+                                // setup violation menu handling
+                                flowAnalysisCtrl.setViolationMenuHandling(response, groupId);
+                            }
+                        }).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();
+                        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
+                        $('#rule-menu-more-info').on( "click", openRuleMoreInfoDialog);
+                        $('#rule-menu-edit-rule').on('click', openRuleDetailsDialog);
+                        $(document).on('click', closeRuleWindow);
+
+                        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 openRuleMoreInfoDialog() {
+                            $('#rule-menu').hide();
+                            $('#rule-type-pill').empty()
+                                                .removeClass()
+                                                .addClass(ruleInfo.enforcementPolicy.toLowerCase() + ' rule-type-pill')
+                                                .append(ruleInfo.enforcementPolicy);
+                            $('#rule-display-name').empty().append(ruleInfo.name);
+                            $('#rule-description').empty().append(ruleInfo.descriptors['component-type'].description);
+                            $( "#rule-menu-more-info-dialog" ).modal( "show" );
+                            $('.rule-docs-link').click(function () {
+                                // open the documentation for this flow analysis rule
+                                nfShell.showPage('../nifi-docs/documentation?' + $.param({
+                                    select: ruleInfo.type,
+                                    group: ruleInfo.bundle.group,
+                                    artifact: ruleInfo.bundle.artifact,
+                                    version: ruleInfo.bundle.version
+                                })).done(function () {});
+                            });
+                            unbindRuleMenuHandling();
+                        };
+
+                        function unbindRuleMenuHandling() {
+                            $('#rule-menu-more-info').unbind('click', openRuleMoreInfoDialog);
+                            $('#rule-menu-edit-rule').unbind('click', openRuleDetailsDialog);
+                            $(document).unbind('click', closeRuleWindow);
+                        }
+
+                    });
+                },
+
+                /**
+                 * Set event bindings for violation menus
+                 */
+                setViolationMenuHandling: function(response, groupId) {
+                    $('.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
+                        $('#violation-menu-more-info').on( "click", openRuleViolationMoreInfoDialog);
+                        $('#violation-menu-go-to').on('click', goToComponent);
+                        $(document).on('click', closeViolationWindow);
+
+                        function closeViolationWindow(e) {
+                            if ($(e.target).parents("#violation-menu").length === 0) {
+                                $("#violation-menu").hide();
+                                unbindViolationMenuHandling();
+                            }
+                        }
+
+                        function openRuleViolationMoreInfoDialog() {
+                            var rule = response.rules.find(function(rule){ 
+                                return rule.id === violationInfo.ruleId;
+                            })
+                            $('#violation-menu').hide();
+                            $('#violation-type-pill').empty()
+                                                    .removeClass()
+                                                    .addClass(violationInfo.enforcementPolicy.toLowerCase() + ' violation-type-pill')
+                                                    .append(violationInfo.enforcementPolicy);
+                            $('#violation-display-name').empty().append(violationInfo.violationMessage);
+                            $('#violation-description').empty().append(rule.descriptors['component-type'].description);
+                            $('#violation-menu-more-info-dialog').modal( "show" );
+                            $('.violation-docs-link').click(function () {
+                                // open the documentation for this flow analysis rule

Review Comment:
   Similarly to Rule details, this "View documentation" is has some error in it.
   2 symptoms (I assume both require multiple enable rules of different types):
   1. Many time the documentation of a wrong rule is shown.
   2. When debugging this function runs multiple times.



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "tpalfy (via GitHub)" <gi...@apache.org>.
tpalfy commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1511380014


##########
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,556 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);

Review Comment:
   @mcgilman I have opened https://issues.apache.org/jira/browse/NIFI-12862 and going to open a PR for the backend to fix this issue.
   



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "tpalfy (via GitHub)" <gi...@apache.org>.
tpalfy commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1513231844


##########
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,556 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);

Review Comment:
   @mcgilman I opened a PR https://issues.apache.org/jira/browse/NIFI-12862 that should take care of this issue.



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "sardell (via GitHub)" <gi...@apache.org>.
sardell commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1567607846


##########
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:
   With the updated logic, Go to component is now shown/hidden based on whether or not it's a processor.
   
   > 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.
   
   Okay, yeah, I see why this is confusing. I'm basing this on the violation's `subjectPermissionDto.canRead`, which is why sometimes those details are disabled and sometimes not based on the component permissions. @tpalfy advised me to take this approach because rules can contain component information which would be displayed in the violation details modal that the button opens.



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "sardell (via GitHub)" <gi...@apache.org>.
sardell commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1474564178


##########
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,554 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);
+                        $(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);
+                        $(warningListItemNameEl).text(warning.subjectDisplayName);
+                        $(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
+                            violationNameEl.text(violation.subjectDisplayName);
+                            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 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);
+
+                                // 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(response);
+
+                                // setup violation menu handling
+                                flowAnalysisCtrl.setViolationMenuHandling(response, groupId);
+                            }
+                        }).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();
+                        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
+                        $('#rule-menu-more-info').on( "click", openRuleMoreInfoDialog);
+                        $('#rule-menu-edit-rule').on('click', openRuleDetailsDialog);
+                        $(document).on('click', closeRuleWindow);
+
+                        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 openRuleMoreInfoDialog() {
+                            $('#rule-menu').hide();
+                            $('#rule-type-pill').empty()
+                                                .removeClass()
+                                                .addClass(ruleInfo.enforcementPolicy.toLowerCase() + ' rule-type-pill')
+                                                .append(ruleInfo.enforcementPolicy);
+                            $('#rule-display-name').empty().append(ruleInfo.name);
+                            $('#rule-description').empty().append(ruleInfo.descriptors['component-type'].description);
+                            $( "#rule-menu-more-info-dialog" ).modal( "show" );
+                            $('.rule-docs-link').click(function () {
+                                // open the documentation for this flow analysis rule

Review Comment:
   Please disregard my last comment. I was finally able to reproduce the issue (happens when clicking from one rule options menu trigger then immediately clicking on another with no breakpoint set).



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "sardell (via GitHub)" <gi...@apache.org>.
sardell commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1476649837


##########
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,554 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);
+                        $(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);
+                        $(warningListItemNameEl).text(warning.subjectDisplayName);
+                        $(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
+                            violationNameEl.text(violation.subjectDisplayName);
+                            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 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);
+
+                                // 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(response);
+
+                                // setup violation menu handling
+                                flowAnalysisCtrl.setViolationMenuHandling(response, groupId);
+                            }
+                        }).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();
+                        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
+                        $('#rule-menu-more-info').on( "click", openRuleMoreInfoDialog);
+                        $('#rule-menu-edit-rule').on('click', openRuleDetailsDialog);
+                        $(document).on('click', closeRuleWindow);
+
+                        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 openRuleMoreInfoDialog() {
+                            $('#rule-menu').hide();
+                            $('#rule-type-pill').empty()
+                                                .removeClass()
+                                                .addClass(ruleInfo.enforcementPolicy.toLowerCase() + ' rule-type-pill')
+                                                .append(ruleInfo.enforcementPolicy);
+                            $('#rule-display-name').empty().append(ruleInfo.name);
+                            $('#rule-description').empty().append(ruleInfo.descriptors['component-type'].description);
+                            $( "#rule-menu-more-info-dialog" ).modal( "show" );
+                            $('.rule-docs-link').click(function () {
+                                // open the documentation for this flow analysis rule
+                                nfShell.showPage('../nifi-docs/documentation?' + $.param({
+                                    select: ruleInfo.type,
+                                    group: ruleInfo.bundle.group,
+                                    artifact: ruleInfo.bundle.artifact,
+                                    version: ruleInfo.bundle.version
+                                })).done(function () {});
+                            });
+                            unbindRuleMenuHandling();
+                        };
+
+                        function unbindRuleMenuHandling() {
+                            $('#rule-menu-more-info').unbind('click', openRuleMoreInfoDialog);
+                            $('#rule-menu-edit-rule').unbind('click', openRuleDetailsDialog);
+                            $(document).unbind('click', closeRuleWindow);
+                        }
+
+                    });
+                },
+
+                /**
+                 * Set event bindings for violation menus
+                 */
+                setViolationMenuHandling: function(response, groupId) {
+                    $('.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
+                        $('#violation-menu-more-info').on( "click", openRuleMoreInfoDialog);
+                        $('#violation-menu-go-to').on('click', goToComponent);
+                        $(document).on('click', closeViolationWindow);
+
+                        function closeViolationWindow(e) {
+                            if ($(e.target).parents("#violation-menu").length === 0) {
+                                $("#violation-menu").hide();
+                                unbindViolationMenuHandling();
+                            }
+                        }
+
+                        function openRuleMoreInfoDialog() {
+                            var rule = response.rules.find(function(rule){ 
+                                return rule.id === violationInfo.ruleId;
+                            })
+                            $('#violation-menu').hide();
+                            $('#violation-type-pill').empty()
+                                                    .removeClass()
+                                                    .addClass(violationInfo.enforcementPolicy.toLowerCase() + ' violation-type-pill')
+                                                    .append(violationInfo.enforcementPolicy);
+                            $('#violation-display-name').empty().append(violationInfo.violationMessage);
+                            $('#violation-description').empty().append(rule.descriptors['component-type'].description);
+                            $('#violation-menu-more-info-dialog').modal( "show" );
+                            $('.violation-docs-link').click(function () {
+                                // open the documentation for this flow analysis rule
+                                nfShell.showPage('../nifi-docs/documentation?' + $.param({
+                                    select: rule.type,
+                                    group: rule.bundle.group,
+                                    artifact: rule.bundle.artifact,
+                                    version: rule.bundle.version
+                                })).done(function () {});
+                            });
+                            unbindViolationMenuHandling();
+                        }
+
+                        function goToComponent() {
+                            $('#violation-menu').hide();
+                            nfCanvasUtils.showComponent(groupId, violationInfo.subjectId);
+                            unbindViolationMenuHandling();
+                        }
+
+                        function unbindViolationMenuHandling() {
+                            $('#violation-menu-go-to').unbind('click', goToComponent);
+                            $('#violation-menu-more-info').unbind('click', openRuleMoreInfoDialog);
+                            $(document).unbind('click', closeViolationWindow);
+                        }
+                    });
+                },
+
+                /**
+                 * Initialize the flow analysis controller.
+                 */
+                init: function () {
+                    var flowAnalysisCtrl = this;
+                    var drawer = $('#flow-analysis-drawer');
+                    var requiredRulesEl = $('#required-rules');
+                    var recommendedRulesEl = $('#recommended-rules');
+                    var newFlowAnalsysisBtnEl = $('#flow-analysis-check-now-btn');
+                    var flowAnalysisRefreshIntervalSeconds = 5;

Review Comment:
   @mcgilman Based on your comment above, I ended up creating a getter/setter in nf-common and calling the set function in nf-canvas-bootstrap. If the approach you had in mind is much different, let me know and I can update accordingly.



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "sardell (via GitHub)" <gi...@apache.org>.
sardell commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1508139735


##########
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,556 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);
+                        $(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);
+                        $(warningListItemNameEl).text(warning.subjectDisplayName);
+                        $(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
+                            violationNameEl.text(violation.subjectDisplayName);
+                            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 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);
+
+                                // 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
+                        $('#rule-menu-edit-rule').on('click', openRuleDetailsDialog);
+                        $('#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();
+                        }

Review Comment:
   Yeah, great point. I'll update the client side to show/hide that button based on a user's permission to the controller.



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "sardell (via GitHub)" <gi...@apache.org>.
sardell commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1516859726


##########
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,556 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);

Review Comment:
   I've updated this PR to show 'Unauthorized' for the fields that will return `null` if a user doesn't have read permission. @tpalfy's PR will need to be merged first as a result.



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "sardell (via GitHub)" <gi...@apache.org>.
sardell commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1476628525


##########
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,554 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);
+                        $(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);
+                        $(warningListItemNameEl).text(warning.subjectDisplayName);
+                        $(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
+                            violationNameEl.text(violation.subjectDisplayName);
+                            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 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);
+
+                                // 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(response);
+
+                                // setup violation menu handling
+                                flowAnalysisCtrl.setViolationMenuHandling(response, groupId);
+                            }
+                        }).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();
+                        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
+                        $('#rule-menu-more-info').on( "click", openRuleMoreInfoDialog);
+                        $('#rule-menu-edit-rule').on('click', openRuleDetailsDialog);
+                        $(document).on('click', closeRuleWindow);
+
+                        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 openRuleMoreInfoDialog() {
+                            $('#rule-menu').hide();
+                            $('#rule-type-pill').empty()
+                                                .removeClass()
+                                                .addClass(ruleInfo.enforcementPolicy.toLowerCase() + ' rule-type-pill')
+                                                .append(ruleInfo.enforcementPolicy);
+                            $('#rule-display-name').empty().append(ruleInfo.name);
+                            $('#rule-description').empty().append(ruleInfo.descriptors['component-type'].description);
+                            $( "#rule-menu-more-info-dialog" ).modal( "show" );
+                            $('.rule-docs-link').click(function () {
+                                // open the documentation for this flow analysis rule
+                                nfShell.showPage('../nifi-docs/documentation?' + $.param({
+                                    select: ruleInfo.type,
+                                    group: ruleInfo.bundle.group,
+                                    artifact: ruleInfo.bundle.artifact,
+                                    version: ruleInfo.bundle.version
+                                })).done(function () {});
+                            });
+                            unbindRuleMenuHandling();
+                        };
+
+                        function unbindRuleMenuHandling() {
+                            $('#rule-menu-more-info').unbind('click', openRuleMoreInfoDialog);
+                            $('#rule-menu-edit-rule').unbind('click', openRuleDetailsDialog);
+                            $(document).unbind('click', closeRuleWindow);
+                        }
+
+                    });
+                },
+
+                /**
+                 * Set event bindings for violation menus
+                 */
+                setViolationMenuHandling: function(response, groupId) {
+                    $('.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
+                        $('#violation-menu-more-info').on( "click", openRuleMoreInfoDialog);
+                        $('#violation-menu-go-to').on('click', goToComponent);

Review Comment:
   This was caused by an incorrect value being passed for the groupId. Should be fixed now.



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "tpalfy (via GitHub)" <gi...@apache.org>.
tpalfy commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1462197492


##########
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,554 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);
+                        $(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);
+                        $(warningListItemNameEl).text(warning.subjectDisplayName);
+                        $(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
+                            violationNameEl.text(violation.subjectDisplayName);
+                            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 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);
+
+                                // 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(response);
+
+                                // setup violation menu handling
+                                flowAnalysisCtrl.setViolationMenuHandling(response, groupId);
+                            }
+                        }).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();
+                        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
+                        $('#rule-menu-more-info').on( "click", openRuleMoreInfoDialog);
+                        $('#rule-menu-edit-rule').on('click', openRuleDetailsDialog);
+                        $(document).on('click', closeRuleWindow);
+
+                        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 openRuleMoreInfoDialog() {
+                            $('#rule-menu').hide();
+                            $('#rule-type-pill').empty()
+                                                .removeClass()
+                                                .addClass(ruleInfo.enforcementPolicy.toLowerCase() + ' rule-type-pill')
+                                                .append(ruleInfo.enforcementPolicy);
+                            $('#rule-display-name').empty().append(ruleInfo.name);
+                            $('#rule-description').empty().append(ruleInfo.descriptors['component-type'].description);
+                            $( "#rule-menu-more-info-dialog" ).modal( "show" );
+                            $('.rule-docs-link').click(function () {
+                                // open the documentation for this flow analysis rule
+                                nfShell.showPage('../nifi-docs/documentation?' + $.param({
+                                    select: ruleInfo.type,
+                                    group: ruleInfo.bundle.group,
+                                    artifact: ruleInfo.bundle.artifact,
+                                    version: ruleInfo.bundle.version
+                                })).done(function () {});
+                            });
+                            unbindRuleMenuHandling();
+                        };
+
+                        function unbindRuleMenuHandling() {
+                            $('#rule-menu-more-info').unbind('click', openRuleMoreInfoDialog);
+                            $('#rule-menu-edit-rule').unbind('click', openRuleDetailsDialog);
+                            $(document).unbind('click', closeRuleWindow);
+                        }
+
+                    });
+                },
+
+                /**
+                 * Set event bindings for violation menus
+                 */
+                setViolationMenuHandling: function(response, groupId) {
+                    $('.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
+                        $('#violation-menu-more-info').on( "click", openRuleMoreInfoDialog);
+                        $('#violation-menu-go-to').on('click', goToComponent);
+                        $(document).on('click', closeViolationWindow);
+
+                        function closeViolationWindow(e) {
+                            if ($(e.target).parents("#violation-menu").length === 0) {
+                                $("#violation-menu").hide();
+                                unbindViolationMenuHandling();
+                            }
+                        }
+
+                        function openRuleMoreInfoDialog() {

Review Comment:
   ```suggestion
                           function openRuleViolationMoreInfoDialog() {
   ```



##########
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,554 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);
+                        $(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);
+                        $(warningListItemNameEl).text(warning.subjectDisplayName);
+                        $(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
+                            violationNameEl.text(violation.subjectDisplayName);
+                            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 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);
+
+                                // 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(response);
+
+                                // setup violation menu handling
+                                flowAnalysisCtrl.setViolationMenuHandling(response, groupId);
+                            }
+                        }).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();
+                        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
+                        $('#rule-menu-more-info').on( "click", openRuleMoreInfoDialog);
+                        $('#rule-menu-edit-rule').on('click', openRuleDetailsDialog);
+                        $(document).on('click', closeRuleWindow);
+
+                        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 openRuleMoreInfoDialog() {
+                            $('#rule-menu').hide();
+                            $('#rule-type-pill').empty()
+                                                .removeClass()
+                                                .addClass(ruleInfo.enforcementPolicy.toLowerCase() + ' rule-type-pill')
+                                                .append(ruleInfo.enforcementPolicy);
+                            $('#rule-display-name').empty().append(ruleInfo.name);
+                            $('#rule-description').empty().append(ruleInfo.descriptors['component-type'].description);

Review Comment:
   This refers to the description of the PropertyDescriptor named 'component-type'.
   That is very specific to the DisallowComponentType rule.
   If a rule doesn't have that descriptor this  fails with an error.
   
   Important to not that some Rules don't have any PropertyDescriptors at all.
   
   In general adding the PropertyDescriptors to this dialog page is not desirable in my opinion.



##########
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,554 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);
+                        $(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);
+                        $(warningListItemNameEl).text(warning.subjectDisplayName);
+                        $(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
+                            violationNameEl.text(violation.subjectDisplayName);
+                            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 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);
+
+                                // 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(response);
+
+                                // setup violation menu handling
+                                flowAnalysisCtrl.setViolationMenuHandling(response, groupId);
+                            }
+                        }).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();
+                        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
+                        $('#rule-menu-more-info').on( "click", openRuleMoreInfoDialog);
+                        $('#rule-menu-edit-rule').on('click', openRuleDetailsDialog);
+                        $(document).on('click', closeRuleWindow);
+
+                        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 openRuleMoreInfoDialog() {
+                            $('#rule-menu').hide();
+                            $('#rule-type-pill').empty()
+                                                .removeClass()
+                                                .addClass(ruleInfo.enforcementPolicy.toLowerCase() + ' rule-type-pill')
+                                                .append(ruleInfo.enforcementPolicy);
+                            $('#rule-display-name').empty().append(ruleInfo.name);
+                            $('#rule-description').empty().append(ruleInfo.descriptors['component-type'].description);
+                            $( "#rule-menu-more-info-dialog" ).modal( "show" );
+                            $('.rule-docs-link').click(function () {
+                                // open the documentation for this flow analysis rule
+                                nfShell.showPage('../nifi-docs/documentation?' + $.param({
+                                    select: ruleInfo.type,
+                                    group: ruleInfo.bundle.group,
+                                    artifact: ruleInfo.bundle.artifact,
+                                    version: ruleInfo.bundle.version
+                                })).done(function () {});
+                            });
+                            unbindRuleMenuHandling();
+                        };
+
+                        function unbindRuleMenuHandling() {
+                            $('#rule-menu-more-info').unbind('click', openRuleMoreInfoDialog);
+                            $('#rule-menu-edit-rule').unbind('click', openRuleDetailsDialog);
+                            $(document).unbind('click', closeRuleWindow);
+                        }
+
+                    });
+                },
+
+                /**
+                 * Set event bindings for violation menus
+                 */
+                setViolationMenuHandling: function(response, groupId) {
+                    $('.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
+                        $('#violation-menu-more-info').on( "click", openRuleMoreInfoDialog);
+                        $('#violation-menu-go-to').on('click', goToComponent);
+                        $(document).on('click', closeViolationWindow);
+
+                        function closeViolationWindow(e) {
+                            if ($(e.target).parents("#violation-menu").length === 0) {
+                                $("#violation-menu").hide();
+                                unbindViolationMenuHandling();
+                            }
+                        }
+
+                        function openRuleMoreInfoDialog() {
+                            var rule = response.rules.find(function(rule){ 
+                                return rule.id === violationInfo.ruleId;
+                            })
+                            $('#violation-menu').hide();
+                            $('#violation-type-pill').empty()
+                                                    .removeClass()
+                                                    .addClass(violationInfo.enforcementPolicy.toLowerCase() + ' violation-type-pill')
+                                                    .append(violationInfo.enforcementPolicy);
+                            $('#violation-display-name').empty().append(violationInfo.violationMessage);
+                            $('#violation-description').empty().append(rule.descriptors['component-type'].description);

Review Comment:
   The same issue here as above:
   This refers to the description of the PropertyDescriptor named 'component-type'.
   That is very specific to the DisallowComponentType rule.
   If a rule doesn't have that descriptor this fails with an error.
   
   But in general I don't think adding the Rule's PropertyDescriptors'  info to the rule violation info dialog page is desirable.



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "sardell (via GitHub)" <gi...@apache.org>.
sardell commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1463913231


##########
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,554 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);
+                        $(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);
+                        $(warningListItemNameEl).text(warning.subjectDisplayName);
+                        $(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
+                            violationNameEl.text(violation.subjectDisplayName);
+                            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 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);
+
+                                // 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(response);
+
+                                // setup violation menu handling
+                                flowAnalysisCtrl.setViolationMenuHandling(response, groupId);
+                            }
+                        }).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();
+                        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
+                        $('#rule-menu-more-info').on( "click", openRuleMoreInfoDialog);
+                        $('#rule-menu-edit-rule').on('click', openRuleDetailsDialog);
+                        $(document).on('click', closeRuleWindow);
+
+                        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 openRuleMoreInfoDialog() {
+                            $('#rule-menu').hide();
+                            $('#rule-type-pill').empty()
+                                                .removeClass()
+                                                .addClass(ruleInfo.enforcementPolicy.toLowerCase() + ' rule-type-pill')
+                                                .append(ruleInfo.enforcementPolicy);
+                            $('#rule-display-name').empty().append(ruleInfo.name);
+                            $('#rule-description').empty().append(ruleInfo.descriptors['component-type'].description);

Review Comment:
   Interesting. ~If descriptions for rules are optional~ If these descriptions are not even for the rule and might not always exist, I'm wondering how valuable that dialog is. The only other information displayed in the dialog is the rule name, enforcement type and documentation link. The former two are already available in the side menu list. Maybe I should remove the dialog and just add another link to the menu for the documentation.



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "sardell (via GitHub)" <gi...@apache.org>.
sardell commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1463925689


##########
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,554 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);
+                        $(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);
+                        $(warningListItemNameEl).text(warning.subjectDisplayName);
+                        $(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
+                            violationNameEl.text(violation.subjectDisplayName);
+                            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 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);
+
+                                // 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(response);
+
+                                // setup violation menu handling
+                                flowAnalysisCtrl.setViolationMenuHandling(response, groupId);
+                            }
+                        }).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();
+                        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
+                        $('#rule-menu-more-info').on( "click", openRuleMoreInfoDialog);
+                        $('#rule-menu-edit-rule').on('click', openRuleDetailsDialog);
+                        $(document).on('click', closeRuleWindow);
+
+                        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 openRuleMoreInfoDialog() {
+                            $('#rule-menu').hide();
+                            $('#rule-type-pill').empty()
+                                                .removeClass()
+                                                .addClass(ruleInfo.enforcementPolicy.toLowerCase() + ' rule-type-pill')
+                                                .append(ruleInfo.enforcementPolicy);
+                            $('#rule-display-name').empty().append(ruleInfo.name);
+                            $('#rule-description').empty().append(ruleInfo.descriptors['component-type'].description);

Review Comment:
   Just to be clear, I think the dialog would be really valuable for a user if we did have descriptions for rules so they don't have to constantly look up documentation to find a short description for what a rule does, but if the property doesn't exist or isn't always present it would probably be better just to direct the user to the rule documentation.



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "sardell (via GitHub)" <gi...@apache.org>.
sardell commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1516856485


##########
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,556 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);
+                        $(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);
+                        $(warningListItemNameEl).text(warning.subjectDisplayName);
+                        $(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
+                            violationNameEl.text(violation.subjectDisplayName);
+                            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 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);
+
+                                // 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
+                        $('#rule-menu-edit-rule').on('click', openRuleDetailsDialog);
+                        $('#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
+                        $('#violation-menu-more-info').on( "click", openRuleViolationMoreInfoDialog);
+                        // If the groupId and subjectId are not the same, we can select the component
+                        console.log(violationInfo.groupId !== violationInfo.subjectId);
+                        console.log(violationInfo);
+                        if (violationInfo.groupId !== violationInfo.subjectId) {
+                            $('#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');
+                        }
+                        $(document).on('click', closeViolationWindow);
+
+                        function closeViolationWindow(e) {
+                            if ($(e.target).parents("#violation-menu").length === 0) {
+                                $("#violation-menu").hide();
+                                unbindViolationMenuHandling();
+                            }
+                        }
+
+                        function openRuleViolationMoreInfoDialog() {
+                            var rule = rules.find(function(rule){ 
+                                return rule.id === violationInfo.ruleId;
+                            });
+                            $('#violation-menu').hide();
+                            $('#violation-type-pill').empty()
+                                                    .removeClass()
+                                                    .addClass(violationInfo.enforcementPolicy.toLowerCase() + ' violation-type-pill')
+                                                    .append(violationInfo.enforcementPolicy);
+                            $('#violation-description').empty().append(violationInfo.violationMessage);
+                            $('#violation-menu-more-info-dialog').modal( "show" );
+                            $('.violation-docs-link').click(function () {
+                                // open the documentation for this flow analysis rule
+                                nfShell.showPage('../nifi-docs/documentation?' + $.param({
+                                    select: rule.type,
+                                    group: rule.bundle.group,
+                                    artifact: rule.bundle.artifact,
+                                    version: rule.bundle.version
+                                })).done(function () {});
+                            });
+                            unbindViolationMenuHandling();
+                        }
+
+                        function goToComponent() {
+                            $('#violation-menu').hide();
+                            nfCanvasUtils.showComponent(violationInfo.groupId, violationInfo.subjectId);
+                            unbindViolationMenuHandling();
+                        }

Review Comment:
   I've tested canvas components and those seemed to work fine for me. Controller Services definitely did not work. @tpalfy added this commit[1] in another PR as a way of identifying the component type. I added a client-side check so the Go To Component button is disabled if the violating component is a CS. As a result, [his PR](https://github.com/apache/nifi/pull/8475) will need to be merged first before this one. 
   
   [1] https://github.com/apache/nifi/pull/8475/commits/054c39cf4a1155c5932d84b830deee8ed95a40e1



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "sardell (via GitHub)" <gi...@apache.org>.
sardell commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1525292949


##########
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,556 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);

Review Comment:
   @mcgilman Quick update: @tpalfy's work has been reviewed and merged into `main`. I've rebased this PR to include those changes.



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "mcgilman (via GitHub)" <gi...@apache.org>.
mcgilman commented on PR #8273:
URL: https://github.com/apache/nifi/pull/8273#issuecomment-1906159835

   > I'm not sure how this will translate over to the new UI work that's currently in-progress, but I'll gladly take responsibility for adding it there as well.
   
   Hey @sardell 👋 Thanks for the PR! Would love to have you help contribute this into the new UI work. That work is still in progress and is being tracked here [1]. Please add a line item there and file a sub-task. Please see [2] for a high-level overview of layout, structure, and dataflow throughout the application. These details will be formalized in a README soon, but wanted to pass along a pointer in the meantime.
   
   [1] https://issues.apache.org/jira/browse/NIFI-12400
   [2] https://github.com/apache/nifi/pull/8053


-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "mcgilman (via GitHub)" <gi...@apache.org>.
mcgilman commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1468196770


##########
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,554 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);
+                        $(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);
+                        $(warningListItemNameEl).text(warning.subjectDisplayName);
+                        $(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
+                            violationNameEl.text(violation.subjectDisplayName);
+                            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 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);
+
+                                // 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(response);
+
+                                // setup violation menu handling
+                                flowAnalysisCtrl.setViolationMenuHandling(response, groupId);
+                            }
+                        }).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();
+                        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
+                        $('#rule-menu-more-info').on( "click", openRuleMoreInfoDialog);
+                        $('#rule-menu-edit-rule').on('click', openRuleDetailsDialog);
+                        $(document).on('click', closeRuleWindow);
+
+                        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 openRuleMoreInfoDialog() {
+                            $('#rule-menu').hide();
+                            $('#rule-type-pill').empty()
+                                                .removeClass()
+                                                .addClass(ruleInfo.enforcementPolicy.toLowerCase() + ' rule-type-pill')
+                                                .append(ruleInfo.enforcementPolicy);
+                            $('#rule-display-name').empty().append(ruleInfo.name);
+                            $('#rule-description').empty().append(ruleInfo.descriptors['component-type'].description);
+                            $( "#rule-menu-more-info-dialog" ).modal( "show" );
+                            $('.rule-docs-link').click(function () {
+                                // open the documentation for this flow analysis rule
+                                nfShell.showPage('../nifi-docs/documentation?' + $.param({
+                                    select: ruleInfo.type,
+                                    group: ruleInfo.bundle.group,
+                                    artifact: ruleInfo.bundle.artifact,
+                                    version: ruleInfo.bundle.version
+                                })).done(function () {});
+                            });
+                            unbindRuleMenuHandling();
+                        };
+
+                        function unbindRuleMenuHandling() {
+                            $('#rule-menu-more-info').unbind('click', openRuleMoreInfoDialog);
+                            $('#rule-menu-edit-rule').unbind('click', openRuleDetailsDialog);
+                            $(document).unbind('click', closeRuleWindow);
+                        }
+
+                    });
+                },
+
+                /**
+                 * Set event bindings for violation menus
+                 */
+                setViolationMenuHandling: function(response, groupId) {
+                    $('.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
+                        $('#violation-menu-more-info').on( "click", openRuleMoreInfoDialog);
+                        $('#violation-menu-go-to').on('click', goToComponent);
+                        $(document).on('click', closeViolationWindow);
+
+                        function closeViolationWindow(e) {
+                            if ($(e.target).parents("#violation-menu").length === 0) {
+                                $("#violation-menu").hide();
+                                unbindViolationMenuHandling();
+                            }
+                        }
+
+                        function openRuleMoreInfoDialog() {
+                            var rule = response.rules.find(function(rule){ 
+                                return rule.id === violationInfo.ruleId;
+                            })
+                            $('#violation-menu').hide();
+                            $('#violation-type-pill').empty()
+                                                    .removeClass()
+                                                    .addClass(violationInfo.enforcementPolicy.toLowerCase() + ' violation-type-pill')
+                                                    .append(violationInfo.enforcementPolicy);
+                            $('#violation-display-name').empty().append(violationInfo.violationMessage);
+                            $('#violation-description').empty().append(rule.descriptors['component-type'].description);
+                            $('#violation-menu-more-info-dialog').modal( "show" );
+                            $('.violation-docs-link').click(function () {
+                                // open the documentation for this flow analysis rule
+                                nfShell.showPage('../nifi-docs/documentation?' + $.param({
+                                    select: rule.type,
+                                    group: rule.bundle.group,
+                                    artifact: rule.bundle.artifact,
+                                    version: rule.bundle.version
+                                })).done(function () {});
+                            });
+                            unbindViolationMenuHandling();
+                        }
+
+                        function goToComponent() {
+                            $('#violation-menu').hide();
+                            nfCanvasUtils.showComponent(groupId, violationInfo.subjectId);
+                            unbindViolationMenuHandling();
+                        }
+
+                        function unbindViolationMenuHandling() {
+                            $('#violation-menu-go-to').unbind('click', goToComponent);
+                            $('#violation-menu-more-info').unbind('click', openRuleMoreInfoDialog);
+                            $(document).unbind('click', closeViolationWindow);
+                        }
+                    });
+                },
+
+                /**
+                 * Initialize the flow analysis controller.
+                 */
+                init: function () {
+                    var flowAnalysisCtrl = this;
+                    var drawer = $('#flow-analysis-drawer');
+                    var requiredRulesEl = $('#required-rules');
+                    var recommendedRulesEl = $('#recommended-rules');
+                    var newFlowAnalsysisBtnEl = $('#flow-analysis-check-now-btn');
+                    var flowAnalysisRefreshIntervalSeconds = 5;

Review Comment:
   In `nf-canvas-bootstrap.js` there is a variable `autoRefreshIntervalSeconds` that you'll have to plumb down a few layers starting from [1]. Try setting a breakpoint in the `init` function in `nf-ng-canvas-flow-status-controller.js` and you see how it needs to be plumbed down.
   
   [1] https://github.com/apache/nifi/blob/main/nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/js/nf/canvas/nf-canvas-bootstrap.js#L353



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "sardell (via GitHub)" <gi...@apache.org>.
sardell commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1474542558


##########
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,554 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);
+                        $(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);
+                        $(warningListItemNameEl).text(warning.subjectDisplayName);
+                        $(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
+                            violationNameEl.text(violation.subjectDisplayName);
+                            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 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);
+
+                                // 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(response);
+
+                                // setup violation menu handling
+                                flowAnalysisCtrl.setViolationMenuHandling(response, groupId);
+                            }
+                        }).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();
+                        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
+                        $('#rule-menu-more-info').on( "click", openRuleMoreInfoDialog);
+                        $('#rule-menu-edit-rule').on('click', openRuleDetailsDialog);
+                        $(document).on('click', closeRuleWindow);
+
+                        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 openRuleMoreInfoDialog() {
+                            $('#rule-menu').hide();
+                            $('#rule-type-pill').empty()
+                                                .removeClass()
+                                                .addClass(ruleInfo.enforcementPolicy.toLowerCase() + ' rule-type-pill')
+                                                .append(ruleInfo.enforcementPolicy);
+                            $('#rule-display-name').empty().append(ruleInfo.name);
+                            $('#rule-description').empty().append(ruleInfo.descriptors['component-type'].description);
+                            $( "#rule-menu-more-info-dialog" ).modal( "show" );
+                            $('.rule-docs-link').click(function () {
+                                // open the documentation for this flow analysis rule

Review Comment:
   I'm still not sure what caused this issue, but I can't reproduce it at all. Perhaps the changes from my last commit, which moves the documentation link to the rule options menu, resolved this issue. Could you test again with the latest when you have a chance, @tpalfy?



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "sardell (via GitHub)" <gi...@apache.org>.
sardell commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1476627180


##########
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,554 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);
+                        $(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);
+                        $(warningListItemNameEl).text(warning.subjectDisplayName);
+                        $(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
+                            violationNameEl.text(violation.subjectDisplayName);
+                            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 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);
+
+                                // 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(response);
+
+                                // setup violation menu handling
+                                flowAnalysisCtrl.setViolationMenuHandling(response, groupId);
+                            }
+                        }).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();
+                        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
+                        $('#rule-menu-more-info').on( "click", openRuleMoreInfoDialog);
+                        $('#rule-menu-edit-rule').on('click', openRuleDetailsDialog);
+                        $(document).on('click', closeRuleWindow);
+
+                        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 openRuleMoreInfoDialog() {
+                            $('#rule-menu').hide();
+                            $('#rule-type-pill').empty()
+                                                .removeClass()
+                                                .addClass(ruleInfo.enforcementPolicy.toLowerCase() + ' rule-type-pill')
+                                                .append(ruleInfo.enforcementPolicy);
+                            $('#rule-display-name').empty().append(ruleInfo.name);
+                            $('#rule-description').empty().append(ruleInfo.descriptors['component-type'].description);
+                            $( "#rule-menu-more-info-dialog" ).modal( "show" );
+                            $('.rule-docs-link').click(function () {
+                                // open the documentation for this flow analysis rule
+                                nfShell.showPage('../nifi-docs/documentation?' + $.param({
+                                    select: ruleInfo.type,
+                                    group: ruleInfo.bundle.group,
+                                    artifact: ruleInfo.bundle.artifact,
+                                    version: ruleInfo.bundle.version
+                                })).done(function () {});
+                            });
+                            unbindRuleMenuHandling();
+                        };
+
+                        function unbindRuleMenuHandling() {
+                            $('#rule-menu-more-info').unbind('click', openRuleMoreInfoDialog);
+                            $('#rule-menu-edit-rule').unbind('click', openRuleDetailsDialog);
+                            $(document).unbind('click', closeRuleWindow);
+                        }
+
+                    });
+                },
+
+                /**
+                 * Set event bindings for violation menus
+                 */
+                setViolationMenuHandling: function(response, groupId) {
+                    $('.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
+                        $('#violation-menu-more-info').on( "click", openRuleViolationMoreInfoDialog);
+                        $('#violation-menu-go-to').on('click', goToComponent);
+                        $(document).on('click', closeViolationWindow);
+
+                        function closeViolationWindow(e) {
+                            if ($(e.target).parents("#violation-menu").length === 0) {
+                                $("#violation-menu").hide();
+                                unbindViolationMenuHandling();
+                            }
+                        }
+
+                        function openRuleViolationMoreInfoDialog() {
+                            var rule = response.rules.find(function(rule){ 
+                                return rule.id === violationInfo.ruleId;
+                            })
+                            $('#violation-menu').hide();
+                            $('#violation-type-pill').empty()
+                                                    .removeClass()
+                                                    .addClass(violationInfo.enforcementPolicy.toLowerCase() + ' violation-type-pill')
+                                                    .append(violationInfo.enforcementPolicy);
+                            $('#violation-display-name').empty().append(violationInfo.violationMessage);
+                            $('#violation-description').empty().append(rule.descriptors['component-type'].description);
+                            $('#violation-menu-more-info-dialog').modal( "show" );
+                            $('.violation-docs-link').click(function () {
+                                // open the documentation for this flow analysis rule

Review Comment:
   Seems like the issue was an event handler that wasn't removed properly. I've since pushed a fix for the issue.



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "sardell (via GitHub)" <gi...@apache.org>.
sardell commented on PR #8273:
URL: https://github.com/apache/nifi/pull/8273#issuecomment-1924612023

   @tpalfy I believe I addressed all of the issues you pointed out. When you have time, could you please take another look?


-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "mcgilman (via GitHub)" <gi...@apache.org>.
mcgilman commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1507857234


##########
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,556 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);
+                        $(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);
+                        $(warningListItemNameEl).text(warning.subjectDisplayName);
+                        $(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
+                            violationNameEl.text(violation.subjectDisplayName);
+                            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 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);
+
+                                // 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
+                        $('#rule-menu-edit-rule').on('click', openRuleDetailsDialog);
+                        $('#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
+                        $('#violation-menu-more-info').on( "click", openRuleViolationMoreInfoDialog);
+                        // If the groupId and subjectId are not the same, we can select the component
+                        console.log(violationInfo.groupId !== violationInfo.subjectId);
+                        console.log(violationInfo);
+                        if (violationInfo.groupId !== violationInfo.subjectId) {
+                            $('#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');
+                        }
+                        $(document).on('click', closeViolationWindow);
+
+                        function closeViolationWindow(e) {
+                            if ($(e.target).parents("#violation-menu").length === 0) {
+                                $("#violation-menu").hide();
+                                unbindViolationMenuHandling();
+                            }
+                        }
+
+                        function openRuleViolationMoreInfoDialog() {
+                            var rule = rules.find(function(rule){ 
+                                return rule.id === violationInfo.ruleId;
+                            });
+                            $('#violation-menu').hide();
+                            $('#violation-type-pill').empty()
+                                                    .removeClass()
+                                                    .addClass(violationInfo.enforcementPolicy.toLowerCase() + ' violation-type-pill')
+                                                    .append(violationInfo.enforcementPolicy);
+                            $('#violation-description').empty().append(violationInfo.violationMessage);
+                            $('#violation-menu-more-info-dialog').modal( "show" );
+                            $('.violation-docs-link').click(function () {
+                                // open the documentation for this flow analysis rule
+                                nfShell.showPage('../nifi-docs/documentation?' + $.param({
+                                    select: rule.type,
+                                    group: rule.bundle.group,
+                                    artifact: rule.bundle.artifact,
+                                    version: rule.bundle.version
+                                })).done(function () {});
+                            });
+                            unbindViolationMenuHandling();
+                        }
+
+                        function goToComponent() {
+                            $('#violation-menu').hide();
+                            nfCanvasUtils.showComponent(violationInfo.groupId, violationInfo.subjectId);
+                            unbindViolationMenuHandling();
+                        }
+
+                        function unbindViolationMenuHandling() {
+                            $('#violation-menu-more-info').off("click");
+                            $('#violation-menu-go-to').off("click");
+                            $(document).unbind('click', closeViolationWindow);
+                        }
+                    });
+                },
+
+                /**
+                 * Initialize the flow analysis controller.
+                 */
+                init: function () {
+                    var flowAnalysisCtrl = this;
+                    var drawer = $('#flow-analysis-drawer');
+                    var requiredRulesEl = $('#required-rules');
+                    var recommendedRulesEl = $('#recommended-rules');
+                    var newFlowAnalsysisBtnEl = $('#flow-analysis-check-now-btn');
+                    var flowAnalysisRefreshIntervalSeconds = nfCommon.getAutoRefreshInterval();
+
+                    $('#flow-analysis').click(function () {
+                        drawer.toggleClass('opened');
+                    });
+                    requiredRulesEl.accordion({
+                        collapsible: true,
+                        active: false,
+                        icons: {
+                            "header": "fa fa-chevron-down",
+                            "activeHeader": "fa fa-chevron-up"
+                        }
+                    });
+
+                    recommendedRulesEl.accordion({
+                        collapsible: true,
+                        active: false,
+                        icons: {
+                            "header": "fa fa-chevron-down",
+                            "activeHeader": "fa fa-chevron-up"
+                        }
+                    });
+                    $('#rule-menu').hide();
+                    $('#violation-menu').hide();
+                    $('#rule-menu-more-info-dialog').modal({
+                        scrollableContentStyle: 'scrollable',
+                        headerText: 'Rule Information',
+                        buttons: [{
+                            buttonText: 'OK',
+                                color: {
+                                    base: '#728E9B',
+                                    hover: '#004849',
+                                    text: '#ffffff'
+                                },
+                            handler: {
+                                click: function () {
+                                    $(this).modal('hide');
+                                }
+                            }
+                        }],
+                        handler: {
+                            close: function () {}
+                        }
+                    });
+                    $('#violation-menu-more-info-dialog').modal({
+                        scrollableContentStyle: 'scrollable',
+                        headerText: 'Violation Information',
+                        buttons: [{
+                            buttonText: 'OK',
+                                color: {
+                                    base: '#728E9B',
+                                    hover: '#004849',
+                                    text: '#ffffff'
+                                },
+                            handler: {
+                                click: function () {
+                                    $(this).modal('hide');
+                                }
+                            }
+                        }],
+                        handler: {
+                            close: function () {}
+                        }
+                    });
+
+                    this.loadFlowPolicies();
+                    setInterval(this.loadFlowPolicies.bind(this), flowAnalysisRefreshIntervalSeconds * 1000);
+
+                    // add click event listener to refresh button
+                    newFlowAnalsysisBtnEl.on('click', this.createNewFlowAnalysisRequest);
+                    
+                    this.toggleOnlyViolations(false);
+                    this.toggleOnlyWarnings(false);
+                    // handle show only violations checkbox
+                    $('#show-only-violations').on('change', function(event) {
+                        var isChecked = $(this).hasClass('checkbox-checked');
+                        flowAnalysisCtrl.toggleOnlyViolations(isChecked);
+                    });
+
+                    $('#show-only-warnings').on('change', function(event) {

Review Comment:
   Choosing `show-only-warnings` is showing some confusing results.
   
   ![Kapture 2024-02-29 at 11 23 45](https://github.com/apache/nifi/assets/123395/c67d6bc9-1041-44b0-9988-000c988fda56)
   



##########
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,556 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);
+                        $(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);
+                        $(warningListItemNameEl).text(warning.subjectDisplayName);
+                        $(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
+                            violationNameEl.text(violation.subjectDisplayName);
+                            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 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);
+
+                                // 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
+                        $('#rule-menu-edit-rule').on('click', openRuleDetailsDialog);
+                        $('#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
+                        $('#violation-menu-more-info').on( "click", openRuleViolationMoreInfoDialog);
+                        // If the groupId and subjectId are not the same, we can select the component
+                        console.log(violationInfo.groupId !== violationInfo.subjectId);
+                        console.log(violationInfo);
+                        if (violationInfo.groupId !== violationInfo.subjectId) {
+                            $('#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');
+                        }
+                        $(document).on('click', closeViolationWindow);
+
+                        function closeViolationWindow(e) {
+                            if ($(e.target).parents("#violation-menu").length === 0) {
+                                $("#violation-menu").hide();
+                                unbindViolationMenuHandling();
+                            }
+                        }
+
+                        function openRuleViolationMoreInfoDialog() {
+                            var rule = rules.find(function(rule){ 
+                                return rule.id === violationInfo.ruleId;
+                            });
+                            $('#violation-menu').hide();
+                            $('#violation-type-pill').empty()
+                                                    .removeClass()
+                                                    .addClass(violationInfo.enforcementPolicy.toLowerCase() + ' violation-type-pill')
+                                                    .append(violationInfo.enforcementPolicy);
+                            $('#violation-description').empty().append(violationInfo.violationMessage);
+                            $('#violation-menu-more-info-dialog').modal( "show" );
+                            $('.violation-docs-link').click(function () {
+                                // open the documentation for this flow analysis rule
+                                nfShell.showPage('../nifi-docs/documentation?' + $.param({
+                                    select: rule.type,
+                                    group: rule.bundle.group,
+                                    artifact: rule.bundle.artifact,
+                                    version: rule.bundle.version
+                                })).done(function () {});
+                            });
+                            unbindViolationMenuHandling();
+                        }
+
+                        function goToComponent() {
+                            $('#violation-menu').hide();
+                            nfCanvasUtils.showComponent(violationInfo.groupId, violationInfo.subjectId);
+                            unbindViolationMenuHandling();
+                        }

Review Comment:
   This doesn't appear to work for non Processor components.



##########
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,556 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);

Review Comment:
   When I was verifying things I noticed that the flow analysis results may leak component details. If I don't have permission for a component that violates a rule, the component name is still rendered. Then the user can simply navigate to the component in question. In past features, referencing components where the user lacked permissions were rendered as `unauthorized`. 



##########
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,85 @@
+<%--
+ 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 class="flow-analysis-refresh-container">
+            <button id="flow-analysis-check-now-btn" class="flow-analysis-check-now-btn">Start a new analysis</button>

Review Comment:
   When the user clicks this button there is no feedback. With no feedback, I was questioning whether it worked or not. As I repeatedly click on the link, new flow analysis reports seem to be generated. At a minimum, we should probably limit a client from having more than one outstanding request.
   
   Alternatively, should be feature be simplified and the UI only show whatever the current results are? If so, we could just remove this link.



##########
nifi-nar-bundles/nifi-framework-bundle/nifi-framework/nifi-web/nifi-web-ui/src/main/webapp/WEB-INF/partials/canvas/flow-status.jsp:
##########
@@ -71,6 +71,7 @@
             <button id="search-button" ng-click="appCtrl.serviceProvider.headerCtrl.flowStatusCtrl.search.toggleSearchField();"><i class="fa fa-search"></i></button>
             <input id="search-field" type="text" placeholder="Search"/>
         </div>
+        <button id="flow-analysis" class="flow-analysis"><i class="fa fa-lightbulb-o flow-analysis-notification-icon"></i></button>

Review Comment:
   Is there a reason the background color of this button is different? It appears to match the hover state of the search button.
   
   ![Screenshot 2024-02-29 at 11 16 04 AM](https://github.com/apache/nifi/assets/123395/8fae8054-5a89-4e55-94c2-d7e53cba3050)
   



##########
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,556 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);
+                        $(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);
+                        $(warningListItemNameEl).text(warning.subjectDisplayName);
+                        $(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
+                            violationNameEl.text(violation.subjectDisplayName);
+                            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 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);
+
+                                // 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
+                        $('#rule-menu-edit-rule').on('click', openRuleDetailsDialog);
+                        $('#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();
+                        }

Review Comment:
   Users that lack permission to the controller won't be able to view the rules. Currently, the application allows all users to attempt this. If the user lacks permission they will be shown a dialog with a corresponding error message. But I think could we should be able to check this ahead of time and hide this menu item accordingly.



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "sardell (via GitHub)" <gi...@apache.org>.
sardell commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1508148578


##########
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,556 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);

Review Comment:
   > In past features, referencing components where the user lacked permissions were rendered as unauthorized
   
   I'm assuming that those past features would handle the permission logic on the backend so details like the name aren't returned in the request response. 
   
   cc @tpalfy 



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org


Re: [PR] NIFI-11520: Add a menu to display Flow Analysis report results [nifi]

Posted by "mcgilman (via GitHub)" <gi...@apache.org>.
mcgilman commented on code in PR #8273:
URL: https://github.com/apache/nifi/pull/8273#discussion_r1526709847


##########
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,556 @@
                 }
             }
 
+            /**
+             * 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();
+                    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);
+                        $(violationListItemNameEl).text(violation.subjectDisplayName);

Review Comment:
   @tpalfy Typically we would just report it as unauthorized and clear out component details. Please see the Controller Service references, Parameter references, Parameter Provider references, etc for examples.



-- 
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: issues-unsubscribe@nifi.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org