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>