ui-modules/blueprint-composer/app/components/quick-fix/quick-fix.js (324 lines of code) (raw):

/* * 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'; const MODULE_NAME = 'brooklyn.components.quick-fix.quick-fix'; angular.module(MODULE_NAME, []); export default MODULE_NAME; export function computeQuickFixes(blueprintService, allIssues) { if (!allIssues) allIssues = blueprintService.getAllIssues(); if (!allIssues) allIssues = {}; if (!allIssues.errors) allIssues.errors = {}; allIssues.errors.byMessage = {}; Object.values(allIssues.errors.byEntity).forEach(list => { list.forEach(issue => { 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); computeQuickFixesForIssue(issue, issue.entity, blueprintService, v.quickFixes) }); }); allIssues.warnings.byMessage = {}; Object.values(allIssues.warnings.byEntity).forEach(list => { list.forEach(issue => { let key = issue.group+":"+issue.ref+":"+issue.message; let v = allIssues.warnings.byMessage[key]; if (!v) { v = allIssues.warnings.byMessage[key] = { group: issue.group, ref: issue.ref, message: issue.message, issues: [], quickFixes: {}, }; } let issueO = { issue, //quickFixes: {}, } v.issues.push(issueO); computeQuickFixesForIssue(issue, issue.entity, blueprintService, v.quickFixes) }); }); return allIssues; } export function computeQuickFixesForIssue(issue, entity, blueprintService, proposalHolder) { let qfs = getQuickFixHintsForIssue(issue, entity); (qfs || []).forEach(qf => { let qfi = getQuickFixProposer(qf['fix']); if (!qfi) { console.log("Skipping unknown quick fix", qf); } else { qfi.propose(qf, issue, entity, blueprintService, proposalHolder); // 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); // issueO from previous method } }); } const QUICK_FIX_PROPOSERS = { clear_config: { // the propose function updates the proposals object propose: (qfdef, issue, entity, blueprintService, proposals) => { if (!issue.ref) return; if (!proposals) proposals = {}; if (!proposals.clear_config) { proposals.clear_config = { text: "Remove value", tooltip: "This will clear the value currently set for config \""+issue.ref+"\".", apply: (issue, entity) => (entity || issue.entity).removeConfig(issue.ref), issues: [], }; } proposals.clear_config.issues.push(issue); }, }, explicit_config: { propose: (qfdef, issue, entity, blueprintService, proposals) => { if (!issue.ref) { return; } if (!proposals) { proposals = {}; } // refer directly to the ancestor, and refer to it as scope root (if root) or using id // no option to refer to parent because that gets weird if things are rearranged // (application root is usually not rearranged) if (!proposals.explicit_config) { let parent = (entity || issue.entity).parent; let entityToReference = parent; while (entityToReference && !entityToReference.config.has(issue.ref)) { entityToReference = entityToReference.parent; } if (!entityToReference) entityToReference = parent; const isParent = entityToReference === parent; const isScopeRoot = blueprintService.get() === entityToReference; const referrent = isParent ? 'parent' : isScopeRoot ? 'root' : 'ancestor'; proposals.explicit_config = { text: 'Set explicit config from '+referrent, tooltip: `This will set the config "${issue.ref}" to refer explicitly to the value set at the `+referrent, apply: (issue, entity) => { let scopeRootOrComponent; if (isScopeRoot) scopeRootOrComponent = 'scopeRoot()'; else { if (!entityToReference.id) { // we could try to make a good uid from the name or type, but for now just make random to ensure it isn't null entityToReference.id = entityToReference._id; } scopeRootOrComponent = `component("${entityToReference.id}")`; } (entity || issue.entity).addConfig(issue.ref, `$brooklyn:${scopeRootOrComponent}.config("${issue.ref}")`); }, issues: [] }; } proposals.explicit_config.issues.push(issue); } }, set_from_key: { propose: proposeSetFromKey() // - key: post_code // fix: set_from_key // message-regex: required // // source-key: postal_code # one of these is required // source-key-regex: ^postal_code($|_.*) // // source-hierarchy: root # optional, root|anywhere|ancestors -- default is root if no source-types given, anywhere if source-types are given // source-types: [ org.apache.brooklyn.api.entity.Application ] # types to filter by // // source-key-createable: true # whether a parameter can be created or a key/param must already exist there (default the latter, ie false) // source-key-parameter-definition: # if createable and did not exist, extra things to add to definition // constraints: // - required }, set_from_template: { propose: proposeSetFromTemplate(), // - key: post_code // fix: set_from_template // message-regex: required // template: ${application}-${entity} # required, the template, supporting vars application, application.id, entity, entity.name, entity._id // preview: "Set post_code '${application}_<entity_name_or_id>'" # optional, summary for button, grouping fixes, and filtering (template rules applied to this, skipped if not applicable) // sanitize: _ # optional, sanitize as specified, eg _ or - or '.' to replace non-alphanumeric chars with that } }; function proposeSetFromKey() { return function (qfdef, issue, entity, blueprintService, proposals) { if (!issue.ref) return; let ckey_exact = qfdef['source-key']; let ckey_regex = qfdef['source-key-regex']; if (!ckey_exact && !ckey_regex) { console.warn("Missing at least one of 'source-key' or 'source-key-regex' on hint", qfdef); return; } if (!proposals) proposals = {}; let createable = qfdef['source-key-createable']; // TODO make default id contain type name // TODO show default id if no id present // TODO if id is changed, update all refs // TODO allow graphically selectable let considerNode = (sourceNode) => { if (qfdef['source-types']) { if (!qfdef['source-types'].includes(sourceNode.entity.type)) { // wrong type; check traits (supertypes) if ((sourceNode.entity.miscData.get("traits") || []).find(t => qfdef['source-types'].includes(t))) { // there was a super-type which matched; we're okay } else { // also no supertype matches the specified source-types, so don't make a proposal for this node return; } } } let contenders = {}; if (ckey_exact) { let exactKey = sourceNode.entity.config[ckey_exact] || (sourceNode.entity.miscData.get("config") || []).find(c => c.name === ckey_exact); // don't think we need to check params -- sourceNode.entity.getParameterNamed(ckey) -- as they should be in config if (exactKey) contenders[ckey_exact] = true; } let create = !Object.keys(contenders).length && createable && ckey_exact; if (create) { contenders[ckey_exact] = true; } if (ckey_regex) { let r = new RegExp(ckey_regex); Object.keys(sourceNode.entity.config).forEach(k => { if (r.test(k)) contenders[k] = true; }); (sourceNode.entity.miscData.get("config") || []).forEach(c => { if (r.test(c.name)) contenders[c.name] = true; }); } if (!Object.keys(contenders).length) { // no proposal available (cannot create) return; } if (!sourceNode.entity.parent) { sourceNode.id = sourceNode.id || 'root'; sourceNode.name = sourceNode.name || sourceNode.entity.name || 'the application root node'; } sourceNode.id = sourceNode.id || sourceNode.entity.id || sourceNode.entity._id; sourceNode.name = sourceNode.name || sourceNode.entity.name || ((sourceNode.entity.type || "Unnamed item") + " " + "(" + (sourceNode.entity.id || sourceNode.entity._id) + ")"); Object.keys(contenders).forEach(ckey => { if (sourceNode.entity._id === entity._id && ckey === issue.ref) { // skip proposal for recursive definition return; } let pkey = 'set_from_key_' + sourceNode.id + '_' + ckey; if (!proposals[pkey]) { if (create) { proposals[pkey] = { text: "Set from new parameter '" + ckey + "' on " + sourceNode.name, tooltip: "This will fix the error by setting the value here equal to the value of a new parameter '" + ckey + "' created on " + sourceNode.name + ". The value of that parameter may need to be set in order to deploy this.", }; } else { proposals[pkey] = { text: "Set from '" + ckey + "' on " + sourceNode.name, tooltip: "This will fix the error by setting the value here equal to the value of " + sourceNode.target_mode + " '" + ckey + "' on " + sourceNode.name, }; } Object.assign(proposals[pkey], { issues: [], apply: (issue, entity) => { if (create) { // check again so we only create once let hasParam = sourceNode.entity.getParameterNamed(ckey); if (!hasParam) { sourceNode.entity.addParameterDefinition(Object.assign( {name: ckey,}, qfdef['source-key-parameter-definition'], )); } } blueprintService.populateId(sourceNode.entity); entity = (entity || issue.entity); entity.addConfig(issue.ref, '$brooklyn:component("' + sourceNode.entity.id + '").config("' + ckey + '")'); } }); } if (proposals[pkey]) { proposals[pkey].issues.push(issue); } }); } if (qfdef['source-hierarchy']=='root' || (!qfdef['source-hierarchy'] && !qfdef['source-types'])) { considerNode({ entity: entity.getApplication() }); } else if (qfdef['source-hierarchy']=='anywhere' || (!qfdef['source-hierarchy'] && qfdef['source-types'])) { entity.getApplication().visitWithDescendants(entity => considerNode({ entity })); } else if (qfdef['source-hierarchy']=='ancestors') { entity.visitWithAncestors(entity => considerNode({ entity })); } else { console.warn("Unsupported source-hierarchy in quick-fix", qfdef); } }; } function proposeSetFromTemplate() { return function (qfdef, issue, entity, blueprintService, proposals) { if (!issue.ref) return; let template = qfdef['template']; if (!template) { console.warn("Missing 'template' on hint", qfdef); return; } let sanitize = qfdef['sanitize']; let sanitizeFn = s => { if (!sanitize) return s; return s.replace(/\W+/g, sanitize); } if (!proposals) proposals = {}; function replace(s, keyword, fn, skipSanitize) { if (!s) return null; let p = "${"+keyword+"}"; if (s.includes(p)) { let v = fn(); if (v) { if (!skipSanitize) v = sanitizeFn(v); do { s = s.replace(p, v); } while (s.includes(p)); } else { return null; } } return s; } function replaceTemplate(result, s, x, idFn, isPreview) { let idLastFn = () => { let last = x.id; let takeLast = last || !isPreview; last = last || idFn(s,x); if (takeLast) last = last.replace(/^.*\W+(\w+\W*)/, '$1'); return last; }; result = replace(result, s, () => x.name || idLastFn(), isPreview && !x.name && !x.id); result = replace(result, s+".name", () => x.name); result = replace(result, s+".nameOrType", () => x.name || x.miscData.get('typeName') || x.type); result = replace(result, s+".typeName", () => x.miscData.get('typeName') || x.type); result = replace(result, s+".type", () => x.type || x.miscData.get('typeName')); result = replace(result, s+".id", () => x.id || idFn(s,x)); // takes the last word of the ID result = replace(result, s+".idLast", idLastFn, isPreview); result = replace(result, s+"._id", () => x._id); return result; } let idFnForPreview = (s,x) => "<"+s+" ID, changed from "+x._id+">"; let preview = qfdef['preview'] || "Set '"+template+"'"; preview = replaceTemplate(preview, "entity", entity, idFnForPreview, true); preview = replaceTemplate(preview, "application", entity.getApplication(), idFnForPreview, true); if (preview) { let pkey = 'set_from_template_' + preview; if (!proposals[pkey]) { proposals[pkey] = { text: preview, tooltip: "This will fix the error by setting the value here based on a template." + (sanitize ? " The result will be sanitized using '" + sanitize + "'." : ""), }; Object.assign(proposals[pkey], { issues: [], apply: (issue, entity) => { entity = (entity || issue.entity); let result = template; let idFnForActual = (s, x) => { blueprintService.populateId(x); return x.id; }; result = replaceTemplate(result, "entity", entity, idFnForActual); result = replaceTemplate(result, "application", entity.getApplication(), idFnForActual); if (!result) { console.warn("Could not apply quick fix: template '"+template+"' not valid at entity", entity); } else { entity.addConfig(issue.ref, result); } } }); } if (proposals[pkey]) { proposals[pkey].issues.push(issue); } } }; } export function getQuickFixProposer(type) { return QUICK_FIX_PROPOSERS[type]; } export function getQuickFixHintsForIssue(issue, entity) { if (issue.group === 'config') { let hints = (entity.miscData.get('ui-composer-hints') || {})['config-quick-fixes'] || []; hints = hints.filter(h => new RegExp(h.key).test(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; }