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 <[email protected]> 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;
