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:47 UTC

[brooklyn-ui] 05/15: add quick fix support

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 b1c9f0766482d83e0f0739af38bddb5db33b945d
Author: Alex Heneveld <al...@cloudsoftcorp.com>
AuthorDate: Thu Apr 22 22:12:51 2021 +0100

    add quick fix support
    
    in global view only; on entity coming soon
---
 docs/basic-with-constraint.bom                     |   6 ++
 .../providers/blueprint-service.provider.js        |  11 +-
 .../app/components/quick-fix/quick-fix.html        |  21 ++++
 .../app/components/quick-fix/quick-fix.js          | 120 +++++++++++++++++++++
 .../app/components/quick-fix/quick-fix.less        |  21 ++++
 .../app/components/util/model/issue.model.js       |  14 +++
 ui-modules/blueprint-composer/app/index.js         |   3 +-
 .../app/views/main/graphical/graphical.state.html  |  61 ++++++++---
 .../app/views/main/graphical/graphical.state.js    |  25 +++--
 .../app/views/main/graphical/graphical.state.less  |   4 +
 10 files changed, 259 insertions(+), 27 deletions(-)

diff --git a/docs/basic-with-constraint.bom b/docs/basic-with-constraint.bom
index 08ed2c6..b3f440a 100644
--- a/docs/basic-with-constraint.bom
+++ b/docs/basic-with-constraint.bom
@@ -27,4 +27,10 @@ brooklyn.catalog:
     - name: zip_code
       constraints:
       - forbiddenIf: post_code
+  tags:
+  - ui-composer-hints:
+      config-quick-fixes:
+      - key: zip_code
+        fix: clear_config
+        message-regex: .*cannot both be set.*
 
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 1639577..c98cf69 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,
+        clearAllIssues: clearAllIssues,
         getAllIssues: getAllIssues,
         populateEntityFromApi: populateEntityFromApiSuccess,
         populateLocationFromApi: populateLocationFromApiSuccess,
@@ -177,6 +178,12 @@ function BlueprintService($log, $q, $sce, paletteApi, iconGenerator, dslService)
         return issues;
     }
 
+    // typically followed by a call to refresh
+    function clearAllIssues(entity = blueprint) {
+        entity.resetIssues();
+        entity.children.forEach(clearAllIssues);
+    }
+
     function getAllIssues(entity = blueprint) {
         return collectAllIssues({}, entity);
     }
@@ -559,9 +566,9 @@ function BlueprintService($log, $q, $sce, paletteApi, iconGenerator, dslService)
             return promises;
         }, [])).then(results => {
             results.forEach(result => {
-                entity.clearIssues({ref: result.key});
+                entity.clearIssues({ref: result.key, phase: 'relationship'});
                 result.issues.forEach(issue => {
-                    entity.addIssue(Issue.builder().group('config').ref(result.key).message($sce.trustAsHtml(issue)).build());
+                    entity.addIssue(Issue.builder().group('config').phase('relationship').ref(result.key).message($sce.trustAsHtml(issue)).build());
                 });
             })
         });
diff --git a/ui-modules/blueprint-composer/app/components/quick-fix/quick-fix.html b/ui-modules/blueprint-composer/app/components/quick-fix/quick-fix.html
new file mode 100644
index 0000000..e9bdb2e
--- /dev/null
+++ b/ui-modules/blueprint-composer/app/components/quick-fix/quick-fix.html
@@ -0,0 +1,21 @@
+<!--
+  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.
+-->
+<span class="quick-fix">
+    bob {{ foo }}.
+</span>
diff --git a/ui-modules/blueprint-composer/app/components/quick-fix/quick-fix.js b/ui-modules/blueprint-composer/app/components/quick-fix/quick-fix.js
new file mode 100644
index 0000000..184de2c
--- /dev/null
+++ b/ui-modules/blueprint-composer/app/components/quick-fix/quick-fix.js
@@ -0,0 +1,120 @@
+/*
+ * 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 template from './quick-fix.html';
+
+const MODULE_NAME = 'brooklyn.components.quick-fix.quick-fix';
+
+angular.module(MODULE_NAME, [])
+    .directive('quickFix', ['$rootScope', quickFixDirective]);
+
+export default MODULE_NAME;
+
+export function quickFixDirective($rootScope) {
+    return {
+        restrict: 'E',
+        template: template,
+        scope: {
+            issues: '=',
+        },
+        link: link,
+    };
+
+    function link(scope, element, attrs, specEditor) {
+        console.log("quick-fix", attrs);
+        scope.foo = "hello";
+    }
+}
+
+export function computeQuickFixes(allIssues) {
+    if (!allIssues) allIssues = {};
+    if (!allIssues.errors) allIssues.errors = {};
+
+    allIssues.errors.byMessage = {};
+    Object.values(allIssues.errors.byEntity).forEach(list => {
+        list.forEach(issue => {
+            // TODO key should be a tuple of group, ref, message
+            let key = issue.group+":"+issue.ref+":"+issue.message;
+            let v = allIssues.errors.byMessage[key];
+            if (!v) {
+                v = allIssues.errors.byMessage[key] = {
+                    group: issue.group,
+                    ref: issue.ref,
+                    message: issue.message,
+                    issues: [],
+                    quickFixes: {},
+                };
+            }
+
+            let issueO = {
+                issue,
+                //quickFixes: {},
+            }
+            v.issues.push(issueO);
+
+            let qfs = getQuickFixHintsForIssue(issue);
+            (qfs || []).forEach(qf => {
+                let qfi = getQuickFixProposer(qf['fix']);
+                if (!qfi) {
+                    console.log("Skipping unknown quick fix", qf);
+                } else {
+                    qfi.propose(issue, v.quickFixes);
+                    // we could offer the fix per-issue, but no need as they can get that by navigating to the entity
+                    //qfi.propose(issue, issueO.quickFixes);
+                }
+            });
+        });
+    });
+    return allIssues;
+}
+
+const QUICK_FIX_PROPOSERS = {
+    clear_config: {
+        // the propose function updates the proposals object
+        propose: (issue, proposals) => {
+            if (!issue.ref) return;
+
+            if (!proposals) proposals = {};
+            if (!proposals.clear_config) {
+                proposals.clear_config = {
+                    text: "Remove the current value (clear config \""+issue.ref+"\")",
+                    apply: (issue) => issue.entity.removeConfig(issue.ref),
+                    issues: [],
+                };
+            }
+            proposals.clear_config.issues.push(issue);
+        },
+    }
+};
+
+export function getQuickFixProposer(type) {
+    return QUICK_FIX_PROPOSERS[type];
+}
+
+export function getQuickFixHintsForIssue(issue) {
+    if (issue.group === 'config') {
+        let hints = (issue.entity.miscData.get('ui-composer-hints') || {})['config-quick-fixes'] || [];
+        hints = hints.filter(h => h.key === issue.ref);
+        if (!hints.length) return null;
+        hints = hints.filter(h => !h['message-regex'] || new RegExp(h['message-regex']).test(issue.message));
+        return hints;
+    }
+    return null;
+}
\ No newline at end of file
diff --git a/ui-modules/blueprint-composer/app/components/quick-fix/quick-fix.less b/ui-modules/blueprint-composer/app/components/quick-fix/quick-fix.less
new file mode 100644
index 0000000..77412b2
--- /dev/null
+++ b/ui-modules/blueprint-composer/app/components/quick-fix/quick-fix.less
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+ 
+quick-fix, .quick-fix {
+}
diff --git a/ui-modules/blueprint-composer/app/components/util/model/issue.model.js b/ui-modules/blueprint-composer/app/components/util/model/issue.model.js
index badfab5..f00bd7d 100644
--- a/ui-modules/blueprint-composer/app/components/util/model/issue.model.js
+++ b/ui-modules/blueprint-composer/app/components/util/model/issue.model.js
@@ -18,6 +18,7 @@
  */
 const MESSAGE = new WeakMap();
 const GROUP = new WeakMap();
+const PHASE = new WeakMap();
 const REF = new WeakMap();
 const LEVEL = new WeakMap();
 
@@ -36,6 +37,7 @@ export class Issue {
     constructor() {
         MESSAGE.set(this, '');
         GROUP.set(this, '');
+        PHASE.set(this, '');
         REF.set(this, '');
         LEVEL.set(this, ISSUE_LEVEL.ERROR);
     }
@@ -56,6 +58,14 @@ export class Issue {
         return GROUP.get(this);
     }
 
+    set phase(group) {
+        PHASE.set(this, group);
+    }
+
+    get phase() {
+        return PHASE.get(this);
+    }
+
     set ref(ref) {
         REF.set(this, ref);
     }
@@ -92,6 +102,10 @@ class Builder {
         return this;
     }
 
+    phase(phase) {
+        this.issue.phase = phase;
+    }
+
     ref(ref) {
         this.issue.ref = ref;
         return this;
diff --git a/ui-modules/blueprint-composer/app/index.js b/ui-modules/blueprint-composer/app/index.js
index 7504627..944fbad 100755
--- a/ui-modules/blueprint-composer/app/index.js
+++ b/ui-modules/blueprint-composer/app/index.js
@@ -59,6 +59,7 @@ import paletteDragAndDropService from "./components/providers/palette-dragndrop.
 import actionService from "./components/providers/action-service.provider";
 import tabService from "./components/providers/tab-service.provider";
 import composerOverrides from "./components/providers/composer-overrides.provider";
+import quickFix from "./components/quick-fix/quick-fix";
 import {mainState} from "./views/main/main.controller";
 import {yamlAutodetectState, yamlCampState, yamlState} from "./views/main/yaml/yaml.state";
 import {graphicalState} from "./views/main/graphical/graphical.state";
@@ -82,7 +83,7 @@ angular.module('brooklynBlueprintComposer', [ngAnimate, ngResource, ngCookies, n
     brServerStatus, brAutoFocus, brIconGenerator, brInterstitialSpinner, brooklynModuleLinks, brooklynUserManagement,
     brYamlEditor, brUtils, brSpecEditor, brooklynCatalogSaver, brooklynApi, bottomSheet, stackViewer, brDragndrop, mdHelper,
     customActionDirective, customConfigSuggestionDropdown, paletteApiProvider, paletteServiceProvider, blueprintLoaderApiProvider,
-    breadcrumbs, catalogSelector, designer, objectCache, entityFilters, locationFilter, actionService, tabService, composerOverrides, blueprintService,
+    breadcrumbs, catalogSelector, designer, objectCache, entityFilters, locationFilter, actionService, tabService, composerOverrides, quickFix, blueprintService,
     dslService, paletteDragAndDropService, recentlyUsedService, scriptTagDecorator, brandAngularJs])
     .filter('dslParamLabel', ['$filter', dslParamLabelFilter])
     .config(['$urlRouterProvider', '$stateProvider', '$logProvider', '$compileProvider', applicationConfig])
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 b6cf40e..dba01b1 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
@@ -87,9 +87,9 @@
           <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>
+              <b>{{ 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></b>
 
               with
 
@@ -123,9 +123,9 @@
           <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>
+              <b>{{ allIssues.errors.count }}
+                  <span ng-if="allIssues.errors.count == 1">error</span>
+                  <span ng-if="allIssues.errors.count != 1">errors</span></b>
               <span ng-show="allIssues.errors.count != vm.size(allIssues.errors.byMessage)"> ({{ vm.size(allIssues.errors.byMessage) }} unique)</span>
 
               in
@@ -143,24 +143,59 @@
                       <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">
+                      <span>
+                          <span ng-show="vm.messageNeedsPrefix(itemV)">
+                              {{ itemV.group }} {{ itemV.ref }}<span ng-show="itemV.message">:</span>
+                          </span>
+                          <span ng-show="itemV.message">
+                              <ng-bind-html ng-bind-html="itemV.message"></ng-bind-html>
+                          </span>
+                      </span>
+
+                      (<a class="hand" ng-click="errorsPane.focus = (errorsPane.focus == 'errors:'+itemK ? null : 'errors:'+itemK)"
+                            ng-class="{active: errorsPane.focus == 'errors:'+itemK }">{{
+                           itemV.issues.length }}
+                        {{ itemV.issues.length==1 ? 'entity' : 'entities'
+                      }}</a><span ng-if="vm.size(itemV.quickFixes)">;
+                          <a class="hand" ng-click="errorsPane.focus = (errorsPane.focus == 'fixes:'+itemK ? null : 'fixes:'+itemK)"
+                                ng-class="{active: errorsPane.focus == 'fixes:'+itemK }">
+                            {{ vm.size(itemV.quickFixes) }} quick fix<span ng-if="vm.size(itemV.quickFixes) != 1">es</span> available</a></span>)
+
+                      <div class="error-line-sub" ng-if="errorsPane.focus == 'errors:'+itemK">
+                          <div ng-repeat="issue in itemV.issues" 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 }}
+                                  {{ issue.issue.entity.id ? issue.issue.entity.id + ' ('+issue.issue.entity.type+')'
+                                      : issue.issue.entity.type }}
+                                  <!-- could offer the issue-specific quick fixes; but clearer to navigate to entity and do there -->
                               </div>
                               <div class="error-line-action">
-                                  <a class="hand" ui-sref="main.graphical.edit.entity({entityId: issue.entity._id})">
+                                  <a class="hand" ui-sref="main.graphical.edit.entity({entityId: issue.issue.entity._id})">
                                       <i class="fa fa-fw fa-external-link"></i>
                                   </a>
                               </div>
                           </div>
                       </div>
+
+                      <div class="error-line-sub" ng-if="errorsPane.focus == 'fixes:'+itemK">
+                          <div ng-repeat="fix in itemV.quickFixes" class="error-line-sub-line">
+                              <div class="error-line-marker">
+                                  <i class="fa fa-fw fa-magic"></i>
+                              </div>
+                              <div class="error-line-text">
+                                  {{ fix.text }}
+                                  <a class="hand btn btn-xs btn-primary" style="float: right;" ng-click="vm.applyQuickFix(fix)"
+                                  >Apply
+                                      ({{ vm.size(fix.issues) }}
+                                      <span ng-if="vm.size(fix.issues) == 1">entity</span
+                                      ><span ng-if="vm.size(fix.issues) != 1">entities</span
+                                      >)
+                                  </a>
+                              </div>
+                          </div>
+                      </div>
                   </div>
                   <!--
                   <div class="error-line-action">
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 143ebb2..16cf7f8 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
@@ -19,6 +19,7 @@
 import {graphicalEditEntityState} from './edit/entity/edit.entity.controller';
 import {graphicalEditPolicyState} from './edit/policy/edit.policy.controller';
 import {graphicalEditEnricherState} from './edit/enricher/edit.enricher.controller';
+import {computeQuickFixes} from '../../../components/quick-fix/quick-fix';
 import {Entity, EntityFamily} from '../../../components/util/model/entity.model';
 import template from './graphical.state.html';
 
@@ -36,6 +37,7 @@ export const graphicalState = {
 };
 
 function graphicalController($scope, $state, $filter, blueprintService, paletteService) {
+    let vm = this;
     this.EntityFamily = EntityFamily;
 
     this.sections = paletteService.getSections();
@@ -44,18 +46,11 @@ function graphicalController($scope, $state, $filter, blueprintService, paletteS
     $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);
+    $scope.$watch('blueprint', () => vm.computeIssues(), true);
 
+    this.computeIssues = () => {
+        $scope.allIssues = computeQuickFixes(blueprintService.getAllIssues());
+    }
     this.onCanvasSelection = (item) => {
         $scope.canvasSelectedItem = item;
     }
@@ -64,6 +59,7 @@ function graphicalController($scope, $state, $filter, blueprintService, paletteS
         return Object.keys(obj).length;
     }
 
+    this.messageNeedsPrefix = (itemV) => !itemV.message || (""+itemV.message).indexOf(itemV.ref)<0;
     this.getOnSelectText = (selectableType) => $scope.canvasSelectedItem ? "Add to " + $filter('entityName')($scope.canvasSelectedItem) : "Add to application";
     
     this.addSelectedTypeToTargetEntity = (selectedType, targetEntity) => {
@@ -96,4 +92,11 @@ function graphicalController($scope, $state, $filter, blueprintService, paletteS
             $state.go(graphicalEditEntityState, {entityId: targetEntity._id});
         }
     };
+
+    this.applyQuickFix = (fix) => {
+        fix.issues.forEach(issue => fix.apply(issue));
+        // recompute errors
+        blueprintService.clearAllIssues();
+        blueprintService.refreshBlueprintMetadata()
+    }
 }
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 eeada0b..3cb5fc1 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
@@ -118,6 +118,10 @@
     }
     max-height: 600px;
     overflow-y: scroll;
+
+    a.active {
+      font-weight: bold;
+    }
   }
   .error-line {
     display: flex;