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/04/27 15:12:44 UTC

[brooklyn-ui] 02/15: add a new bit in the composer to show errors and warnings

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 b51d3dc1312754dd4b1e61178684f78a7ef0a492
Author: Alex Heneveld <al...@cloudsoftcorp.com>
AuthorDate: Wed Apr 21 09:30:09 2021 +0100

    add a new bit in the composer to show errors and warnings
    
    can navigate quickly to entities with problems;
    and have a place we can logically add "quick fixes"
---
 .../catalog-selector/catalog-selector.less         |   2 +-
 .../providers/blueprint-service.provider.js        |  57 +++++++-
 .../app/views/main/graphical/graphical.state.html  | 144 ++++++++++++++++++++-
 .../app/views/main/graphical/graphical.state.js    |  19 +++
 .../app/views/main/graphical/graphical.state.less  | 119 ++++++++++++++---
 .../blueprint-composer/app/views/main/main.less    |   6 +-
 6 files changed, 321 insertions(+), 26 deletions(-)

diff --git a/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.less b/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.less
index 1a2ac11..a1e5a61 100644
--- a/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.less
+++ b/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.less
@@ -22,7 +22,7 @@ catalog-selector {
   
   display: block;
   margin-top: 15px;
-  height: 100%;
+  //height: 100%; //makes it too big
   
   .grid {
     margin-bottom: 15px;
diff --git a/ui-modules/blueprint-composer/app/components/providers/blueprint-service.provider.js b/ui-modules/blueprint-composer/app/components/providers/blueprint-service.provider.js
index 19794ab..7f78da1 100644
--- a/ui-modules/blueprint-composer/app/components/providers/blueprint-service.provider.js
+++ b/ui-modules/blueprint-composer/app/components/providers/blueprint-service.provider.js
@@ -65,6 +65,7 @@ function BlueprintService($log, $q, $sce, paletteApi, iconGenerator, dslService)
         isReservedKey: isReservedKey,
         getIssues: getIssues,
         hasIssues: hasIssues,
+        getAllIssues: getAllIssues,
         populateEntityFromApi: populateEntityFromApiSuccess,
         populateLocationFromApi: populateLocationFromApiSuccess,
         addConfigKeyDefinition: addConfigKeyDefinition,
@@ -148,32 +149,74 @@ function BlueprintService($log, $q, $sce, paletteApi, iconGenerator, dslService)
         return RESERVED_KEYS.indexOf(key) > -1;
     }
 
-    function getIssues(entity = blueprint) {
+    function getIssues(entity = blueprint, nonRecursive) {
         let issues = [];
 
         if (entity.hasIssues()) {
-            issues = issues.concat(entity.issues.map((issue)=> {
+            issues = issues.concat(entity.issues.map((issue) => {
                 let newIssue = Issue.builder().message(issue.message).group(issue.group).ref(issue.ref).level(issue.level).build();
                 newIssue.entity = entity;
                 return newIssue;
             }));
         }
 
-        entity.policies.forEach((policy)=> {
+        entity.policies.forEach((policy) => {
             issues = issues.concat(getIssues(policy));
         });
 
-        entity.enrichers.forEach((enricher)=> {
+        entity.enrichers.forEach((enricher) => {
             issues = issues.concat(getIssues(enricher));
         });
 
-        entity.children.forEach((child)=> {
-            issues = issues.concat(getIssues(child));
-        });
+        if (!nonRecursive) {
+            entity.children.forEach((child) => {
+                issues = issues.concat(getIssues(child));
+            });
+        }
 
         return issues;
     }
 
+    function getAllIssues(entity = blueprint) {
+        return collectAllIssues({}, entity);
+    }
+
+    function collectAllIssues(result, entity) {
+        if (!result.entities) {
+            result.entities = {};
+            result.byEntity = {};
+            result.count = 0;
+            result.errors = { byEntity: {}, count: 0 }
+            result.warnings = { byEntity: {}, count: 0 }
+        }
+        if (result.entities[entity._id]) {
+            // already visited, some sort of reference; ignore
+        } else {
+            result.entities[entity._id] = entity;
+
+            let issues = getIssues(entity, true);
+            if (issues.length) {
+                result.byEntity[entity._id] = issues;
+                result.count += issues.length;
+
+                let errors = issues.filter(i => i.level.id == 'error');
+                if (errors.length) {
+                    result.errors.byEntity[entity._id] = errors;
+                    result.errors.count += errors.length;
+                }
+
+                let warnings = issues.filter(i => i.level.id != 'error');
+                if (warnings.length) {
+                    result.warnings.byEntity[entity._id] = warnings;
+                    result.warnings.count += warnings.length;
+                }
+            }
+
+            entity.children.forEach((child)=> collectAllIssues(result, child));
+        }
+        return result;
+    }
+
     function hasIssues() {
         return this.getIssues().length > 0;
     }
diff --git a/ui-modules/blueprint-composer/app/views/main/graphical/graphical.state.html b/ui-modules/blueprint-composer/app/views/main/graphical/graphical.state.html
index 711d4c1..b6cf40e 100644
--- a/ui-modules/blueprint-composer/app/views/main/graphical/graphical.state.html
+++ b/ui-modules/blueprint-composer/app/views/main/graphical/graphical.state.html
@@ -22,15 +22,157 @@
   <div class="layout">
     <div class="toolbar">
         <div class="list-group">
-            <a href class="list-group-item"
+            <a class="hand list-group-item"
                ng-repeat="section in vm.sections track by $index"
                ng-class="{'active': vm.selectedSection === section}"
                ng-click="vm.selectedSection = section">
                 <i class="fa fa-fw" ng-class="section.icon"></i>
             </a>
+
+            <div class="spacer"></div>
+            <a class="hand list-group-item errors-square"
+               title="Click to show/hide details of errors"
+               ng-class="{ errorsActive: errorsPane.level }"
+               ng-show="allIssues.errors.count"
+               ng-click="errorsPane.level = errorsPane.level ? null : 'summary'">
+                <i class="fa fa-fw fa-times-circle"></i> {{ allIssues.errors.count }}
+            </a>
+            <a class="hand list-group-item warning-square"
+               title="Click to show/hide details of warnings"
+               ng-class="{ errorsActive: errorsPane.level }"
+               ng-show="!allIssues.errors.count && allIssues.warnings.count"
+               ng-click="errorsPane.level = errorsPane.level ? null : 'summary'">
+                <i class="fa fa-fw fa-exclamation-triangle"></i> {{ allIssues.warnings.count }}
+            </a>
         </div>
     </div>
 
+      <div ng-if="errorsPane.level == 'summary'" class="errors-popout">
+          <div class="errors-body">
+
+              <a ng-click="errorsPane.level = null" class="hand errors-close"><i class="fa fa-fw fa-times"></i></a>
+
+            <div ng-if="allIssues.errors.count">
+                <a class="hand" ng-click="errorsPane.level = 'error-errors'">
+                {{ allIssues.errors.count }}
+                    <span ng-if="allIssues.errors.count == 1">error</span>
+                    <span ng-if="allIssues.errors.count != 1">errors</span>
+                </a>
+                in
+                <a class="hand" ng-click="errorsPane.level = 'error-entities'">
+                    {{ vm.size(allIssues.errors.byEntity) }}
+                    <span ng-if="vm.size(allIssues.errors.byEntity) == 1">entity</span>
+                    <span ng-if="vm.size(allIssues.errors.byEntity) != 1">entities</span>
+                </a>
+            </div>
+
+            <!-- TODO add links and panels for warnings -->
+            <div ng-if="allIssues.warnings.count">
+                {{ allIssues.warnings.count }}
+                    <span ng-if="allIssues.errors.count == 1">warning</span>
+                    <span ng-if="allIssues.errors.count != 1">warnings</span>
+                in {{ vm.size(allIssues.warnings.byEntity) }}
+                    <span ng-if="vm.size(allIssues.warnings.byEntity) == 1">entity</span>
+                    <span ng-if="vm.size(allIssues.warnings.byEntity) != 1">entities</span>
+            </div>
+
+            <div ng-if="!allIssues.errors.count && !allIssues.warnings.count">
+                No errors or warnings.
+            </div>
+
+          </div>
+      </div>
+
+      <div ng-if="errorsPane.level == 'error-entities'" class="errors-popout">
+          <div class="errorsHeader">
+              <a ng-click="errorsPane.level = null" class="hand errors-close"><i class="fa fa-fw fa-times"></i></a>
+
+              {{ vm.size(allIssues.errors.byEntity) }}
+              <span ng-if="vm.size(allIssues.errors.byEntity) == 1">entity</span>
+              <span ng-if="vm.size(allIssues.errors.byEntity) != 1">entities</span>
+
+              with
+
+              <a class="hand" ng-click="errorsPane.level = 'error-errors'">
+                  {{ allIssues.errors.count }}
+                  <span ng-if="allIssues.errors.count == 1">error</span>
+                  <span ng-if="allIssues.errors.count != 1">errors</span>
+              </a>
+          </div>
+
+          <div class="errors-body">
+            <div ng-repeat="(itemK,itemV) in allIssues.errors.byEntity" class="error-line">
+                <div class="error-line-marker">
+                    <i class="fa fa-fw fa-times-circle"></i>
+                </div>
+                <div class="error-line-text">
+                    {{ allIssues.entities[itemK].id ? allIssues.entities[itemK].id + ' ('+allIssues.entities[itemK].type+')'
+                        : allIssues.entities[itemK].type }}:
+                    {{ itemV.length }} {{ itemV.length==1 ? 'error' : 'errors' }}
+                </div>
+                <div class="error-line-action">
+                    <a class="hand" ui-sref="main.graphical.edit.entity({entityId: itemK})">
+                        <i class="fa fa-fw fa-external-link"></i>
+                    </a>
+                </div>
+            </div>
+          </div>
+      </div>
+
+      <div ng-if="errorsPane.level == 'error-errors'" class="errors-popout">
+          <div class="errorsHeader">
+              <a ng-click="errorsPane.level = null" class="hand errors-close"><i class="fa fa-fw fa-times"></i></a>
+
+              {{ allIssues.errors.count }}
+              <span ng-if="allIssues.errors.count == 1">error</span>
+              <span ng-if="allIssues.errors.count != 1">errors</span>
+              <span ng-show="allIssues.errors.count != vm.size(allIssues.errors.byMessage)"> ({{ vm.size(allIssues.errors.byMessage) }} unique)</span>
+
+              in
+
+              <a class="hand" ng-click="errorsPane.level = 'error-entities'">
+                  {{ vm.size(allIssues.errors.byEntity) }}
+                  <span ng-if="vm.size(allIssues.errors.byEntity) == 1">entity</span>
+                  <span ng-if="vm.size(allIssues.errors.byEntity) != 1">entities</span>
+              </a>
+          </div>
+
+          <div class="errors-body">
+              <div ng-repeat="(itemK,itemV) in allIssues.errors.byMessage" class="error-line">
+                  <div class="error-line-marker">
+                      <i class="fa fa-fw fa-times-circle"></i>
+                  </div>
+                  <div class="error-line-text">
+                      {{ itemK }}:
+                      <a class="hand" ng-click="errorsPane.focus = (errorsPane.focus == itemK ? null : itemK)">{{ itemV.length }} {{ itemV.length==1 ? 'error' : 'errors' }}</a>
+                      <div class="error-line-sub" ng-if="errorsPane.focus == itemK">
+                          <div ng-repeat="issue in itemV" class="error-line-sub-line">
+                              <div class="error-line-marker">
+                                  <i class="fa fa-fw fa-circle"></i>
+                              </div>
+                              <div class="error-line-text">
+                                  {{ issue.entity.id ? issue.entity.id + ' ('+issue.entity.type+')'
+                                      : issue.entity.type }}
+                              </div>
+                              <div class="error-line-action">
+                                  <a class="hand" ui-sref="main.graphical.edit.entity({entityId: issue.entity._id})">
+                                      <i class="fa fa-fw fa-external-link"></i>
+                                  </a>
+                              </div>
+                          </div>
+                      </div>
+                  </div>
+                  <!--
+                  <div class="error-line-action">
+                      <a class="hand" ui-sref="main.graphical.edit.entity({entityId: itemK})">
+                          <i class="fa fa-fw fa-external-link"></i>
+                      </a>
+                  </div>
+                  -->
+              </div>
+          </div>
+      </div>
+
     <designer on-selection-change="vm.onCanvasSelection"></designer>
   </div>
 
diff --git a/ui-modules/blueprint-composer/app/views/main/graphical/graphical.state.js b/ui-modules/blueprint-composer/app/views/main/graphical/graphical.state.js
index 0e7e0b2..143ebb2 100644
--- a/ui-modules/blueprint-composer/app/views/main/graphical/graphical.state.js
+++ b/ui-modules/blueprint-composer/app/views/main/graphical/graphical.state.js
@@ -41,10 +41,29 @@ function graphicalController($scope, $state, $filter, blueprintService, paletteS
     this.sections = paletteService.getSections();
     this.selectedSection = Object.values(this.sections).find(section => section.type === EntityFamily.ENTITY);
     $scope.paletteState = {};  // share state among all sections
+    $scope.errorsPane = { level: null };
+
+    $scope.blueprint = blueprintService.get();
+    $scope.$watch('blueprint', ()=> {
+        $scope.allIssues = blueprintService.getAllIssues();
+        $scope.allIssues.errors.byMessage = {};
+        Object.values($scope.allIssues.errors.byEntity).forEach(list => {
+           list.forEach(issue => {
+               $scope.allIssues.errors.byMessage[issue.group+":"+issue.ref] = $scope.allIssues.errors.byMessage[issue.group+" "+issue.ref] || [];
+               $scope.allIssues.errors.byMessage[issue.group+":"+issue.ref].push(issue);
+           });
+        });
+        //console.log("blueprint update, get all issues", $scope.allIssues);
+    }, true);
 
     this.onCanvasSelection = (item) => {
         $scope.canvasSelectedItem = item;
     }
+    this.size = (obj) => {
+        if (!obj) return 0;
+        return Object.keys(obj).length;
+    }
+
     this.getOnSelectText = (selectableType) => $scope.canvasSelectedItem ? "Add to " + $filter('entityName')($scope.canvasSelectedItem) : "Add to application";
     
     this.addSelectedTypeToTargetEntity = (selectedType, targetEntity) => {
diff --git a/ui-modules/blueprint-composer/app/views/main/graphical/graphical.state.less b/ui-modules/blueprint-composer/app/views/main/graphical/graphical.state.less
index 915d352..eeada0b 100644
--- a/ui-modules/blueprint-composer/app/views/main/graphical/graphical.state.less
+++ b/ui-modules/blueprint-composer/app/views/main/graphical/graphical.state.less
@@ -28,6 +28,7 @@
   .toolbar {
     position: relative;
     height: calc(~"100vh - 105px");
+    width: 48px;  // palette pane has left: 49px
     background-color: #fff;
     overflow-y: scroll;
     z-index: 40;
@@ -43,27 +44,113 @@
       background-color: @navbar-default-border;
     }
 
-    .list-group-item {
-      @active-border-width: 4px;
-      padding-top: 10px + @active-border-width;
-      padding-bottom: 9px + @active-border-width;
-      border-left: none;
-      border-radius: 0;
+    .list-group {
+      display: flex;
+      flex-direction: column;
+      height: 100%;
+      margin-bottom: 0;
+
+      .list-group-item {
+        @active-border-width: 4px;
+        padding-top: 10px + @active-border-width;
+        padding-bottom: 9px + @active-border-width;
+        border-left: none;
+        border-radius: 0;
+
+        &:first-child {
+          border-top: none;
+        }
+        &.active {
+          border-right: none;
+          border-color: @list-group-border;
+          border-bottom: @active-border-width solid @brand-primary;
+          background-color: #fff;
+          color: @list-group-link-color;
+          padding-bottom: 10px;
+        }
+      }
 
-      &:first-child {
-        border-top: none;
+
+      .spacer {
+        flex: 1 1 auto;
+      }
+
+      .errors-square, .warning-square {
+        padding-left: 0;
+        padding-right: 0;
+        text-align: center;
+        &.errorsActive {
+          background: @gray-lighter;
+        }
       }
-      &.active {
-        border-right: none;
-        border-color: @list-group-border;
-        border-bottom: @active-border-width solid @brand-primary;
-        background-color: #fff;
-        color: @list-group-link-color;
-        padding-bottom: 10px;
+      .errors-square {
+        color: @brand-danger;
+      }
+      .warning-square {
+        color: @brand-warning;
+      }
+    }
+  }
+  .errors-popout {
+    position: absolute;
+    left: 49px;
+    width: 440px;
+    bottom: 0;
+    z-index: 60;
+    border-top: 1px solid @gray-lighter;
+    background: @gray-lightest;
+
+    .errors-close {
+      float: right;
+    }
+
+    .errors-body {
+      padding: 8px;
+    }
+
+    .errorsHeader {
+      width: 100%;
+      background: @gray-lighter;
+      border-top: 1px solid @gray-light;
+      border-bottom: 1px solid @gray-light;
+      padding: 8px;
+      margin-bottom: 8px;
+    }
+    max-height: 600px;
+    overflow-y: scroll;
+  }
+  .error-line {
+    display: flex;
+    justify-self: stretch;
+    padding-bottom: 8px;
+    .error-line-marker, .error-line-action {
+      flex: 0 0 auto;
+      margin-top: 4px;
+    }
+    .error-line-text {
+      flex: 1 1 auto;
+    }
+    .error-line-marker {
+      color: @brand-danger;
+      padding-right: 0.5ex;
+    }
+    .error-line-action {
+      padding-left: 0.5ex;
+    }
+    .error-line-sub-line {
+      display: flex;
+      justify-self: stretch;
+      margin-bottom: 4px;
+      margin-top: 4px;
+      .error-line-marker {
+        color: inherit;
+        font-size: 50%;
+        padding-right: 1em;
+        margin-top: 6px;
       }
     }
   }
-  
+
   #palette-controls-button {
     margin-right: -2px;  // not sure why the default -1px doesn't work to merge the borders?
   }
diff --git a/ui-modules/blueprint-composer/app/views/main/main.less b/ui-modules/blueprint-composer/app/views/main/main.less
index 06e390b..8e38515 100644
--- a/ui-modules/blueprint-composer/app/views/main/main.less
+++ b/ui-modules/blueprint-composer/app/views/main/main.less
@@ -111,4 +111,8 @@
 
 .flex {
     display: flex;
-}
\ No newline at end of file
+}
+
+.hand {
+  cursor: pointer;
+}