ui-modules/blueprint-composer/app/components/dsl-editor/dsl-editor.js (328 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';
import angularSanitize from 'angular-sanitize';
import {Dsl, DslParser, KIND, TARGET} 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) {
const blueprint = blueprintService.get();
blueprint.isInDslEdit = true;
scope.$on('$destroy', () => {
blueprint.isInDslEdit = false;
});
scope.$on('d3.entity-selected', (event, entity) => {
scope.state.filter = scope.filters.find(filter => filter.id === entity._id);
});
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, entity, id } = item;
name += attrs.length > 0 ? ` (${attrs.join(', ')})` : ` (${entity.id || id})`;
return {
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();
});
}
try {
let relatedEntity = scope.dsl.params.length && 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);
}
} catch (e) {
console.log("Error analysing DSL (ignore)", scope.dsl, scope.filters, e);
}
}
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 entityPropertyParserFor(type, filterFunc=()=>true) {
return (entity, propertyName) => entity.miscData.get(propertyName)
.filter(filterFunc)
.map(({ name, description }) => ({
id: name,
type,
entity,
name,
description,
}));
}
function uniqueItems(items) {
const IDs = new Set();
// filtering with both own and parent's ID in case we have same-type child nodes
return items.filter(({ id, entity }) => {
const marker = id + ':' + (entity.id || entity._id || '-');
if (IDs.has(marker)) return false;
IDs.add(marker);
return true;
})
}
function getConfigItems(entity, definition, nested=false) {
const parseAsConfig = entityPropertyParserFor(DSL_KINDS.CONFIG, item=>item !== definition);
const result = [
...parseAsConfig(entity, 'config'),
...parseAsConfig(entity, 'parameters'),
];
Object.values(entity.getClusterMemberspecEntities() || {}).forEach(member => {
result.push(...getConfigItems(member, definition, true));
});
(entity.children || []).forEach(child => {
result.push(...getConfigItems(child, definition, true));
});
return nested
? result
: uniqueItems(result); // only need to check distinct items once, not in every recursion
}
function getSensorItems(entity, nested=false) {
const parseAsSensors = entityPropertyParserFor(DSL_KINDS.SENSOR);
const result = parseAsSensors(entity, 'sensors');
Object.values(entity.getClusterMemberspecEntities() || {}).forEach(member => {
result.push(...getSensorItems(member, true));
});
(entity.children || []).forEach(child => {
result.push(...getSensorItems(child, true));
});
return nested
? result
: uniqueItems(result);
}
function getEntityItems(entity, nested=false) {
const result = [{
id: entity._id,
type: DSL_KINDS.ENTITY,
entity: entity,
name: entity.miscData.get('typeName') || $filter('entityName')(entity) || 'New application',
description: entity.description
}];
Object.values(entity.getClusterMemberspecEntities() || {}).forEach(member => {
result.push(...getEntityItems(member, true));
});
(entity.children || []).forEach(child => {
result.push(...getEntityItems(child, true));
});
return nested
? result
: uniqueItems(result);
}
function getScopedDsl(entity, targetEntity, state) {
if (entity === targetEntity) {
return new Dsl(KIND.TARGET, TARGET.SELF);
}
if (blueprintService.get() === targetEntity) {
return new Dsl(KIND.TARGET, TARGET.SCOPEROOT);
}
if (!targetEntity.hasId()) {
if (brUtilsGeneral.isNonEmpty(state.entityId)) {
targetEntity.id = state.entityId;
} else {
blueprintService.populateId(targetEntity);
}
}
return new Dsl(KIND.TARGET, TARGET.COMPONENT).param(new Dsl(KIND.STRING, targetEntity.id));
}
function isSelfDsl(dsl) {
return dsl && dsl.kind === KIND.TARGET && dsl.name === TARGET.SELF;
}
}
function templateCache($templateCache) {
$templateCache.put(TEMPLATE_URL, template);
}