| /* |
| * 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 angularSanitize from 'angular-sanitize'; |
| import {Dsl, DslParser, KIND} from '../util/model/dsl.model'; |
| import template from './dsl-editor.template.html'; |
| import brAutoFocus from 'brooklyn-ui-utils/autofocus/autofocus'; |
| import brUtils from 'brooklyn-ui-utils/utils/general'; |
| |
| const MODULE_NAME = 'brooklyn.components.dsl-editor'; |
| const TEMPLATE_URL = 'blueprint-composer/component/dsl-editor/index.html'; |
| const DSL_KINDS = { |
| ALL: { |
| id: 'all', |
| label: 'Config, sensor or entity', |
| }, |
| CONFIG: { |
| id: 'config', |
| label: 'Config' |
| }, |
| SENSOR: { |
| id: 'sensor', |
| label: 'Sensor' |
| }, |
| ENTITY: { |
| id: 'entity', |
| label: 'Entity' |
| }, |
| FORMAT_STRING: { |
| id: 'formatString', |
| label: 'Formatted string' |
| }, |
| }; |
| |
| angular.module(MODULE_NAME, [angularSanitize, brAutoFocus, brUtils]) |
| .directive('dslEditor', ['$rootScope', '$filter', '$log', 'brUtilsGeneral', 'blueprintService', dslEditorDirective]) |
| .run(['$templateCache', templateCache]); |
| |
| export default MODULE_NAME; |
| |
| export function dslEditorDirective($rootScope, $filter, $log, brUtilsGeneral, blueprintService) { |
| return { |
| restrict: 'E', |
| templateUrl: function (tElement, tAttrs) { |
| return tAttrs.templateUrl || TEMPLATE_URL; |
| }, |
| scope: { |
| definition: '=', |
| entity: '=', |
| dsl: '=' |
| }, |
| link: link |
| }; |
| |
| function link(scope) { |
| scope.DSL_KINDS = DSL_KINDS; |
| |
| scope.kinds = Object.values(DSL_KINDS); |
| |
| scope.filters = [{ |
| id: 'blueprint', |
| label: 'Anywhere on the blueprint', |
| scope: 'Global' |
| }].concat(getEntityItems(blueprintService.get()).map(item => { |
| let attrs = []; |
| if (item.entity === scope.entity) { |
| attrs.push('this'); |
| } |
| if (!item.entity.hasParent()) { |
| attrs.push('root'); |
| } |
| if (scope.entity.parent === item.entity) { |
| attrs.push('parent'); |
| } |
| |
| let name = item.name; |
| if (attrs.length > 0) { |
| name += ` (${attrs.join(', ')})`; |
| } |
| |
| return { |
| id: item.id, |
| label: name, |
| scope: 'On specific entity' |
| }; |
| })); |
| |
| scope.orders = [{ |
| label: 'name', |
| property: 'name' |
| }, { |
| label: 'entity', |
| property: 'entity.type' |
| }]; |
| |
| scope.items = [].concat( |
| getConfigItems(blueprintService.get(), scope.definition), |
| getSensorItems(blueprintService.get()), |
| getEntityItems(blueprintService.get(), scope.definition.type) |
| ); |
| |
| scope.state = { |
| kind: scope.kinds[0], |
| filter: scope.filters[0], |
| orderBy: scope.orders[0], |
| search: '', |
| sensor: false, |
| arguments: [], |
| toggles: [], |
| entityId: '', |
| }; |
| |
| if (scope.dsl) { |
| let lastMethod = scope.dsl.getLastMethod(); |
| if (lastMethod.kind === KIND.METHOD && lastMethod.name === 'config') { |
| scope.state.kind = scope.kinds[1]; |
| scope.state.search = lastMethod.params[0].name; |
| scope.state.item = scope.items.find(item => (item.type === DSL_KINDS.CONFIG && item.name === lastMethod.params[0].name)); |
| } |
| if (lastMethod.kind === KIND.METHOD && ['attributeWhenReady', 'sensor'].includes(lastMethod.name)) { |
| scope.state.kind = scope.kinds[2]; |
| scope.state.search = lastMethod.params[0].name; |
| scope.state.item = scope.items.find(item => (item.type === DSL_KINDS.SENSOR && item.name === lastMethod.params[0].name)); |
| scope.state.sensor = lastMethod.name === 'sensor'; |
| } |
| if (lastMethod.kind === KIND.UTILITY && lastMethod.name === 'formatString') { |
| scope.state.kind = scope.kinds[4]; |
| scope.state.pattern = scope.dsl.params[0].name; |
| scope.state.arguments = Array.from(scope.dsl.params).splice(1).map(argument => { |
| return argument.kind === KIND.STRING ? argument.name : argument.toString(); |
| }); |
| } |
| let relatedEntity = scope.dsl.getRoot().relationships.find(entity => entity.id === scope.dsl.params[0].name); |
| if (relatedEntity) { |
| scope.state.filter = scope.filters.find(filter => filter.id === relatedEntity._id); |
| } |
| } |
| |
| scope.$watch('state.pattern', (newValue, oldValue) => { |
| if (!newValue || angular.equals(newValue, oldValue)) { |
| return; |
| } |
| scope.dsl.params.splice(0, 1, new Dsl(KIND.STRING, newValue)); |
| }); |
| |
| scope.$watchCollection('state.arguments', (newValue, oldValue) => { |
| if (!newValue || angular.equals(newValue, oldValue)) { |
| return; |
| } |
| newValue.forEach((argument, index) => { |
| let dsl; |
| try { |
| dsl = new DslParser().parseString(argument, scope.entity, blueprintService.get()); |
| scope.dsl.params.splice(index + 1, 1, dsl); |
| } catch (ex) { |
| $log.debug(`Argument ${index} is not a DSL. Defaulting to string`, ex); |
| dsl = new Dsl(KIND.STRING, argument); |
| } |
| scope.dsl.params.splice(index + 1, 1, dsl); |
| }) |
| }); |
| |
| scope.isDsl = (index) => { |
| return scope.dsl.params[index + 1] instanceof Dsl && scope.dsl.params[index + 1].kind !== KIND.STRING; |
| }; |
| |
| scope.predicate = (value, index, array) => { |
| let predicates = []; |
| |
| let validTypes = scope.state.kind.id === DSL_KINDS.ALL.id ? Object.values(DSL_KINDS).filter(type => type !== DSL_KINDS.FORMAT_STRING) : [scope.state.kind]; |
| predicates.push(validTypes.map(type => type.id).includes(value.type.id)); |
| |
| if (scope.state.filter.id !== 'blueprint') { |
| predicates.push(scope.state.filter.id === value.entity._id); |
| } |
| |
| if (scope.state.search) { |
| let searchPredicate = value.name.toLowerCase().indexOf(scope.state.search.toLowerCase()) > -1; |
| if (value.description) { |
| searchPredicate |= value.description.toLowerCase().indexOf(scope.state.search.toLowerCase()) > -1; |
| } |
| if ([ DSL_KINDS.ENTITY.id, DSL_KINDS.ALL.id ].includes(scope.state.kind.id) ) { |
| // if searching for entity or config/sensors/entity, show everything. |
| // but searching just for a config or sensor doesn't show everything. |
| // (not sure that's the right semantics?) |
| searchPredicate |= value.entity.id && value.entity.id.toLowerCase().indexOf(scope.state.search.toLowerCase()) > -1; |
| searchPredicate |= value.entity.name && value.entity.name.toLowerCase().indexOf(scope.state.search.toLowerCase()) > -1; |
| } |
| predicates.push(searchPredicate); |
| } |
| return predicates.reduce((ret, predicate) => (ret && predicate), true); |
| }; |
| |
| scope.selectItem = (item, event) => { |
| scope.state.item = item; |
| |
| event.preventDefault(); |
| event.stopPropagation(); |
| }; |
| |
| scope.selectDsl = () => { |
| scope.dsl = buildDsl(); |
| |
| $rootScope.$broadcast(`${MODULE_NAME}.select`, { |
| dsl: scope.dsl, |
| definition: scope.definition |
| }); |
| }; |
| |
| scope.nestDsl = (index) => { |
| scope.dsl = buildDsl(); |
| |
| $rootScope.$broadcast(`${MODULE_NAME}.nest`, { |
| dsl: scope.dsl, |
| definition: scope.definition, |
| index: index |
| }); |
| }; |
| |
| scope.isDone = () => { |
| if (scope.state.kind.id !== DSL_KINDS.FORMAT_STRING.id) { |
| return angular.isDefined(scope.state.item); |
| } else { |
| return brUtilsGeneral.isNonEmpty(scope.state.pattern) && brUtilsGeneral.isNonEmpty(scope.state.arguments); |
| } |
| }; |
| |
| function buildDsl() { |
| let dsl; |
| let funcDsl; |
| |
| if (scope.state.kind.id !== DSL_KINDS.FORMAT_STRING.id) { |
| let scopedDsl = getScopedDsl(scope.entity, scope.state.item.entity, scope.state); |
| |
| switch (scope.state.item.type) { |
| case DSL_KINDS.CONFIG: |
| funcDsl = new Dsl(KIND.METHOD, 'config').param(new Dsl(KIND.STRING, scope.state.item.name)); |
| scopedDsl.chain(funcDsl); |
| dsl = isSelfDsl(scopedDsl) ? funcDsl : scopedDsl; |
| break; |
| case DSL_KINDS.SENSOR: |
| funcDsl = new Dsl(KIND.METHOD, scope.state.sensor ? 'sensor' : 'attributeWhenReady').param(new Dsl(KIND.STRING, scope.state.item.name)); |
| scopedDsl.chain(funcDsl); |
| dsl = isSelfDsl(scopedDsl) ? funcDsl : scopedDsl; |
| break; |
| case DSL_KINDS.ENTITY: |
| dsl = scopedDsl; |
| break; |
| } |
| } else { |
| dsl = new Dsl(KIND.UTILITY, 'formatString').param(new Dsl(KIND.STRING, scope.state.pattern)); |
| scope.state.arguments.forEach((arg, index) => { |
| dsl.param(scope.isDsl(index) ? scope.dsl.params[index + 1] : new Dsl(KIND.STRING, arg)); |
| }); |
| } |
| |
| try { |
| dsl = new DslParser().parseString(dsl.toString(), scope.entity, blueprintService.get()); |
| } catch (ex) { |
| $log.debug(`Cannot get DSL relationship for DSL "${dsl}`, ex); |
| } |
| |
| return dsl; |
| } |
| } |
| |
| function getConfigItems(entity, definition) { |
| let config = entity.miscData.get('config').filter(config => config !== definition).map(config => { |
| return { |
| id: config.name, |
| type: DSL_KINDS.CONFIG, |
| entity: entity, |
| name: config.name, |
| description: config.description |
| }; |
| }); |
| |
| let params = entity.miscData.get('parameters').filter(param => param !== definition).map(param => { |
| return { |
| id: param.name, |
| type: DSL_KINDS.CONFIG, |
| entity: entity, |
| name: param.name, |
| description: param.description |
| }; |
| }); |
| |
| config = config.concat(params); |
| |
| config = Object.values(entity.getClusterMemberspecEntities()).reduce((acc, spec) => { |
| return acc.concat(getConfigItems(spec, definition)); |
| }, config); |
| |
| return entity.children.reduce((acc, child) => { |
| return acc.concat(getConfigItems(child, definition)); |
| }, config); |
| } |
| |
| function getSensorItems(entity) { |
| let sensors = entity.miscData.get('sensors').map(sensor => { |
| return { |
| id: sensor.name, |
| type: DSL_KINDS.SENSOR, |
| entity: entity, |
| name: sensor.name, |
| description: sensor.description |
| }; |
| }); |
| |
| sensors = Object.values(entity.getClusterMemberspecEntities()).reduce((acc, spec) => { |
| return acc.concat(getSensorItems(spec)); |
| }, sensors); |
| |
| return entity.children.reduce((acc, child) => { |
| return acc.concat(getSensorItems(child)); |
| }, sensors); |
| } |
| |
| function getEntityItems(entity, type) { |
| let entities = []; |
| |
| entities.push({ |
| id: entity._id, |
| type: DSL_KINDS.ENTITY, |
| entity: entity, |
| name: entity.miscData.get('typeName') || $filter('entityName')(entity) || 'New application', |
| description: entity.description |
| }); |
| |
| entities = Object.values(entity.getClusterMemberspecEntities()).reduce((acc, spec) => { |
| return acc.concat(getEntityItems(spec, type)); |
| }, entities); |
| |
| return entity.children.reduce((acc, child) => { |
| return acc.concat(getEntityItems(child, type)); |
| }, entities); |
| } |
| |
| function getScopedDsl(entity, targetEntity, state) { |
| if (entity === targetEntity) { |
| return new Dsl(KIND.TARGET, 'self'); |
| } |
| if (entity.parent === targetEntity) { |
| return new Dsl(KIND.TARGET, 'parent'); |
| } |
| |
| if (!targetEntity.hasId()) { |
| if (brUtilsGeneral.isNonEmpty(state.entityId)) { |
| targetEntity.id = state.entityId; |
| } else { |
| targetEntity.id = targetEntity.hasName() ? |
| targetEntity.name.replace(/\W/g, '-').toLowerCase() : |
| targetEntity._id; |
| } |
| } |
| |
| return new Dsl(KIND.METHOD, 'component').param(new Dsl(KIND.STRING, targetEntity.id)); |
| } |
| |
| function isSelfDsl(dsl) { |
| return dsl && dsl.kind === KIND.TARGET && dsl.name === 'self'; |
| } |
| } |
| |
| function templateCache($templateCache) { |
| $templateCache.put(TEMPLATE_URL, template); |
| } |