You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@brooklyn.apache.org by he...@apache.org on 2021/05/03 08:27:18 UTC

[brooklyn-ui] 01/08: Display node hierarchy in App Inspector based on known relationships in the node tree

This is an automated email from the ASF dual-hosted git repository.

heneveld pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/brooklyn-ui.git

commit 9cecc3748d72c7b7272467d789238d62b921b3d0
Author: Mykola Mandra <my...@cloudsoftcorp.com>
AuthorDate: Mon Apr 26 12:42:05 2021 +0100

    Display node hierarchy in App Inspector based on known relationships in the node tree
    
    Signed-off-by: Mykola Mandra <my...@cloudsoftcorp.com>
---
 .../app/components/entity-tree/entity-node.html    |  14 +-
 .../app/components/entity-tree/entity-node.less    |   3 +
 .../entity-tree/entity-tree.directive.js           | 185 ++++++++++++++++++++-
 .../app/components/entity-tree/entity-tree.html    |   2 +-
 .../app/views/main/main.controller.js              |  21 +++
 ui-modules/app-inspector/app/views/main/main.less  |   7 +
 .../app/views/main/main.template.html              |  16 +-
 7 files changed, 241 insertions(+), 7 deletions(-)

diff --git a/ui-modules/app-inspector/app/components/entity-tree/entity-node.html b/ui-modules/app-inspector/app/components/entity-tree/entity-node.html
index dd5fd6d..cbc417f 100644
--- a/ui-modules/app-inspector/app/components/entity-tree/entity-node.html
+++ b/ui-modules/app-inspector/app/components/entity-tree/entity-node.html
@@ -17,7 +17,7 @@
   under the License.
 -->
 <div class="entity-node">
-    <div ng-if="isOpen" class="entity-node-item" ng-class="{'active': isSelected()}" uib-popover-template="'EntityNodeInfoTemplate.html'" popover-trigger="'mouseenter'" popover-placement="right" popover-popup-delay="1000">
+    <div ng-if="isOpen" class="entity-node-item" ng-class="{ 'active': isSelected(), 'secondary' : !isInSpotlight() }" uib-popover-template="'EntityNodeInfoTemplate.html'" popover-trigger="'mouseenter'" popover-placement="right" popover-popup-delay="1000">
         <a ng-href="{{getHref()}}" class="entity-node-link">
             <brooklyn-status-icon value="{{entity.serviceState}}" ng-if="entity.serviceState || entity.applicationId"></brooklyn-status-icon>
             <i class="fa fa-2x fa-external-link" ng-if="!entity.serviceState && !entity.applicationId"></i>
@@ -25,14 +25,20 @@
             <span class="node-icon"><img ng-src="{{ iconUrl }}"/></span>
         </a>
         <div class="entity-node-toggle-wrapper">
-          <div class="entity-node-toggle" ng-if="entity.children.length > 0 || entity.members.length > 0" ng-click="onToggle($event)" >
+          <div class="entity-node-toggle" ng-if="entitiesInCurrentView(entity.children) > 0 || entitiesInCurrentView(entity.members) > 0" ng-click="onToggle($event)" >
             <span class="glyphicon" ng-class="isChildrenOpen ? 'glyphicon-chevron-up' : 'glyphicon-chevron-down'"></span>
           </div>
         </div>
     </div>
     <div class="entity-node-children" ng-show="isChildrenOpen">
-        <entity-node ng-repeat="child in entity.children track by child.id" entity="child" application-id="applicationId"></entity-node>
-        <entity-node ng-if="!entity.children || entity.children.length === 0" ng-repeat="child in entity.members track by child.id" entity="child" application-id="applicationId"></entity-node>
+        <!-- Entity children -->
+        <entity-node ng-repeat="child in entity.children track by child.id"
+                     ng-show="child.viewModes.has(viewMode)"
+                     entity="child" application-id="applicationId" view-mode="viewMode"></entity-node>
+        <!-- Or entity members -->
+        <entity-node ng-repeat="child in entity.members track by child.id"
+                     ng-show="child.viewModes.has(viewMode) && (!entity.children || entity.children.length === 0)"
+                     entity="child" application-id="applicationId" view-mode="viewMode"></entity-node>
     </div>
     <script type="text/ng-template" id="EntityNodeInfoTemplate.html">
         <table ng-if="isOpen" class="info-table">
diff --git a/ui-modules/app-inspector/app/components/entity-tree/entity-node.less b/ui-modules/app-inspector/app/components/entity-tree/entity-node.less
index 0ec4b46..a3fe401 100644
--- a/ui-modules/app-inspector/app/components/entity-tree/entity-node.less
+++ b/ui-modules/app-inspector/app/components/entity-tree/entity-node.less
@@ -47,6 +47,9 @@
     border-radius: @border-radius-base;
     box-shadow: 0 1px 2px rgba(0,0,0,0.1);
     transition: all .2s ease-in-out;
+    &.secondary {
+      background-color: lighten(@brand-primary, 55%);
+    }
     &:hover {
       background-color: @body-bg;
     }
diff --git a/ui-modules/app-inspector/app/components/entity-tree/entity-tree.directive.js b/ui-modules/app-inspector/app/components/entity-tree/entity-tree.directive.js
index f6d3426..43aad2c 100644
--- a/ui-modules/app-inspector/app/components/entity-tree/entity-tree.directive.js
+++ b/ui-modules/app-inspector/app/components/entity-tree/entity-tree.directive.js
@@ -30,6 +30,12 @@ import {detailState} from '../../views/main/inspect/activities/detail/detail.con
 import {managementState} from '../../views/main/inspect/management/management.controller';
 import {detailState as managementDetailState} from '../../views/main/inspect/management/detail/detail.controller';
 import {HIDE_INTERSTITIAL_SPINNER_EVENT} from 'brooklyn-ui-utils/interstitial-spinner/interstitial-spinner';
+import {
+    RELATIONSHIP_HOST_FOR,
+    RELATIONSHIP_HOSTED_ON,
+    VIEW_HOST_FOR_HOSTED_ON,
+    VIEW_PARENT_CHILD
+} from '../../views/main/main.controller';
 
 const MODULE_NAME = 'inspector.entity.tree';
 
@@ -44,7 +50,9 @@ export function entityTreeDirective() {
         restrict: 'E',
         template: entityTreeTemplate,
         scope: {
-           sortReverse: '=',
+            sortReverse: '=',
+            viewModes: '=',
+            viewMode: '<'
         },
         controller: ['$scope', '$state', 'applicationApi', 'entityApi', 'iconService', 'brWebNotifications', controller],
         controllerAs: 'vm'
@@ -59,6 +67,7 @@ export function entityTreeDirective() {
 
         applicationApi.applicationsTree().then((response)=> {
             vm.applications = response.data;
+            analyzeRelationships(vm.applications);
 
             observers.push(response.subscribe((response)=> {
                 response.data
@@ -79,6 +88,7 @@ export function entityTreeDirective() {
                     });
 
                 vm.applications = response.data;
+                analyzeRelationships(vm.applications);
 
                 function spawnNotification(app, opts) {
                     iconService.get(app).then((icon)=> {
@@ -90,6 +100,166 @@ export function entityTreeDirective() {
                     });
                 }
             }));
+
+            // TODO SMART-143
+            function analyzeRelationships(entityTree) {
+                let entities = entityTreeToArray(entityTree);
+                let relationships = findAllRelationships(entities);
+
+                // Initialize entity tree with 'parent/child' view first (default view).
+                initParentChildView(entities);
+
+                // Identify new view modes based on relationships. This adds a drop-down menu with new views if found any.
+                updateViewModes(relationships);
+
+                // Re-arrange entity tree for 'host_for/hosted_on' view if present.
+                if ($scope.viewModes.has(VIEW_HOST_FOR_HOSTED_ON)) {
+                    addHostForHostedOnView(entities, relationships);
+                }
+            }
+
+            // TODO SMART-143
+            function entityTreeToArray(entities) {
+                let children = [];
+                if (!Array.isArray(entities) || entities.length === 0) {
+                    return children;
+                }
+                entities.forEach(entity => {
+                    children = children.concat(entityTreeToArray(entity.children));
+                    children = children.concat(entityTreeToArray(entity.members));
+                })
+                return entities.concat(children);
+            }
+
+            // TODO SMART-143
+            function addHostForHostedOnView(entities, relationships) {
+                entities.forEach(entity => {
+                    let relationship = relationships.find(r => r.id === entity.id);
+                    if (relationship && relationship.name === RELATIONSHIP_HOST_FOR) {
+                        displayEntityInView(entity, VIEW_HOST_FOR_HOSTED_ON);
+                        spotlightEntityInView(entity, VIEW_HOST_FOR_HOSTED_ON);
+
+                        relationship.targets.forEach(target => {
+                            let child = entities.find(e => e.id === target);
+                            if (child) {
+                                spotlightEntityInView(child, VIEW_HOST_FOR_HOSTED_ON);
+                                if (child.parentId !== entity.id) { // Move (copy) child under 'hosted_on' entity.
+                                    let childCopy = Object.assign({}, child); // Copy entity
+
+                                    // Display in 'host_for/hosted_on' view only.
+                                    childCopy.viewModes = null;
+                                    displayEntityInView(childCopy, VIEW_HOST_FOR_HOSTED_ON);
+
+                                    let parent = findEntity(entities, child.parentId);
+                                    if (parent) {
+                                        childCopy.name += ' (' + parent.name + ')';
+                                    }
+
+                                    if (!entity.children) {
+                                        entity.children = [childCopy];
+                                    } else {
+                                        entity.children.push(childCopy);
+                                    }
+                                }
+                                displayParentsInView(entities, child.parentId, VIEW_HOST_FOR_HOSTED_ON);
+                            }
+                        });
+                    } else if (!relationship || relationship.name !== RELATIONSHIP_HOSTED_ON) {
+                        // Display original position for any other entity under 'host_for/hosted_on' view.
+                        displayEntityInView(entity, VIEW_HOST_FOR_HOSTED_ON);
+                        // Spotlight will not be on entities that are required to be displayed but do not belong to this view.
+                    }
+                });
+            }
+
+            // TODO SMART-143
+            function displayParentsInView(entities, id, viewMode) {
+                let entity = findEntity(entities, id);
+                if (entity) {
+                    displayEntityInView(entity, viewMode);
+                    displayParentsInView(entities, entity.parentId, viewMode);
+                }
+            }
+
+            // TODO SMART-143
+            function findEntity(entities, id) {
+                return entities.find(entity => entity.id === id);
+            }
+
+            // TODO SMART-143
+            function displayEntityInView(entity, viewMode) {
+                if (!entity.viewModes) {
+                    entity.viewModes = new Set([viewMode]);
+                } else {
+                    entity.viewModes.add(viewMode);
+                }
+            }
+
+            // TODO SMART-143
+            function spotlightEntityInView(entity, viewMode) {
+                if (!entity.viewModesSpotLight) {
+                    entity.viewModesSpotLight = new Set([viewMode]);
+                } else {
+                    entity.viewModesSpotLight.add(viewMode);
+                }
+            }
+
+            /**
+             * Initializes entity tree with 'parent/child' view mode. This is a default view mode.
+             *
+             * @param {Object} entities The entity tree to initialize with 'parent/child' view mode.
+             */
+            function initParentChildView(entities) {
+                entities.forEach(entity => {
+                    displayEntityInView(entity, VIEW_PARENT_CHILD);
+                    spotlightEntityInView(entity, VIEW_PARENT_CHILD);
+                });
+            }
+
+            /**
+             * Identifies new view modes based on relationships between entities. Updates $scope.viewModes set.
+             *
+             * @param {Object} relationships The entity tree relationships.
+             */
+            function updateViewModes(relationships) {
+                let viewModesDiscovered = new Set([VIEW_PARENT_CHILD]); // 'parent/child' view mode is a minimum required
+
+                relationships.forEach(relationship => {
+                    relationship.targets.forEach(id => {
+                        let target = relationships.find(item => item.id === id);
+                        if (target) {
+                            let uniqueRelationshipName = [relationship.name, target.name].sort().join('/'); // e.g. host_for/hosted_on
+                            viewModesDiscovered.add(uniqueRelationshipName);
+                        }
+                    })
+                });
+
+                $scope.viewModes = viewModesDiscovered; // Refresh view modes
+            }
+
+            // TODO SMART-143
+            function findAllRelationships(entities) {
+                let relationships = [];
+
+                if (!Array.isArray(entities) || entities.length === 0) {
+                    return relationships;
+                }
+
+                entities.forEach(entity => {
+                    if (Array.isArray(entity.relations)) {
+                        entity.relations.forEach(r => {
+                            let relationship = {
+                                id: entity.id,
+                                name: r.type.name.split('/')[0], // read name up until '/', e.g. take 'hosted_on' from 'hosted_on/oU7i'
+                                targets: Array.isArray(r.targets) ? r.targets : []
+                            }
+                            relationships.push(relationship)
+                        });
+                    }
+                });
+
+                return relationships;
+            }
         });
 
         $scope.$on('$destroy', ()=> {
@@ -107,6 +277,7 @@ export function entityNodeDirective() {
         scope: {
             entity: '<',
             applicationId: '<',
+            viewMode: '<'
         },
         link: link,
         controller: ['$scope', '$state', '$stateParams', 'iconService', controller]
@@ -189,5 +360,17 @@ export function entityNodeDirective() {
             }
         };
 
+        // TODO SMART-143
+        $scope.isInSpotlight = function() {
+            return $scope.entity.viewModesSpotLight.has($scope.viewMode);
+        };
+
+        // TODO SMART-143
+        $scope.entitiesInCurrentView = (entities) => {
+            if (!entities) {
+                return 0;
+            }
+            return entities.filter(entity => entity.viewModes.has($scope.viewMode)).length || 0;
+        }
     }
 }
diff --git a/ui-modules/app-inspector/app/components/entity-tree/entity-tree.html b/ui-modules/app-inspector/app/components/entity-tree/entity-tree.html
index b105215..4906d1e 100644
--- a/ui-modules/app-inspector/app/components/entity-tree/entity-tree.html
+++ b/ui-modules/app-inspector/app/components/entity-tree/entity-tree.html
@@ -16,7 +16,7 @@
   specific language governing permissions and limitations
   under the License.
 -->
-<entity-node ng-repeat="application in vm.applications | orderBy: sortReverse? '-creationTimeUtc': 'creationTimeUtc' track by application.id" entity="application" application-id="application.id"></entity-node>
+<entity-node ng-repeat="application in vm.applications | orderBy: sortReverse? '-creationTimeUtc': 'creationTimeUtc' track by application.id" entity="application" application-id="application.id" view-mode="viewMode"></entity-node>
 <p class="expand-tree-message text-center" ng-if="vm.applications.length > 0"><small><kbd>shift</kbd> + <kbd>{{navigator.appVersion.indexOf("Mac") !== -1 ? '⌘' : '⊞'}}</kbd> + click to expand all children</small></p>
 <div class="empty-tree text-muted text-center" ng-if="vm.applications.length === 0">
     <hr />
diff --git a/ui-modules/app-inspector/app/views/main/main.controller.js b/ui-modules/app-inspector/app/views/main/main.controller.js
index 31502f8..c268bc2 100644
--- a/ui-modules/app-inspector/app/views/main/main.controller.js
+++ b/ui-modules/app-inspector/app/views/main/main.controller.js
@@ -27,6 +27,15 @@ export const mainState = {
     controllerAs: 'ctrl'
 };
 
+// Entity relationship constants
+export const RELATIONSHIP_HOST_FOR = 'host_for';
+export const RELATIONSHIP_HOSTED_ON = 'hosted_on';
+
+// View mode constants
+export const RELATIONSHIP_VIEW_DELIMITER = '/';
+export const VIEW_PARENT_CHILD = 'parent/child';
+export const VIEW_HOST_FOR_HOSTED_ON = RELATIONSHIP_HOST_FOR + RELATIONSHIP_VIEW_DELIMITER + RELATIONSHIP_HOSTED_ON;
+
 const savedSortReverse = 'app-inspector-sort-reverse';
 
 export function mainController($scope, $q, brWebNotifications, brBrandInfo) {
@@ -36,6 +45,18 @@ export function mainController($scope, $q, brWebNotifications, brBrandInfo) {
 
     ctrl.composerUrl = brBrandInfo.blueprintComposerBaseUrl;
 
+    // TODO SMART-143
+    ctrl.viewMode = VIEW_PARENT_CHILD;
+    ctrl.viewModes = new Set([VIEW_PARENT_CHILD]);
+    ctrl.viewModesArray = () => Array.from(ctrl.viewModes); // Array from set for ng-repeat component
+
+    // TODO SMART-143
+    $scope.$watch('ctrl.viewModes', () => {
+        if (!ctrl.viewModes.has(ctrl.viewMode)) {
+            ctrl.viewMode = VIEW_PARENT_CHILD; // Default to 'parent/child' view if current is not available anymore.
+        }
+    });
+
     ctrl.sortReverse = localStorage && localStorage.getItem(savedSortReverse) !== null ?
         JSON.parse(localStorage.getItem(savedSortReverse)) :
         true;
diff --git a/ui-modules/app-inspector/app/views/main/main.less b/ui-modules/app-inspector/app/views/main/main.less
index 527554f..2e2ebde 100644
--- a/ui-modules/app-inspector/app/views/main/main.less
+++ b/ui-modules/app-inspector/app/views/main/main.less
@@ -52,5 +52,12 @@
   .entity-tree-action-bar {
     margin-right: -0.5rem;
   }
+
+  .view-mode-item {
+    &.active {
+      color: #fff;
+      background-color: @brand-primary;
+    }
+  }
 }
 
diff --git a/ui-modules/app-inspector/app/views/main/main.template.html b/ui-modules/app-inspector/app/views/main/main.template.html
index f3bec97..a4165b5 100644
--- a/ui-modules/app-inspector/app/views/main/main.template.html
+++ b/ui-modules/app-inspector/app/views/main/main.template.html
@@ -29,6 +29,20 @@
                                     <span class="glyphicon" ng-class="ctrl.sortReverse ? 'fa fa-sort-amount-desc' : 'fa fa-sort-amount-asc'"></span>
                                 </div>
                             </button>
+                            <div class="btn-group" ng-if="ctrl.viewModes.size > 1" uib-dropdown>
+                                <button class="btn btn-sm btn-default" uib-tooltip="Switch view" tooltip-append-to-body="true" uib-dropdown-toggle/>
+                                    <div>
+                                        <span class="fa fa-sitemap"></span>
+                                    </div>
+                                </button>
+                                <ul class="dropdown-menu" uib-dropdown-menu>
+                                    <li ng-repeat="viewMode in ctrl.viewModesArray()">
+                                        <a class="view-mode-item"
+                                           ng-click="ctrl.viewMode = viewMode"
+                                           ng-class="{'active': ctrl.viewMode === viewMode}">{{viewMode}}</a>
+                                    </li>
+                                </ul>
+                            </div>
                         </div>
                         <div class="entity-tree-header-section">
                             <button class="btn btn-link entity-tree-action"
@@ -51,7 +65,7 @@
                         </div>
                     </div>
 
-                    <entity-tree sort-reverse="ctrl.sortReverse"></entity-tree>
+                    <entity-tree sort-reverse="ctrl.sortReverse" view-mode="ctrl.viewMode" view-modes="ctrl.viewModes"></entity-tree>
                 </br-card-content>
             </br-card>