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>
