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 <[email protected]>
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 <[email protected]>
---
 .../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>
 

Reply via email to