| /* |
| * 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. |
| */ |
| import angular from 'angular'; |
| |
| import brooklynStatus from 'brooklyn-ui-utils/status/status'; |
| import brWebNotifications from 'brooklyn-ui-utils/web-notifications/web-notifications'; |
| |
| import entityTreeTemplate from './entity-tree.html'; |
| import entityNodeTemplate from './entity-node.html'; |
| import {inspectState} from '../../views/main/inspect/inspect.controller'; |
| import {summaryState} from '../../views/main/inspect/summary/summary.controller'; |
| import {activitiesState} from '../../views/main/inspect/activities/activities.controller'; |
| import {detailState} from '../../views/main/inspect/activities/detail/detail.controller'; |
| 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_VIEW_DELIMITER, VIEW_PARENT_CHILD} from '../../views/main/main.controller'; |
| |
| const MODULE_NAME = 'inspector.entity.tree'; |
| |
| angular.module(MODULE_NAME, [brooklynStatus, brWebNotifications]) |
| .directive('entityTree', entityTreeDirective) |
| .directive('entityNode', entityNodeDirective); |
| |
| export default MODULE_NAME; |
| |
| export function entityTreeDirective() { |
| return { |
| restrict: 'E', |
| template: entityTreeTemplate, |
| scope: { |
| sortReverse: '=', |
| viewModes: '=', |
| viewMode: '<' |
| }, |
| controller: ['$scope', '$state', 'applicationApi', 'entityApi', 'iconService', 'brWebNotifications', controller], |
| controllerAs: 'vm' |
| }; |
| |
| function controller($scope, $state, applicationApi, entityApi, iconService, brWebNotifications) { |
| $scope.$emit(HIDE_INTERSTITIAL_SPINNER_EVENT); |
| |
| let vm = this; |
| |
| let observers = []; |
| |
| applicationApi.applicationsTree({ |
| sensors: 'deployment.metadata' |
| }).then((response)=> { |
| vm.applications = response.data; |
| analyzeRelationships(vm.applications); |
| |
| observers.push(response.subscribe((response)=> { |
| response.data |
| .filter(x => vm.applications.map(y => y.id).indexOf(x.id) === -1) |
| .forEach((app) => { |
| spawnNotification(app, { |
| body: 'New application deployment. Current status: ' + app.serviceState, |
| data: $state.href('main.inspect.summary', {applicationId: app.id, entityId: app.id}) |
| }); |
| }); |
| vm.applications |
| .filter(x => response.data.map(y => y.id).indexOf(x.id) === -1) |
| .forEach((app) => { |
| spawnNotification(app, { |
| body: 'Application permanently un-deployed', |
| data: $state.href('main') |
| }); |
| }); |
| |
| vm.applications = response.data; |
| analyzeRelationships(vm.applications); |
| |
| function spawnNotification(app, opts) { |
| iconService.get(app).then((icon)=> { |
| let options = Object.assign({ |
| icon: app.iconUrl || icon, |
| }, opts); |
| |
| brWebNotifications.send('Brooklyn: ' + app.name, options); |
| }); |
| } |
| })); |
| |
| /** |
| * Analyzes relationships of the entity tree and prepares mode views, e.g. 'parent/child'. |
| * |
| * @param {Array.<Object>} entityTree The entity tree to process and prepare view modes for. |
| */ |
| 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 other views if present. |
| addOtherViews(entities, relationships); |
| } |
| |
| /** |
| * Converts entity tree to array of entities. |
| * |
| * @param {Array.<Object>} entities The entity tree to convert. |
| * @returns {Array.<Object>} The array of all entities found in the tree. |
| */ |
| function entityTreeToArray(entities) { |
| let nodes = []; |
| if (!Array.isArray(entities) || entities.length === 0) { |
| return nodes; |
| } |
| entities.forEach(entity => { |
| nodes = nodes.concat(entityTreeToArray(entity.children)); |
| nodes = nodes.concat(entityTreeToArray(entity.members)); |
| }); |
| return entities.concat(nodes); |
| } |
| |
| /** |
| * Extends entity tree with other view modes. Moves entities (creates copies) if their host is |
| * not a parent and labels them to display under other view modes only. |
| * |
| * @param {Array.<Object>} entities The entity tree converted to array. |
| * @param {Array.<Object>} relationships The relationships of entities. |
| */ |
| function addOtherViews(entities, relationships) { |
| let otherViews = Array.from($scope.viewModes).filter(r => r !== VIEW_PARENT_CHILD); |
| otherViews.forEach(view => { |
| |
| // Get 'OTHER_PARENT' and 'OTHER_CHILD' identifiers. |
| const {OTHER_PARENT, OTHER_CHILD} = getRelationshipViewIdentifiers(view); |
| |
| // Phase 1. Look through all entities found in the entity tree: process entity with 'OTHER_PARENT' |
| // roles and entities without roles. |
| entities.forEach(entity => { |
| |
| // Get relationship roles for an entity. |
| const {parentRole, childRole} = getRelationshipRoles(relationships, entity.id, OTHER_PARENT, OTHER_CHILD); |
| |
| if (parentRole) { |
| |
| // Label every 'OTHER_PARENT' entity to display and highlight in 'OTHER_PARENT/OTHER_CHILD' |
| // view mode, only if it does not play the role of 'OTHER_CHILD' at the same time. |
| if (!childRole) { |
| displayEntityInView(entity, view); |
| highlightEntityInView(entity, view); |
| } |
| |
| // Look for 'OTHER_CHILD' entities under 'OTHER_PARENT', flip or move them and label to display in |
| // 'OTHER_PARENT/OTHER_CHILD' view mode respectively. |
| parentRole.targets.forEach(target => { |
| let relatedEntity = findEntity(entities, target); |
| if (relatedEntity) { |
| highlightEntityInView(relatedEntity, view); |
| displayParentsInView(entities, relatedEntity.parentId, view); |
| |
| // Re-arrange the tree if related 'OTHER_CHILD' entity is not a child of 'OTHER_PARENT'. |
| if (relatedEntity.parentId !== entity.id) { |
| |
| if (relatedEntity.id === entity.parentId) { |
| // 4.1. Flip 'OTHER_CHILD' parent with a 'OTHER_PARENT' child. |
| flipParentAndChild(relatedEntity, entity, entities, view); |
| } else { |
| // 4.2. Move 'OTHER_CHILD' entity to a new 'OTHER_PARENT' parent. |
| moveEntityToParent(relatedEntity, entity, entities, view); |
| } |
| } |
| } |
| }); |
| } else if (!parentRole && !childRole) { |
| |
| // Display original position for any other entity under 'OTHER_PARENT/OTHER_CHILD' view. Do |
| // no highlight entities that are required to be displayed but do not belong to this view. |
| displayEntityInView(entity, view); |
| } |
| }); |
| |
| // Phase 2. Look through all entities found again and process entities that play both roles: |
| // 'OTHER_PARENT' and 'OTHER_CHILD'. |
| entities.forEach(entity => { |
| |
| // Get relationship roles for an entity. |
| const {parentRole, childRole} = getRelationshipRoles(relationships, entity.id, OTHER_PARENT, OTHER_CHILD); |
| |
| if (childRole) { |
| |
| if (parentRole) { |
| // Find new position where entity is an 'OTHER_CHILD' already, but to become 'OTHER_PARENT' as well. |
| let newParentPosition = findEntityInOtherNodes(entities, entity.id); |
| |
| // Move 'OTHER_CHILD' entities added in Phase 1 to a new parent. |
| if (entity.otherNodes && newParentPosition) { |
| entity.otherNodes.forEach(entityToMove => { |
| moveEntityToParent(entityToMove, newParentPosition, entities, view, false); |
| }); |
| } |
| } |
| |
| adjustVisibilityOfMovedEntity(entity, view); |
| } |
| }); |
| }); |
| } |
| |
| /** |
| * @returns {{OTHER_PARENT, OTHER_CHILD}} tuple with relationship view identifiers. |
| */ |
| function getRelationshipViewIdentifiers(view) { |
| const otherParentChildIdentifiers = view.split(RELATIONSHIP_VIEW_DELIMITER); |
| let OTHER_PARENT = otherParentChildIdentifiers[0]; |
| let OTHER_CHILD = otherParentChildIdentifiers[1]; |
| return {OTHER_PARENT, OTHER_CHILD}; |
| } |
| |
| /** |
| * @returns {{parentRole, childRole}} relationships tuple with parent and child roles. |
| */ |
| function getRelationshipRoles(relationships, entityId, parentViewIdentifier, childViewIdentifier) { |
| const relationshipsFound = relationships.filter(r => r.id === entityId); |
| const parentRole = relationshipsFound.find(r => r.name === parentViewIdentifier); |
| const childRole = relationshipsFound.find(r => r.name === childViewIdentifier); |
| return {parentRole, childRole}; |
| } |
| |
| /** |
| * Flips parent entity with its child. |
| * |
| * @param {Object} parent The parent entity to flip with its child. |
| * @param {Object} child The child entity to flip with its parent. |
| * @param {Array.<Object>} entities The entity tree converted to array. |
| * @param {string} viewMode The view mode to display copy of the entity in only. |
| */ |
| function flipParentAndChild(parent, child, entities, viewMode) { |
| let parentOfTheParent = findEntity(entities, parent.parentId); |
| if (parentOfTheParent) { |
| hideEntityInView(child, viewMode); |
| let childCopy = moveEntityToParent(child, parentOfTheParent, entities, viewMode); |
| moveEntityToParent(parent, childCopy, entities, viewMode); |
| } |
| } |
| |
| /** |
| * Moves entity to a new parent, creates copy of the entity under parent.otherNodes. |
| * |
| * @param {Object} entity The entity to move. |
| * @param {Object} parent The parent to move entity to. |
| * @param {Array.<Object>} entities The entity tree converted to array. |
| * @param {string} viewMode The view mode to display copy of the entity in only. |
| * @param {boolean} decorateName Indicates whether to decorate name or not, true by default. |
| * @returns {Object} The copy of the entity under parent.otherNodes. |
| */ |
| function moveEntityToParent(entity, parent, entities, viewMode, decorateName = true) { |
| // 1. Create a copy. |
| let entityCopy = Object.assign({}, entity); |
| |
| // 2. Include the name of the original parent. |
| let parentOfEntity = findEntity(entities, entityCopy.parentId); |
| if (parentOfEntity && decorateName) { |
| entityCopy.name += ' (' + parentOfEntity.name + ')'; |
| } |
| |
| // 3. Label it to display in a view mode specified only. |
| entityCopy.viewModes = null; |
| displayEntityInView(entityCopy, viewMode); |
| |
| // 4. Mark original entity as been "moved" in this view. |
| if (!entity.movedInView) { |
| entity.movedInView = new Set([viewMode]); |
| } else { |
| entity.movedInView.add(viewMode); |
| } |
| |
| // 5. Add copy under otherNodes. |
| if (!parent.otherNodes) { |
| parent.otherNodes = [entityCopy]; |
| } else { |
| parent.otherNodes.push(entityCopy); |
| } |
| |
| return entityCopy; |
| } |
| |
| /** |
| * Labels all parents to display for a particular view mode starting from a specified ID, traverses the node |
| * tree recursively, bottom-up. |
| * |
| * @param {Array.<Object>} entities The array of entities to search parents to label. |
| * @param {string} id The ID of a parent entity to start labelling. |
| * @param {string} viewMode The view mode to display parent in. |
| */ |
| function displayParentsInView(entities, id, viewMode) { |
| let entity = findEntity(entities, id); |
| if (entity) { |
| displayEntityInView(entity, viewMode); |
| displayParentsInView(entities, entity.parentId, viewMode); |
| } |
| } |
| |
| /** |
| * Attempts to find entity with ID specified in array of entities. |
| * |
| * @param {Array.<Object>} entities The array of entities to search for a particular entity in. |
| * @param {string} id The ID of entity to look for. |
| * @returns {Object} The entity with ID requested, and undefined otherwise. |
| */ |
| function findEntity(entities, id) { |
| return entities.find(entity => entity.id === id) || null; |
| } |
| |
| /** |
| * Attempts to find entity with ID specified in array of entities under 'otherNodes'. |
| * |
| * @param {Array.<Object>} entities The array of entities to search for a particular entity in. |
| * @param {string} id The ID of entity to look for under 'otherNodes'. |
| * @returns {Object} The entity with ID requested, and undefined otherwise. |
| */ |
| function findEntityInOtherNodes(entities, id) { |
| for (const entity of entities) { |
| if (entity.otherNodes) { |
| let found = entity.otherNodes.find(e => e.id === id); |
| if (found) { |
| return found; |
| } |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Labels entity to not display in particular view mode. |
| * |
| * @param {Object} entity The entity to label. |
| * @param {string} viewMode The view mode to not display entity in. |
| */ |
| function hideEntityInView(entity, viewMode) { |
| if (entity.viewModes) { |
| entity.viewModes.delete(viewMode); |
| } |
| } |
| |
| /** |
| * Labels entity to display in particular view mode. |
| * |
| * @param {Object} entity The entity to label. |
| * @param {string} viewMode The view mode to display entity in. |
| */ |
| function displayEntityInView(entity, viewMode) { |
| if (!entity.viewModes) { |
| entity.viewModes = new Set([viewMode]); |
| } else { |
| entity.viewModes.add(viewMode); |
| } |
| } |
| |
| /** |
| * Hides entity if marked as moved in a particular view, and displays it otherwise. |
| * |
| * @param {Object} entity The entity to adjust visibility for. |
| * @param {string} viewMode The view mode to check visibility of a moved entity |
| */ |
| function adjustVisibilityOfMovedEntity(entity, viewMode) { |
| if (entity.movedInView && entity.movedInView.has(viewMode)) { |
| hideEntityInView(entity, viewMode); |
| } else { |
| displayEntityInView(entity, viewMode); |
| } |
| } |
| |
| /** |
| * Labels entity to highlight in particular view mode. |
| * |
| * @param {Object} entity The entity to label. |
| * @param {string} viewMode The view mode to highlight entity in. |
| */ |
| function highlightEntityInView(entity, viewMode) { |
| if (!entity.viewModesHighlight) { |
| entity.viewModesHighlight = new Set([viewMode]); |
| } else { |
| entity.viewModesHighlight.add(viewMode); |
| } |
| } |
| |
| /** |
| * Initializes entity tree with 'parent/child' view mode. This is a default view mode. |
| * |
| * @param {Array.<Object>} entities The entity tree to initialize with 'parent/child' view mode. |
| */ |
| function initParentChildView(entities) { |
| entities.forEach(entity => { |
| displayEntityInView(entity, VIEW_PARENT_CHILD); |
| }); |
| } |
| |
| /** |
| * Identifies new view modes based on relationships between entities. Updates {@link $scope.viewModes} set. |
| * |
| * @param {Array.<Object>} relationships The relationships of entities. |
| */ |
| 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 && relationship.name !== target.name) { |
| let uniqueRelationshipName = [relationship.name, target.name].sort().join(RELATIONSHIP_VIEW_DELIMITER); |
| viewModesDiscovered.add(uniqueRelationshipName); |
| } |
| }) |
| }); |
| |
| $scope.viewModes = viewModesDiscovered; // Refresh view modes |
| } |
| |
| /** |
| * Finds relationships in array of entities. |
| * |
| * @param {Array.<Object>} entities The array of entities to search relationships in. |
| * @returns {Array.<Object>} Relationships found in entities. |
| */ |
| 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 '/' |
| targets: Array.isArray(r.targets) ? r.targets : [] |
| } |
| relationships.push(relationship) |
| }); |
| } |
| }); |
| |
| return relationships; |
| } |
| }); |
| |
| $scope.$on('$destroy', ()=> { |
| observers.forEach((observer)=> { |
| observer.unsubscribe(); |
| }); |
| }); |
| } |
| } |
| |
| export function entityNodeDirective() { |
| return { |
| restrict: 'E', |
| template: entityNodeTemplate, |
| scope: { |
| entity: '<', |
| applicationId: '<', |
| viewMode: '<' |
| }, |
| link: link, |
| controller: ['$scope', '$state', '$stateParams', 'iconService', controller] |
| }; |
| |
| function link($scope) { |
| $scope.$on('notifyEntity', function (ev, data) { |
| if ($scope.entity.id) { |
| if (data.id !== $scope.entity.id) { |
| switch (data.message) { |
| case 'expandChildren': |
| $scope.isOpen = data.open; |
| $scope.isChildrenOpen = data.open; |
| break; |
| case 'openChildren' : |
| $scope.isOpen = data.open; |
| } |
| } |
| } |
| }); |
| } |
| |
| function controller ($scope, $state, $stateParams, iconService) { |
| $scope.isOpen = true; |
| if ($scope.entity.type) { |
| iconService.get($scope.entity, true).then(value => $scope.iconUrl = value); |
| } else { |
| // it's a member of a group; we could look up the target and take that icon, but for now, no icon |
| } |
| |
| if ($stateParams.entityId === $scope.entity.id) { |
| $scope.$emit('notifyEntity', { |
| message: 'expandChildren', |
| id: $scope.entity.id, |
| open: true |
| }); |
| } |
| |
| $scope.isSelected = function() { |
| return $stateParams.entityId === $scope.entity.id; |
| }; |
| |
| $scope.getHref = function() { |
| if ($state.current.name.startsWith(detailState.name)) { |
| return $state.href(activitiesState.name, { |
| applicationId: $scope.applicationId, |
| entityId: $scope.entity.id |
| }); |
| } |
| if ($state.current.name.startsWith(managementDetailState.name)) { |
| return $state.href(managementState.name, { |
| applicationId: $scope.applicationId, |
| entityId: $scope.entity.id |
| }); |
| } |
| if ($state.current.name.startsWith(inspectState.name)) { |
| return $state.href($state.current.name, { |
| applicationId: $scope.applicationId, |
| entityId: $scope.entity.id |
| }); |
| } |
| return $state.href(summaryState.name, { |
| applicationId: $scope.applicationId, |
| entityId: $scope.entity.id |
| }); |
| }; |
| |
| $scope.onToggle = function ($event) { |
| if ($event.shiftKey && $event.metaKey) { |
| $scope.isChildrenOpen = !$scope.isChildrenOpen; |
| $scope.$broadcast('notifyEntity', { |
| 'message': 'expandChildren', |
| 'id': $scope.entity.id, |
| 'open': $scope.isChildrenOpen |
| }); |
| } else { |
| $scope.isOpen = true; |
| $scope.isChildrenOpen = !$scope.isChildrenOpen; |
| $scope.$broadcast('notifyEntity', { |
| 'message': 'openChildren', |
| 'id': $scope.entityId, |
| 'open': $scope.isOpen |
| }); |
| } |
| }; |
| |
| /** |
| * @returns {boolean} True if to highlight entity in a current view, false otherwise. |
| */ |
| $scope.isHighlight = function() { |
| return $scope.entity.viewModesHighlight && $scope.entity.viewModesHighlight.has($scope.viewMode); |
| }; |
| |
| /** |
| * Counts amount of entity nodes that are expected to be displayed in the current view. |
| * |
| * @returns {number} Amount of entity nodes in the current view. |
| */ |
| $scope.nodesInCurrentView = () => { |
| let amount = 0; |
| if ($scope.entity.children) { |
| amount += $scope.entity.children.filter(entity => entity.viewModes.has($scope.viewMode)).length; |
| } |
| if ($scope.entity.members) { |
| amount += $scope.entity.members.filter(entity => entity.viewModes.has($scope.viewMode)).length; |
| } |
| if ($scope.entity.otherNodes) { |
| amount += $scope.entity.otherNodes.filter(entity => entity.viewModes.has($scope.viewMode)).length; |
| } |
| return amount; |
| } |
| } |
| } |