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 2018/07/26 10:50:35 UTC
[18/28] brooklyn-ui git commit: This adds the contributed
brooklyn-ui-angular code in the folder new/.
http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/29927b73/new/ui-modules/blueprint-composer/app/components/providers/blueprint-service.provider.js
----------------------------------------------------------------------
diff --git a/new/ui-modules/blueprint-composer/app/components/providers/blueprint-service.provider.js b/new/ui-modules/blueprint-composer/app/components/providers/blueprint-service.provider.js
new file mode 100644
index 0000000..0e4b360
--- /dev/null
+++ b/new/ui-modules/blueprint-composer/app/components/providers/blueprint-service.provider.js
@@ -0,0 +1,590 @@
+/*
+ * 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 {Entity, EntityFamily} from "../util/model/entity.model";
+import {Issue, ISSUE_LEVEL} from '../util/model/issue.model';
+import {Dsl} from "../util/model/dsl.model";
+import jsYaml from "js-yaml";
+import typeNotFoundIcon from "../../img/icon-not-found.svg";
+
+const TAG = 'SERVICE :: BLUEPRINT :: ';
+export const RESERVED_KEYS = ['name', 'location', 'locations', 'type', 'services', 'brooklyn.config', 'brooklyn.children', 'brooklyn.enrichers', 'brooklyn.policies'];
+export const DSL_ENTITY_SPEC = '$brooklyn:entitySpec';
+
+export function blueprintServiceProvider() {
+ return {
+ $get: ['$log', '$q', '$sce', 'paletteApi', 'iconGenerator', 'dslService',
+ function ($log, $q, $sce, paletteApi, iconGenerator, dslService) {
+ return new BlueprintService($log, $q, $sce, paletteApi, iconGenerator, dslService);
+ }]
+ }
+}
+
+function BlueprintService($log, $q, $sce, paletteApi, iconGenerator, dslService) {
+ let blueprint = new Entity();
+
+ return {
+ setFromJson: setBlueprintFromJson,
+ setFromYaml: setBlueprintFromYaml,
+ get: getBlueprint,
+ getAsJson: getBlueprintAsJson,
+ getAsYaml: getBlueprintAsYaml,
+ reset: resetBlueprint,
+ find: findEntity,
+ findAny: findAnyEntity,
+ getEntityMetadata: getEntityMetadata,
+ entityHasMetadata: entityHasMetadata,
+ refreshBlueprintMetadata: refreshBlueprintMetadata,
+ refreshEntityMetadata: refreshEntityMetadata,
+ refreshConfigConstraints: refreshConfigConstraints,
+ refreshRelationships: refreshRelationships,
+ refreshAllRelationships: refreshAllRelationships,
+ isReservedKey: isReservedKey,
+ getIssues: getIssues,
+ hasIssues: hasIssues,
+ populateEntityFromApi: populateEntityFromApiSuccess,
+ populateLocationFromApi: populateLocationFromApiSuccess,
+ addConfigKeyDefinition: addConfigKeyDefinition,
+ getRelationships: getRelationships,
+ };
+
+ function setBlueprintFromJson(jsonBlueprint) {
+ if (jsonBlueprint) {
+ blueprint.setEntityFromJson(jsonBlueprint);
+ $log.debug(TAG + 'Blueprint set from JS', blueprint);
+ } else {
+ resetBlueprint();
+ }
+ }
+
+ function setBlueprintFromYaml(yamlBlueprint, reset = false) {
+ let newBlueprint = jsYaml.safeLoad(yamlBlueprint);
+ if (reset) {
+ resetBlueprint();
+ }
+ setBlueprintFromJson(newBlueprint);
+ $log.debug(TAG + 'Blueprint set from YAML', blueprint);
+ }
+
+ function getBlueprint() {
+ return blueprint;
+ }
+
+ function getBlueprintAsJson() {
+ return blueprint.getData();
+ }
+
+ function getBlueprintAsYaml() {
+ try {
+ let currentModel = blueprint.getData();
+ return (Object.keys(currentModel).length === 0) ? '' : jsYaml.safeDump(currentModel, { lineWidth: 127 });
+ } catch (err) {
+ $log.error("Error converting brooklyn blueprint (rethrowing)", err);
+ throw 'Could not convert the Brooklyn blueprint into YAML format';
+ }
+ }
+
+ function resetBlueprint() {
+ blueprint.reset();
+ }
+
+ function findEntity(id) {
+ if (!id) {
+ return null;
+ }
+ return lookup(blueprint, id);
+ }
+
+
+ function findAnyEntity(id) {
+ if (!id) {
+ return null;
+ }
+ return lookup(blueprint, id, true);
+ }
+
+ function getEntityMetadata(entity) {
+ let metadata = new Map();
+ if (entity instanceof Entity) {
+ entity.metadata.forEach((value, key)=> {
+ if (RESERVED_KEYS.indexOf(key) === -1) {
+ metadata.set(key, value);
+ }
+ });
+ }
+ return metadata;
+ }
+
+ function entityHasMetadata(entity) {
+ return this.getEntityMetadata(entity).size > 0;
+ }
+
+ function isReservedKey(key) {
+ return RESERVED_KEYS.indexOf(key) > -1;
+ }
+
+ function getIssues(entity = blueprint) {
+ let issues = [];
+
+ if (entity.hasIssues()) {
+ issues = issues.concat(entity.issues.map((issue)=> {
+ let newIssue = Issue.builder().message(issue.message).group(issue.group).ref(issue.ref).level(issue.level).build();
+ newIssue.entity = entity;
+ return newIssue;
+ }));
+ }
+
+ entity.policies.forEach((policy)=> {
+ issues = issues.concat(getIssues(policy));
+ });
+
+ entity.enrichers.forEach((enricher)=> {
+ issues = issues.concat(getIssues(enricher));
+ });
+
+ entity.children.forEach((child)=> {
+ issues = issues.concat(getIssues(child));
+ });
+
+ return issues;
+ }
+
+ function hasIssues() {
+ return this.getIssues().length > 0;
+ }
+
+ function lookup(entity, id, any = false) {
+ if (entity._id === id) {
+ return entity;
+ }
+ if (entity.childrenAsMap.has(id)) {
+ return entity.childrenAsMap.get(id);
+ }
+ if (entity.policies.has(id)) {
+ return entity.policies.get(id);
+ }
+ if (entity.enrichers.has(id)) {
+ return entity.enrichers.get(id);
+ }
+ let memberSpecs = Object.values(entity.getClusterMemberspecEntities()).filter((memberSpec)=>(memberSpec._id === id));
+ if (memberSpecs.length === 1) {
+ return memberSpecs[0];
+ }
+ for (let child of entity.childrenAsMap.values()) {
+ let ret = lookup(child, id, any);
+ if (ret !== null) {
+ return ret;
+ }
+ }
+ return null;
+ }
+
+ function refreshBlueprintMetadata(entity = blueprint, family = 'ENTITY') {
+ return refreshEntityMetadata(entity, family).then(()=> {
+ return $q.all(entity.children.reduce((result, child) => {
+ result.push(refreshBlueprintMetadata(child));
+ return result;
+ }, []));
+ }).then(() => {
+ return entity;
+ });
+ }
+
+ function refreshEntityMetadata(entity, family) {
+ entity.miscData.set('loading', true);
+ return $q.all([refreshTypeMetadata(entity, family), refreshLocationMetadata(entity)]).then(()=> {
+ entity.miscData.set('loading', false);
+ return refreshRelationships(entity);
+ }).then(() => {
+ return $q.all([
+ refreshConfigConstraints(entity),
+ refreshConfigMemberspecsMetadata(entity),
+ refreshPoliciesMetadata(entity),
+ refreshEnrichersMetadata(entity),
+ ]);
+ }).then(()=> {
+ return entity;
+ });
+ }
+
+ function refreshTypeMetadata(entity, family) {
+ let deferred = $q.defer();
+
+ if (entity.hasType()) {
+ entity.family = family;
+
+ let promise = entity.miscData.has('bundle')
+ ? paletteApi.getBundleType(entity.miscData.get('bundle').symbolicName, entity.miscData.get('bundle').version, entity.type, entity.version)
+ : paletteApi.getType(entity.type, entity.version);
+
+ promise.then((data)=> {
+ deferred.resolve(populateEntityFromApiSuccess(entity, data));
+ }).catch(function () {
+ deferred.resolve(populateEntityFromApiError(entity));
+ });
+ } else if (entity.parent) {
+ entity.clearIssues({group: 'type'}).addIssue(Issue.builder().group('type').message('Entity needs a type').level(ISSUE_LEVEL.WARN).build());
+ entity.miscData.set('sensors', []);
+ entity.miscData.set('traits', []);
+ deferred.resolve(entity);
+ addUnlistedConfigKeysDefinitions(entity);
+ } else {
+ entity.miscData.set('sensors', []);
+ entity.miscData.set('traits', []);
+ deferred.resolve(entity);
+ addUnlistedConfigKeysDefinitions(entity);
+ }
+
+ return deferred.promise;
+ }
+
+ function refreshLocationMetadata(entity) {
+ let deferred = $q.defer();
+
+ if (entity.hasLocation()) {
+ paletteApi.getLocation(entity.location).then((location)=> {
+ deferred.resolve(populateLocationFromApiSuccess(entity, location.catalog || location));
+ }).catch(function () {
+ deferred.resolve(populateLocationFromApiError(entity));
+ });
+ } else {
+ deferred.resolve(entity);
+ }
+
+ return deferred.promise;
+ }
+
+ function refreshConfigConstraints(entity) {
+ return $q((resolve) => {
+ if (entity.miscData.has('config')) {
+ entity.miscData.get('config')
+ .filter(config => config.constraints && Object.keys(config.constraints).length > 0)
+ .forEach(config => {
+ for (let [key, constraint] of Object.entries(config.constraints) ) {
+ let message = null;
+ switch (key) {
+ case 'Predicates.notNull()':
+ case 'required':
+ if (!angular.isDefined(config.defaultValue) && (!entity.config.has(config.name) || !angular.isDefined(entity.config.get(config.name)))) {
+ message = `<samp>${config.name}</samp> is required`;
+ }
+ break;
+ case 'regex':
+ if (!entity.config.has(config.name) || !angular.isDefined(entity.config.get(config.name)) || !(new RegExp(constraint).test(entity.config.get(config.name)))) {
+ message = `<samp>${config.name}</samp> does not match the required format: <samp>${config.constraints.regex}</samp>`;
+ }
+ break;
+ }
+ if (message !== null) {
+ entity.addIssue(Issue.builder().group('config').ref(config.name).message($sce.trustAsHtml(message)).build());
+ }
+ }
+ });
+ }
+ resolve();
+ });
+ }
+
+ function refreshConfigMemberspecsMetadata(entity) {
+ let promiseArray = [];
+ if (entity.isCluster()) {
+ Object.values(entity.getClusterMemberspecEntities()).forEach((memberSpec)=> {
+ // memberSpec can be `undefined` if the member spec is not a `$brooklyn:entitySpec`, e.g. it is `$brooklyn:config("spec")`.
+ // there may be a better way but this seems to handle it.
+ if (memberSpec) promiseArray.push(refreshBlueprintMetadata(memberSpec, 'SPEC'));
+ });
+ }
+ return $q.all(promiseArray);
+ }
+
+ function refreshPoliciesMetadata(entity) {
+ return $q.all(entity.getPoliciesAsArray().reduce((result, policy)=> {
+ policy.miscData.set('loading', true);
+ policy.family = 'POLICY';
+
+ let deferred = $q.defer();
+
+ paletteApi.getType(policy.type, policy.version).then((data)=> {
+ deferred.resolve(populateEntityFromApiSuccess(policy, data));
+ }).catch(function () {
+ deferred.resolve(populateEntityFromApiError(policy));
+ }).finally(()=> {
+ policy.miscData.set('loading', false);
+ });
+ result.push(deferred);
+ return result;
+ }, []));
+ }
+
+ function refreshEnrichersMetadata(entity) {
+ return $q.all(entity.getEnrichersAsArray().reduce((result, enricher)=> {
+ enricher.miscData.set('loading', true);
+ enricher.family = 'ENRICHER';
+
+ let deferred = $q.defer();
+
+ paletteApi.getType(enricher.type, enricher.version).then((data)=> {
+ deferred.resolve(populateEntityFromApiSuccess(enricher, data));
+ }).catch(function () {
+ deferred.resolve(populateEntityFromApiError(enricher));
+ }).finally(()=> {
+ enricher.miscData.set('loading', false);
+ });
+ result.push(deferred);
+ return result;
+ }, []));
+ }
+
+ function parseInput(input, entity) {
+ return $q((resolve, reject) => {
+ try {
+ let parsed = dslService.parse(input, entity, getBlueprint());
+ if (parsed.kind && parsed.kind.family === 'constant') {
+ reject('constants not interpreted as DSL when parsed', input);
+ } else {
+ resolve(parsed);
+ }
+ } catch (ex) {
+ $log.debug(ex);
+ reject(ex, input);
+ }
+ });
+ }
+
+ function refreshRelationships(entity) {
+ return $q.all(Array.from(entity.config.keys()).reduce((promises, key) => {
+ let value = entity.config.get(key);
+
+ // Return promises that returns an object like { key: "my-config-key-key", issues: [] }
+ if (value instanceof Dsl) {
+ promises.push(
+ parseInput(value.toString(), entity).then(dsl => {
+ entity.config.set(key, dsl);
+ return {
+ key: key,
+ issues: dsl.getAllIssues()
+ };
+ }).catch(() => $q.resolve({
+ key: key,
+ issues: []
+ }))
+ );
+ } else if (value instanceof Array) {
+ promises.push(
+ $q.all(value.reduce((issues, itemValue, itemIndex) => {
+ return issues.concat(
+ parseInput(itemValue, entity).then(dsl => {
+ value[itemIndex] = dsl;
+ return dsl.getAllIssues();
+ }).catch(() => [])
+ );
+ }, [])).then(issues => {
+ return {
+ key: key,
+ issues: issues.reduce((acc, issue) => acc.concat(issue), [])
+ }
+ })
+ );
+ } else if (value instanceof Object) {
+ promises.push(
+ $q.all(Object.keys(value).reduce((issues, itemKey) => {
+ return issues.concat(
+ parseInput(value[itemKey], entity).then(dsl => {
+ value[itemKey] = dsl;
+ return dsl.getAllIssues();
+ }).catch(() => [])
+ );
+ }, [])).then(issues => {
+ return {
+ key: key,
+ issues: issues.reduce((acc, issue) => acc.concat(issue), [])
+ }
+ })
+ );
+ } else {
+ promises.push(
+ parseInput(value, entity).then(dsl => {
+ entity.config.set(key, dsl);
+ return {
+ key: key,
+ issues: dsl.getAllIssues()
+ };
+ }).catch(() => $q.resolve({
+ key: key,
+ issues: []
+ }))
+ );
+ }
+
+ return promises;
+ }, [])).then(results => {
+ results.forEach(result => {
+ entity.clearIssues({ref: result.key});
+ result.issues.forEach(issue => {
+ entity.addIssue(Issue.builder().group('config').ref(result.key).message($sce.trustAsHtml(issue)).build());
+ });
+ })
+ });
+ }
+
+ function refreshAllRelationships(entity = blueprint) {
+ let promises = [];
+ promises.push(refreshRelationships(entity));
+ promises.concat(entity.children.map(child => refreshAllRelationships(child)));
+
+ return $q.all(promises);
+ }
+
+ function addConfigKeyDefinition(config, key) {
+ config.push({
+ "constraints": [],
+ "description": "",
+ "name": key,
+ "label": key,
+ "priority": 1,
+ "pinned": true,
+ "type": "java.lang.String",
+ });
+ }
+
+ function addUnlistedConfigKeysDefinitions(entity) {
+ let allConfig = entity.miscData.get('config') || [];
+ entity.config.forEach((value, key) => {
+ if (!allConfig.some((e) => e.name === key)) {
+ addConfigKeyDefinition(allConfig, key);
+ }
+ });
+ entity.miscData.set('config', allConfig);
+ }
+
+ function populateEntityFromApiSuccess(entity, data) {
+ entity.clearIssues({group: 'type'});
+ entity.type = data.symbolicName;
+ entity.icon = data.iconUrl || iconGenerator(data.symbolicName);
+ entity.miscData.set('important', !!data.iconUrl);
+ entity.miscData.set('bundle', {
+ symbolicName: data.containingBundle.split(':')[0],
+ version: data.containingBundle.split(':')[1]
+ });
+ entity.miscData.set('typeName', data.displayName || data.symbolicName);
+ entity.miscData.set('config', (data.config || []).map(config => {
+ if (config.constraints) {
+ config.constraints = config.constraints.reduce((map, constraint) => {
+ if (constraint.startsWith('matchesRegex')) {
+ map.regex = constraint.replace(/matchesRegex\(([^)]*)\)/, '$1');
+ } else {
+ // The constraint "required" falls back into this case. Good enough for now as we only support
+ // 2 types of constraints.
+ map[constraint] = true;
+ }
+ return map;
+ }, {});
+ }
+ return config;
+ }));
+ entity.miscData.set('sensors', data.sensors || []);
+ entity.miscData.set('traits', data.supertypes || []);
+ entity.miscData.set('virtual', data.virtual || null);
+ addUnlistedConfigKeysDefinitions(entity);
+ return entity;
+ }
+
+ function populateEntityFromApiError(entity) {
+ entity.clearIssues({group: 'type'});
+ entity.addIssue(Issue.builder().group('type').message($sce.trustAsHtml(`Type <samp>${entity.type + (entity.hasVersion ? ':' + entity.version : '')}</samp> does not exist`)).build());
+ entity.miscData.set('typeName', entity.type || '');
+ entity.miscData.set('config', []);
+ entity.miscData.set('sensors', []);
+ entity.miscData.set('traits', []);
+ entity.miscData.set('virtual', null);
+ entity.icon = typeNotFoundIcon;
+ addUnlistedConfigKeysDefinitions(entity);
+ return entity;
+ }
+
+ function populateLocationFromApiSuccess(entity, data) {
+ entity.clearIssues({group: 'location'});
+ entity.location = data.symbolicName;
+ entity.miscData.set('locationName', data.name);
+ entity.miscData.set('locationIcon', data.iconUrl || iconGenerator(data.symbolicName));
+ return entity;
+ }
+
+ function populateLocationFromApiError(entity) {
+ entity.clearIssues({group: 'location'});
+ entity.addIssue(Issue.builder().level(ISSUE_LEVEL.WARN).group('location').message($sce.trustAsHtml(`Location <samp>${!(entity.location instanceof String) ? JSON.stringify(entity.location) : entity.location}</samp> does not exist in your local catalog. Deployment might fail.`)).build());
+ entity.miscData.set('locationName', entity.location);
+ entity.miscData.set('locationIcon', typeNotFoundIcon);
+ return entity;
+ }
+
+ /**
+ * Retrieves all the Entities referenced by an Entity
+ * @param {Entity} entity the Entity to resolve relative references from
+ * @return {Array} of objects that contains source and target entities
+ */
+ function getRelationships(entity = blueprint) {
+ let set = Array.from(entity.config.values())
+ .reduce((set, config)=> {
+ if (config instanceof Dsl) {
+ config.relationships.forEach((entity) => {
+ if (entity !== null) {
+ set.add(entity);
+ }
+ });
+ }
+ if (config instanceof Array) {
+ config
+ .filter(conf => conf instanceof Dsl)
+ .reduce((set, config)=> {
+ config.relationships.forEach((entity)=> {
+ if (entity !== null) {
+ set.add(entity);
+ }
+ });
+ return set;
+ }, set);
+ }
+ if (config instanceof Object) {
+ Object.keys(config)
+ .filter(key => config[key] instanceof Dsl)
+ .reduce((set, key)=> {
+ config[key].relationships.forEach((entity)=> {
+ if (entity !== null) {
+ set.add(entity);
+ }
+ });
+ return set;
+ }, set);
+ }
+ return set;
+ }, new Set());
+
+ let relationships = Array.from(set).map((relation) => {
+ return {
+ source: entity,
+ target: relation
+ };
+ });
+
+ return entity.children.reduce((relationships, child) => {
+ return relationships.concat(getRelationships(child))
+ }, relationships);
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/29927b73/new/ui-modules/blueprint-composer/app/components/providers/blueprint-service.provider.spec.js
----------------------------------------------------------------------
diff --git a/new/ui-modules/blueprint-composer/app/components/providers/blueprint-service.provider.spec.js b/new/ui-modules/blueprint-composer/app/components/providers/blueprint-service.provider.spec.js
new file mode 100644
index 0000000..2514e9e
--- /dev/null
+++ b/new/ui-modules/blueprint-composer/app/components/providers/blueprint-service.provider.spec.js
@@ -0,0 +1,75 @@
+/*
+ * 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 {blueprintServiceProvider} from "./blueprint-service.provider";
+import {dslServiceProvider} from "./dsl-service.provider";
+import {catalogApiProvider} from "brooklyn-ui-utils/providers/catalog-api.provider";
+import {locationApiProvider} from "brooklyn-ui-utils/providers/location-api.provider";
+import {iconGeneratorProvider} from "brooklyn-ui-utils/icon-generator/icon-generator";
+import {Entity} from "../util/model/entity.model";
+import {paletteApiProvider} from './palette-api.provider';
+
+
+describe('Blueprint Service', ()=> {
+ let $rootScope, blueprintService;
+
+ beforeEach(angular.mock.module(($provide)=> {
+ $provide.provider('catalogApi', catalogApiProvider);
+ $provide.provider('locationApi', locationApiProvider);
+ $provide.provider('paletteApi', paletteApiProvider);
+ $provide.provider('iconGenerator', iconGeneratorProvider);
+ $provide.provider('dslService', dslServiceProvider);
+ $provide.provider('blueprintService', blueprintServiceProvider);
+ }));
+ beforeEach(angular.mock.inject((_$rootScope_, _blueprintService_)=> {
+ $rootScope = _$rootScope_;
+ blueprintService = _blueprintService_;
+ }));
+
+ it('should load blueprint from YAML', ()=> {
+ blueprintService.setFromYaml(YAML_BLUEPRINT);
+ let bp = blueprintService.get();
+ expect(bp instanceof Entity).toBe(true);
+ expect(bp.hasName()).toBe(true);
+ expect(bp.children.length).toBe(3);
+ expect(bp.children[0].hasType()).toBe(true);
+ expect(bp.children[0].type).toBe('com.example.Entity1');
+ expect(blueprintService.getAsYaml()).toEqual(YAML_BLUEPRINT);
+ });
+
+ it('should return an entity by internal id', ()=> {
+ blueprintService.setFromYaml(YAML_BLUEPRINT);
+ for (let childId of blueprintService.get().childrenAsMap.keys()) {
+ let childEntity = blueprintService.find(childId);
+ expect(childEntity).toBeDefined();
+ expect(childEntity).not.toBeNull();
+ expect(childEntity._id).toEqual(childId);
+ }
+ let unknownEntity = blueprintService.find(Math.random().toString(36).slice(2))
+ expect(unknownEntity).toBeDefined();
+ expect(unknownEntity).toBeNull();
+ });
+
+});
+
+const YAML_BLUEPRINT = '' +
+ 'name: Test Blueprint\n' +
+ 'services:\n' +
+ ' - type: com.example.Entity1\n' +
+ ' - type: com.example.Entity2\n' +
+ ' - type: com.example.Entity3\n';
http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/29927b73/new/ui-modules/blueprint-composer/app/components/providers/dsl-service.provider.js
----------------------------------------------------------------------
diff --git a/new/ui-modules/blueprint-composer/app/components/providers/dsl-service.provider.js b/new/ui-modules/blueprint-composer/app/components/providers/dsl-service.provider.js
new file mode 100644
index 0000000..3d5f665
--- /dev/null
+++ b/new/ui-modules/blueprint-composer/app/components/providers/dsl-service.provider.js
@@ -0,0 +1,82 @@
+/*
+ * 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 {Dsl, DslParser, KIND} from "../util/model/dsl.model";
+import {Entity} from "../util/model/entity.model";
+
+const TAG = 'SERVICE :: DSL :: ';
+
+export function dslServiceProvider() {
+ return {
+ $get: ['$log', function ($log) {
+ return new DslService($log);
+ }]
+ }
+}
+
+/**
+ *
+ * @param $log
+ * @return {{parse: parse, generate: generate}}
+ * @constructor
+ */
+function DslService($log) {
+
+ return {
+ parse: parse,
+ generate: generate,
+ };
+
+ /**
+ * Parses a string containing a DSL expression and resolve its references to Entities
+ * @param dsl the string to parse
+ * @param {Entity} entity the base Entity to resolve relative references from
+ * @param {Entity} blueprint the root Entity
+ * @return {Dsl} a Dsl object containing relationships (referenced Entities)
+ */
+ function parse(dsl, entity, blueprint) {
+ return new DslParser(dsl).parse(entity, function lookupById(id, entity = blueprint) {
+ if (entity.id === id) {
+ return entity;
+ }
+ for (let child of entity.childrenAsMap.values()) {
+ let ret = lookupById(id, child);
+ if (ret !== null) {
+ return ret;
+ }
+ }
+ return null;
+ });
+ }
+
+ /**
+ * Generates the DSL representation for a Dsl object
+ * @param dsl
+ * @return {string}
+ */
+ function generate(dsl) {
+ if (!dsl) {
+ throw new Error('"dsl" param must be specified');
+ }
+ if (!(dsl instanceof Dsl)) {
+ throw new Error('"dsl" param must be an instance of Dsl');
+ }
+ return dsl.getRoot().toString();
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/29927b73/new/ui-modules/blueprint-composer/app/components/providers/dsl-service.provider.spec.js
----------------------------------------------------------------------
diff --git a/new/ui-modules/blueprint-composer/app/components/providers/dsl-service.provider.spec.js b/new/ui-modules/blueprint-composer/app/components/providers/dsl-service.provider.spec.js
new file mode 100644
index 0000000..aaa0063
--- /dev/null
+++ b/new/ui-modules/blueprint-composer/app/components/providers/dsl-service.provider.spec.js
@@ -0,0 +1,119 @@
+/*
+ * 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 {Dsl, DslParser, KIND} from "../util/model/dsl.model";
+import {Entity} from "../util/model/entity.model";
+import {dslServiceProvider} from "./dsl-service.provider";
+import {blueprintServiceProvider} from "./blueprint-service.provider";
+import {catalogApiProvider} from "brooklyn-ui-utils/providers/catalog-api.provider";
+import {locationApiProvider} from "brooklyn-ui-utils/providers/location-api.provider";
+import {iconGeneratorProvider} from "brooklyn-ui-utils/icon-generator/icon-generator";
+
+
+describe('Dsl Service', ()=> {
+ let $rootScope, dslService;
+
+ beforeEach(angular.mock.module(($provide)=> {
+ $provide.provider('catalogApi', catalogApiProvider);
+ $provide.provider('locationApi', locationApiProvider);
+ $provide.provider('iconGenerator', iconGeneratorProvider);
+ $provide.provider('blueprintService', blueprintServiceProvider);
+ $provide.provider('dslService', dslServiceProvider);
+ }));
+ beforeEach(angular.mock.inject((_$rootScope_, _dslService_)=> {
+ $rootScope = _$rootScope_;
+ dslService = _dslService_;
+ }));
+
+ it('should skip relationships to self()', ()=> {
+ let entity1 = new Entity();
+ const CONFIG_OBJECT = {
+ textKey: 'textValue1',
+ boolKey: false,
+ numKey: 123456789,
+ nullKey: null,
+ objectKey: {
+ key: 'val1',
+ }
+ };
+ entity1.setEntityFromJson(CONFIG_OBJECT);
+
+ let target_expr = '$brooklyn:self().attributeWhenReady("http.port")';
+ let dsl = dslService.parse(target_expr, entity1, entity1);
+
+ expect(dsl.relationships.length).toBe(0);
+ });
+
+ it('should resolve relationships to parent()', ()=> {
+ let entity1 = new Entity();
+ const CONFIG_OBJECT1 = {
+ textKey: 'textValue1',
+ boolKey: false,
+ numKey: 123456789,
+ nullKey: null,
+ objectKey: {
+ key: 'val1',
+ }
+ };
+ entity1.setEntityFromJson(CONFIG_OBJECT1);
+
+ let entity2 = new Entity();
+ const CONFIG_OBJECT2 = {
+ textKey: 'textValue2',
+ boolKey: false,
+ numKey: 123456789,
+ nullKey: null,
+ objectKey: {
+ key: 'val2',
+ }
+ };
+ entity2.setEntityFromJson(CONFIG_OBJECT2);
+
+ entity2.parent = entity1;
+
+ let target_expr = '$brooklyn:self().parent().attributeWhenReady("http.port")';
+ let dsl = dslService.parse(target_expr, entity2, entity1);
+
+ expect(dsl.relationships.length).toBe(1);
+ expect(dsl.relationships.includes(entity1)).toBe(true);
+ });
+
+ it('should populate issues for non-existing nested entities', ()=> {
+ let entity1 = new Entity();
+ const CONFIG_OBJECT = {
+ textKey: 'textValue1',
+ boolKey: false,
+ numKey: 123456789,
+ nullKey: null,
+ objectKey: {
+ key: 'val1',
+ }
+ };
+ entity1.setEntityFromJson(CONFIG_OBJECT);
+
+ let target_expr = '$brooklyn:formatString("%s:%s", $brooklyn:component("foo").attributeWhenReady("host.name"), $brooklyn:component("bar").attributeWhenReady("http.port"))';
+ let dsl = dslService.parse(target_expr, entity1, entity1);
+
+ expect(dsl.relationships.length).toBe(0);
+ // look for issues containing "foo", "bar"
+ let issues = dsl.getAllIssues();
+ expect(issues.length).toBe(2);
+ expect(issues[0].search(/foo|bar/)).toBeGreaterThan(-1);
+ expect(issues[1].search(/foo|bar/)).toBeGreaterThan(-1);
+ });
+});
http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/29927b73/new/ui-modules/blueprint-composer/app/components/providers/palette-api.js
----------------------------------------------------------------------
diff --git a/new/ui-modules/blueprint-composer/app/components/providers/palette-api.js b/new/ui-modules/blueprint-composer/app/components/providers/palette-api.js
new file mode 100644
index 0000000..28ada7b
--- /dev/null
+++ b/new/ui-modules/blueprint-composer/app/components/providers/palette-api.js
@@ -0,0 +1,62 @@
+/*
+ * 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.
+ */
+
+export class PaletteApi {
+ constructor($http, $q) {
+ this.$http = $http;
+ this.$q = $q;
+ }
+
+ getTypes(family, search) {
+ // no-op
+ return Promise.resolve();
+ }
+
+ getType(typeSymbolicName, typeVersion) {
+ // no-op
+ return Promise.resolve();
+ }
+
+ getTypeVersion(typeSymbolicName) {
+ // no-op
+ return Promise.resolve();
+ }
+
+ getBundleType(bundleSymbolicName, bundleVersion, typeSymbolicName, typeVersion) {
+ // no-op
+ return Promise.resolve();
+ }
+
+ getLocations() {
+ // no-op
+ return Promise.resolve();
+ }
+
+ getLocation(locationSymbolicName) {
+ // no-op
+ return Promise.resolve();
+ }
+
+ create(bom, params) {
+ // no-op
+ return Promise.resolve();
+ }
+}
+
+window.PaletteApi = PaletteApi;
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/29927b73/new/ui-modules/blueprint-composer/app/components/providers/palette-api.provider.js
----------------------------------------------------------------------
diff --git a/new/ui-modules/blueprint-composer/app/components/providers/palette-api.provider.js b/new/ui-modules/blueprint-composer/app/components/providers/palette-api.provider.js
new file mode 100644
index 0000000..a2e9639
--- /dev/null
+++ b/new/ui-modules/blueprint-composer/app/components/providers/palette-api.provider.js
@@ -0,0 +1,94 @@
+/*
+ * 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 brApiModule from 'brooklyn-ui-utils/brooklyn.api/brooklyn.api';
+import {PaletteApi} from './palette-api';
+
+const MODULE_NAME = 'brooklyn.composer.api.palette';
+
+angular.module(MODULE_NAME, [brApiModule])
+ .provider('paletteApi', paletteApiProvider);
+
+export default MODULE_NAME;
+
+export function paletteApiProvider() {
+ let implementation = PaletteApiProvider;
+
+ return {
+ implementation: function (impl) {
+ if (!(impl.prototype === PaletteApi.prototype)) {
+ throw new Error(`Expected an implementation extending ${PaletteApi} but got ${impl}`);
+ }
+ implementation = impl;
+ },
+ $get: ['catalogApi', 'locationApi', '$http', '$q', function (catalogApi, locationApi, $http, $q) {
+ return new implementation(catalogApi, locationApi, $http, $q);
+ }]
+ }
+}
+
+class PaletteApiProvider extends PaletteApi {
+ constructor(catalogApi, locationApi, $http, $q) {
+ super($http, $q);
+ this.catalogApi = catalogApi;
+ this.locationApi = locationApi;
+ }
+
+ getTypes(params) {
+ return this.catalogApi.getTypes(params);
+ }
+
+ getType(typeSymbolicName, typeVersion) {
+ return this.catalogApi.getType(typeSymbolicName, typeVersion);
+ }
+
+ getTypeVersion(typeSymbolicName) {
+ return this.catalogApi.getTypeVersion(typeSymbolicName);
+ }
+
+ getBundleType(bundleSymbolicName, bundleVersion, typeSymbolicName, typeVersion) {
+ return this.catalogApi.getBundleType(bundleSymbolicName, bundleVersion, typeSymbolicName, typeVersion);
+ }
+
+ getLocations() {
+ return this.locationApi.getLocations().then(locations => {
+ let supertypes = ['org.apache.brooklyn.api.location'];
+ return locations.map(location => {
+ if (location.catalog) {
+ location.catalog.displayName = location.config.displayName;
+ location.catalog.supertypes = supertypes;
+ return location.catalog;
+ }
+ return {
+ symbolicName: location.name,
+ displayName: location.config ? location.config.displayName : location.name,
+ supertypes: supertypes
+ };
+ });
+ });
+ }
+
+ getLocation(locationSymbolicName) {
+ return this.locationApi.getLocation(locationSymbolicName);
+ }
+
+ create(bom, params) {
+ return this.catalogApi.create(bom, params);
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/29927b73/new/ui-modules/blueprint-composer/app/components/providers/palette-dragndrop.provider.js
----------------------------------------------------------------------
diff --git a/new/ui-modules/blueprint-composer/app/components/providers/palette-dragndrop.provider.js b/new/ui-modules/blueprint-composer/app/components/providers/palette-dragndrop.provider.js
new file mode 100644
index 0000000..dee0836
--- /dev/null
+++ b/new/ui-modules/blueprint-composer/app/components/providers/palette-dragndrop.provider.js
@@ -0,0 +1,96 @@
+/*
+ * 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 {Entity} from '../util/model/entity.model';
+
+const TAG = 'SERVICE :: DRAGNDROP :: ';
+
+export function paletteDragAndDropServiceProvider() {
+ return {
+ $get: ['$log', function ($log) {
+ return new paletteDragAndDropService($log);
+ }]
+ }
+}
+
+/**
+ * Provides state for Drag and Drop operations
+ * from the palette into the blueprint graph
+ * @param $log
+ * @constructor
+ */
+function paletteDragAndDropService($log) {
+
+ let state = 0; /* 0 = no drag; 1 = drag in progress; 2 = over target */
+ let draggedItem = null;
+ let dropTarget = null;
+ let targetIndex = -1;
+
+ return {
+ dragStart: dragStart,
+ dragEnd: dragEnd,
+ dragEnter: dragEnter,
+ dragLeave: dragLeave,
+ isDropAllowed: isDropAllowed,
+ draggedItem: () => draggedItem,
+ dropTarget: () => dropTarget,
+ targetIndex: () => targetIndex,
+ };
+
+ function isDropAllowed() {
+ return state === 2 && draggedItem && dropTarget;
+ }
+
+ function dragLeave(id) {
+ if (state === 2 && dropTarget && dropTarget._id === id) {
+ state = 1;
+ dropTarget = null;
+ targetIndex = -1;
+ }
+ }
+
+ function dragEnd() {
+ state = 0;
+ draggedItem = null;
+ dropTarget = null;
+ targetIndex = -1;
+ }
+
+ /**
+ * @param {Entity} entity the Entity
+ */
+ function dragEnter(entity, index = -1) {
+ if (state === 1) {
+ state = 2;
+ }
+ dropTarget = entity;
+ targetIndex = index;
+ }
+
+ /**
+ * @param item
+ */
+ function dragStart(item) {
+ state = 1;
+ if (!item) {
+ throw new Error('"item" param must be specified');
+ }
+ draggedItem = item;
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/brooklyn-ui/blob/29927b73/new/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.directive.js
----------------------------------------------------------------------
diff --git a/new/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.directive.js b/new/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.directive.js
new file mode 100644
index 0000000..7a581b7
--- /dev/null
+++ b/new/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.directive.js
@@ -0,0 +1,794 @@
+/*
+ * 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 onEnter from 'brooklyn-ui-utils/on-enter/index';
+import autoGrow from 'brooklyn-ui-utils/autogrow/index';
+import blurOnEnter from 'brooklyn-ui-utils/blur-on-enter/index';
+import {EntityFamily} from '../util/model/entity.model';
+import {Dsl} from '../util/model/dsl.model';
+import {Issue, ISSUE_LEVEL} from '../util/model/issue.model';
+import {RESERVED_KEYS, DSL_ENTITY_SPEC} from '../providers/blueprint-service.provider';
+import brooklynDslEditor from '../dsl-editor/dsl-editor';
+import brooklynDslViewer from '../dsl-viewer/dsl-viewer';
+import template from './spec-editor.template.html';
+
+const MODULE_NAME = 'brooklyn.components.spec-editor';
+const ANY_MEMBERSPEC_REGEX = /(^.*[m,M]ember[s,S]pec$)/;
+const REPLACED_DSL_ENTITYSPEC = '___brooklyn:entitySpec';
+
+angular.module(MODULE_NAME, [onEnter, autoGrow, blurOnEnter, brooklynDslEditor, brooklynDslViewer])
+ .directive('specEditor', ['$rootScope', '$filter', '$log', '$sce', '$timeout', '$document', 'blueprintService', specEditorDirective])
+ .filter('specEditorConfig', specEditorConfigFilter)
+ .filter('specEditorType', specEditorTypeFilter);
+
+export default MODULE_NAME;
+
+export const CONFIG_FILTERS = [
+ {
+ id: 'suggested',
+ label: 'Suggested',
+ filter: (item)=> {
+ return item.pinned && item.priority > -1;
+ }
+ },
+ {
+ id: 'required',
+ label: 'Required',
+ filter: (item)=> {
+ return item.constraints && item.constraints.required;
+ }
+ },
+ {
+ id: 'inuse',
+ label: 'In Use',
+ filter: (item, currentConfig)=> {
+ return currentConfig.has(item.name);
+ }
+ },
+ {
+ id: 'all',
+ label: 'All',
+ filter: (item)=> {
+ return item;
+ }
+ }
+];
+
+export function specEditorDirective($rootScope, $filter, $log, $sce, $timeout, $document, blueprintService) {
+ return {
+ restrict: 'E',
+ scope: {
+ model: '='
+ },
+ template: template,
+ link: link
+ };
+
+ function link(scope) {
+ scope.addConfigKey = addConfigKey;
+ scope.FAMILIES = EntityFamily;
+ scope.RESERVED_KEYS = RESERVED_KEYS;
+ scope.REPLACED_DSL_ENTITYSPEC = REPLACED_DSL_ENTITYSPEC;
+
+ let defaultState = {
+ config: {
+ add: {
+ value: '',
+ open: false
+ },
+ search: '',
+ focus: '',
+ filter: {
+ values: { suggested: true, required: true, inuse: true },
+ open: false,
+ },
+ // TODO would be nice to set null here, then have it go true if there is filtered config
+ // but the collapsible widget doesn't seem to respond to subsequent state changed
+ open: true,
+
+ dslViewerManualOverride: {},
+ codeModeActive: {},
+ codeModeForced: {},
+ codeModeError: {},
+ },
+ location: {
+ open: false
+ },
+ policy: {
+ search: '',
+ open: false
+ },
+ enricher: {
+ search: '',
+ open: false
+ }
+ };
+
+ scope.filters = {
+ config: CONFIG_FILTERS
+ };
+ scope.isFilterDisabled = (filter) => filter.id!='all' && scope.state.config.filter.values['all'];
+ scope.onFilterClicked = (filter) => {
+ if (!scope.isFilterDisabled(filter)) scope.state.config.filter.values[ filter.id ] = !scope.state.config.filter.values[ filter.id ];
+ };
+
+ scope.state = sessionStorage && sessionStorage.getItem(scope.model._id)
+ ? JSON.parse(sessionStorage.getItem(scope.model._id))
+ : defaultState;
+ scope.$watch('state', () => {
+ if (sessionStorage) {
+ try {
+ sessionStorage.setItem(scope.model._id, JSON.stringify(scope.state));
+ } catch (ex) {
+ $log.error('Cannot save edit state: ' + ex.message);
+ }
+ }
+ }, true);
+ scope.$watch('state.config.filter.values', ()=> {
+ scope.state.config.add.list = getAddListConfig();
+ });
+
+ scope.config = {};
+ scope.$watch('model', (newVal, oldVal)=> {
+ if (newVal && !newVal.equals(oldVal)) {
+ loadLocalConfigFromModel();
+ }
+ }, true);
+
+ scope.$watch('model.id', ()=> {
+ blueprintService.refreshAllRelationships();
+ });
+
+ // Config
+ scope.$watch('config', (newVal, oldVal)=> {
+ setModelFromLocalConfig();
+ scope.model.clearIssues({group: 'config'});
+ blueprintService.refreshRelationships(scope.model).then(() => {
+ return blueprintService.refreshConfigConstraints(scope.model);
+ }).then(() => {
+ refreshCustomValidation(scope.model);
+ });
+ }, true);
+
+ scope.getObjectSize = (object)=> {
+ return scope.defined(object) && object!=null ? Object.keys(object).length : 0;
+ };
+ function findNext(element, clazz, stopClazz) {
+ let el = element, last = null;
+ while (last!=el) {
+ last = el;
+ if (el.children().length) {
+ el = angular.element(el.children()[0]);
+ } else {
+ while (el.length && !el.next().length) {
+ el = el.parent();
+ }
+ el = el.next();
+ }
+ if (el.hasClass(clazz)) {
+ return el;
+ }
+ if (stopClazz && el.hasClass(stopClazz)) break;
+ }
+ return null;
+ }
+ function findPrev(element, clazz, stopClazz) {
+ let el = element, last = null;
+ while (last!=el) {
+ last = el;
+ if (el.children().length) {
+ // this still does children first, not ideal but okay for how we use it
+ el = angular.element(el.children()[el.children().length-1]);
+ } else {
+ while (el.length && !el[0].previousElementSibling) {
+ el = el.parent();
+ }
+ el = angular.element(el[0].previousElementSibling);
+ }
+ if (el.hasClass(clazz)) {
+ return el;
+ }
+ if (stopClazz && el.hasClass(stopClazz)) break;
+ }
+ return null;
+ }
+ function findAncestor(element, clazz, stopClazz) {
+ let el = element, last = null;
+ while (last!=el) {
+ last = el;
+ el = el.parent();
+ if (el.hasClass(clazz)) {
+ return el;
+ }
+ if (stopClazz && el.hasClass(stopClazz)) return null;
+ }
+ return null;
+ }
+ function focusIfPossible(event, next) {
+ if (next && next.length) {
+ if (event) event.preventDefault();
+ if (event) event.stopPropagation();
+ next[0].focus();
+ return true;
+ } else {
+ return false;
+ }
+ }
+ scope.nonempty = (o) => o && Object.keys(o).length;
+ scope.defined = (o) => (typeof o !== 'undefined');
+ scope.advanceOutToFormGroupInPanel = (element, event) => {
+ focusIfPossible(event, findAncestor(element, "form-group", "panel-body")) || element[0].blur();
+ };
+ scope.advanceControlInFormGroup = (element, event) => {
+ focusIfPossible(event, findNext(element, "form-control", "form-group")) ||
+ scope.advanceOutToFormGroupInPanel(element, event);
+ };
+
+ scope.onAddMapProperty = (configKey, key, ev)=> {
+ if (key) {
+ if (!scope.config[configKey]) {
+ scope.config[configKey] = {};
+ }
+ if (!scope.config[configKey].hasOwnProperty(key)) {
+ scope.config[configKey][key] = '';
+
+ // defer so control exists, then focus on previous item (new control)
+ $timeout(() => {
+ let element = angular.element(ev.target);
+ let prev = findPrev(element, "form-control", "form-group");
+ focusIfPossible(null, prev) ||
+ scope.advanceOutToFormGroupInPanel(element, null);
+ }, 0);
+ } else {
+ // user entered a key that already exists;
+ // would be nice to select the item they requested but with just jqlite that's more pain than it's worth!
+ }
+ }
+ };
+ scope.cycleExpandMode = function (expandMode, ctx, item, focus) {
+ return expandMode == 'default' ? 'open' :
+ expandMode == 'open' ? 'closed' :
+ 'default';
+ }
+ scope.onDeleteMapProperty = function (model, key) {
+ if (model && model.hasOwnProperty(key)) {
+ delete model[key];
+ }
+ };
+ scope.onAddListItem = (configKey, item, ev)=> {
+ if (item) {
+ if (!scope.config[configKey]) {
+ scope.config[configKey] = [];
+ }
+ if (scope.config[configKey].indexOf(item) === -1) {
+ scope.config[configKey].push(item);
+ }
+ ev.target.focus();
+ }
+ };
+ scope.onDeleteListItem = function (model, index) {
+ if (model && index < model.length) {
+ model.splice(index, 1);
+ }
+ };
+ scope.isConfigHidden = (config)=> {
+ let allConfig = scope.model.miscData.get('config');
+ if (allConfig.indexOf(config) === -1) {
+ return false;
+ }
+ return $filter('specEditorConfig')(allConfig, scope.state.config.filter.values).indexOf(config) === -1;
+ };
+ scope.onFocusOnConfig = ($item)=> {
+ scope.state.config.search = '';
+ scope.state.config.add.value = '';
+ scope.state.config.add.open = false;
+ scope.state.config.focus = $item.name;
+ if ($item.isHidden) {
+ scope.state.config.filter.values[ CONFIG_FILTERS[CONFIG_FILTERS.length - 1] ].id = true;
+ }
+ };
+ scope.recordFocus = ($item)=> {
+ scope.state.config.focus = $item.name;
+ };
+
+ scope.removeAdjunct = ($event, adjunct)=> {
+ $event.preventDefault();
+ $event.stopPropagation();
+ switch(adjunct.family) {
+ case EntityFamily.POLICY:
+ scope.model.removePolicy(adjunct._id);
+ break;
+ case EntityFamily.ENRICHER:
+ scope.model.removeEnricher(adjunct._id);
+ break;
+ }
+ };
+
+ scope.removeEntity = ()=> {
+ $rootScope.$broadcast('d3.remove', scope.model);
+ };
+
+ scope.getConfigIssues = ()=> {
+ return scope.model.issues
+ .filter((issue)=>(issue.group === 'config'))
+ .concat(Object.values(scope.model.getClusterMemberspecEntities())
+ .filter((spec)=>(spec && spec.hasIssues()))
+ .reduce((acc, spec)=>(acc.concat(spec.issues)), []));
+ };
+
+ scope.getPoliciesIssues = ()=> {
+ return scope.model.getPoliciesAsArray().reduce((acc, policy)=> {
+ if (policy.hasIssues()) {
+ acc = acc.concat(policy.issues)
+ }
+ return acc;
+ }, []);
+ };
+
+ scope.getEnrichersIssues = ()=> {
+ return scope.model.getEnrichersAsArray().reduce((acc, enricher)=> {
+ if (enricher.hasIssues()) {
+ acc = acc.concat(enricher.issues)
+ }
+ return acc;
+ }, []);
+ };
+
+ scope.getBadgeClass = (issues) => {
+ return issues.some(issue => issue.level === ISSUE_LEVEL.ERROR) ? 'badge-danger' : 'badge-warning';
+ };
+
+ function getConfigWidgetModeInternal(item, val) {
+ if (angular.element($document[0].activeElement).hasClass("form-control") && item.widgetMode) {
+ // don't switch mode in mid-edit, e.g. if you are manually typing $brooklyn:component("x").config("y")
+ // don't interrupt when the user has typed $brooklyn:component("x")!
+ return item.widgetMode;
+ }
+
+ if (scope.isDsl(item.name)) {
+ return scope.state.config.dslViewerManualOverride && scope.state.config.dslViewerManualOverride[item.name] ?
+ "dsl-manual" // causes default string editor
+ : "dsl-viewer";
+ }
+
+ if (!scope.defined(val)) val = scope.config[item.name];
+ let type = item.type;
+
+ // if actual value's type does not match declared type,
+ // e.g. object is a map when declared type is object or string or something else,
+ // we could render e.g. w map editor as a convenience;
+ // but for now the logic is only based on the declared type
+
+ if (type === 'java.lang.Boolean') type = 'boolean';
+ else if (type === 'java.util.Map') type = 'map';
+ else if (type === 'java.util.Set' || type === 'java.util.List' || type === 'java.util.Collection') type = 'array';
+
+ if (scope.defined(val)) {
+ // override to use string editor if the editor doesn't support the value
+ // (probably this is an error, though type-coercion might make it not so)
+ if (type === 'boolean') {
+ if (typeof val !== type) return type+'-manual'; // causes default string editor
+ } else if (type === 'map') {
+ if (typeof val !== 'object') return type+'-manual'; // causes default string editor
+ } else if (type === 'array') {
+ if (!Array.isArray(val)) return type+'-manual'; // causes default string editor
+ }
+ }
+ if (scope.state.config.codeModeActive[item.name]) {
+ // code mode forces manual editor
+ return type+'-manual';
+ }
+
+ return type;
+ };
+ scope.getConfigWidgetMode = (item, value) => {
+ // record the value as `item.widgetMode` so we can reference it subsequently, as well as returning it
+
+ if (item.type) {
+ return item.widgetMode = getConfigWidgetModeInternal(item, value);
+ }
+
+ // if type isn't set then infer
+ if (value instanceof Array) {
+ definition.widgetMode = 'array';
+ } else if (value instanceof Object) {
+ definition.widgetMode = 'map';
+ } else {
+ definition.widgetMode = 'unknown';
+ }
+ }
+ scope.isCodeModeAvailable = (item) => {
+ let val = scope.config[item.name];
+
+ if (scope.state.config.codeModeActive[item.name]) {
+ // simple check to update whether it is forced when active -- if it starts with { or [
+ scope.state.config.codeModeForced[item.name] = (typeof val === 'string' && val.trim().match(/^[{\[]/));
+ // always available if user has set it active, and poor-man's forced check above should apply
+ return true;
+ }
+ // else don't force unless the parse confirms it
+ scope.state.config.codeModeForced[item.name] = false;
+ // otherwise true if the text entered is json
+ if (!scope.defined(val)) return false;
+ if (typeof val == 'string') {
+ try {
+ // a YAML parse would be nicer, but JSON is simpler, and sufficient
+ // (esp as we export a JSON stringify; preferable would be to export the exact YAML from model,
+ // including comments, but again lower priority)
+ let parsed = JSON.parse(val);
+ // if parse returns a string or a complex object, then it looks like JSON was entered
+ // (e.g. "foo" or {a:1}); if not, eg user entered boolean or number (45 or true),
+ // then don't offer code mode; if it was complex, additionally force code mode
+ scope.state.config.codeModeForced[item.name] = typeof parsed === 'object' || typeof parsed === 'array';
+ return (scope.state.config.codeModeForced[item.name] || typeof parsed === 'string');
+ } catch (ex) {
+ // not valid text input for code mode
+ // if "codeModeForced" with poor-man's check was enabled we should disable that
+ scope.state.config.codeModeForced[item.name] = false;
+ return false;
+ }
+ }
+ if (val instanceof Dsl) return false;
+ if (val instanceof Array || val instanceof Object) return true;
+ // other primitive
+ return false;
+ }
+ scope.codeModeClick = (item) => {
+ if (scope.state.config.codeModeForced[item.name] && scope.state.config.codeModeActive[item.name]) {
+ // if forced and active, don't allow clicks
+ return;
+ }
+ let oldMode = !!(scope.state.config.codeModeActive[item.name]);
+ // convert local config from json to non-json or vice-versa
+ let value = scope.config[item.name];
+ if (!scope.defined(value)) {
+ value = null;
+ }
+ if (value!=null) {
+ if (oldMode) {
+ // leaving code mode
+ try {
+ // if it's a parseable string, then parse it
+ if (typeof value === 'string' && value.length) {
+ value = JSON.parse(value);
+ if (value instanceof Array || value instanceof Object) {
+ // if result is not a primitive then don't allow user to leave code mode
+ // (if type is known as a map/list then likely we should allow leaving code mode,
+ // if we check and confirm that all entries are json primitives, but that can be left as a TODO;
+ // only would enter that state if user clicked to enter code for a map/list,
+ // and they can reset by switching to yaml)
+ $log.warn("JSON code is not a primitive; leaving as code mode:", item.name, value);
+ return;
+ }
+ // parsed value is a primitive, make that be the value shown
+ } else {
+ // if type is not a string, then show unchanged value
+ // (would only come here on weird changes)
+ value = null;
+ }
+ } catch (notJson) {
+ // if not parseable then leave alone
+ value = null;
+ }
+ } else {
+ // entering code mode
+ try {
+ if (typeof value === 'string') {
+ if (value.length) {
+ JSON.parse(value);
+ // if it parses, treat as a json object
+ } else {
+ // if empty, leave empty
+ }
+ } else {
+ // if not a string then treat as not parseable
+ throw 'not json';
+ }
+ } catch (notJson) {
+ // if not parseable or not a string, then convert to json
+ value = JSON.stringify(value);
+ }
+ }
+ if (value!=null) {
+ scope.config[item.name] = value;
+ }
+ }
+ scope.state.config.codeModeActive[item.name] = !oldMode;
+ if (value!=null) {
+ // local config changed, make sure model is updated too
+ setModelFromLocalConfig();
+ }
+ }
+ scope.getJsonModeTitle = (itemName) => {
+ if (!scope.state.config.codeModeActive[itemName]) {
+ return "Treat this value as a JSON-encoded object ["+itemName+"]";
+ }
+ if (scope.state.config.codeModeForced[itemName]) {
+ return "This data is a complex object and can only be entered as JSON ["+itemName+"]";
+ } else {
+ return "Edit in simple mode, unwrapping JSON if possible ["+itemName+"]";
+ }
+ }
+
+ scope.isDsl = (key, index) => {
+ let val = scope.model.config.get(key);
+ if (scope.defined(val) && scope.defined(index) && index!=null) val = val[index];
+ return scope.isDslVal(val);
+ };
+ scope.isDslVal = (val) => {
+ // don't treat constants as DSL (not helpful, and the DSL editor doesn't support it)
+ return (val instanceof Dsl) && val.kind && val.kind.family != 'constant';
+ };
+ scope.isDslWizardButtonAllowed = (key, index) => {
+ let val = scope.model.config.get(key);
+ if (scope.defined(val) && scope.defined(index) && index!=null) val = val[index];
+ if (!scope.defined(val) || val===null || val==='') return true;
+ if (scope.isDslVal(val)) {
+ return true;
+ }
+ // non-DSL values cannot be opened in DSL editor
+ return false;
+ };
+
+ function getAddListConfig() {
+ if (!angular.isArray(scope.model.miscData.get('config'))) {
+ return [];
+ }
+ let filteredConfig = $filter('specEditorConfig')(scope.model.miscData.get('config'), scope.state.config.filter.values, scope.model.config);
+ return scope.model.miscData.get('config').map((config)=> {
+ config.isHidden = scope.model.miscData.get('config').indexOf(config) > -1 ? filteredConfig.indexOf(config) === -1 : false;
+ return config;
+ });
+ }
+
+ /* config state for each item is stored in multiple places:
+ * * scope.config = map of values used/set by editor (strings, map of strings, json code if using code mode, etc);
+ * this should be suitable for ng-model to work with, so e.g. if using code mode we need to put JSON.stringify value in here,
+ * and note any change here immediately (on next digest) updates scope.model.config, which e.g. in code mode
+ * will JSON.parse
+ * * scope.model.config = map of values used in internal model
+ * * scope.model.miscData.get('config') = list of config keys with their metadata, including derived widgetMode
+ * * scope.state.config.{codeModeActive,dslManualOverride} = maps of booleans where edit modes are set and remembered for configs
+ */
+
+ function loadLocalConfigFromModel() {
+ let map = scope.model.config;
+ let result = {};
+ for (let [key, value] of map) {
+ if (blueprintService.isReservedKey(key)) {
+ // skip
+ continue;
+ }
+
+ result[key] = getLocalConfigValueFromModelValue(key, value);
+ }
+ scope.config = result;
+ }
+
+ function getLocalConfigValueFromModelValue(key, value) {
+ if (!scope.defined(value) || value==null) {
+ return value;
+ }
+
+ if (ANY_MEMBERSPEC_REGEX.test(key) && value.hasOwnProperty(DSL_ENTITY_SPEC)) {
+ let wrapper = {};
+ wrapper[REPLACED_DSL_ENTITYSPEC] = value[DSL_ENTITY_SPEC];
+ return wrapper;
+ }
+ if (value instanceof Dsl) {
+ return value.toString();
+ }
+
+ let definition = scope.model.miscData.get('config').find(config => config.name === key);
+ if (!definition) {
+ // odd, no def'n for this key
+ definition = {};
+ scope.getConfigWidgetMode(definition, value)
+ } else {
+ if (!definition.widgetMode) scope.getConfigWidgetMode(definition);
+ }
+
+ // if json mode, then just stringify
+ if (scope.state.config.codeModeActive[key]) {
+ if (scope.state.config.codeModeError[key]) {
+ // errors don't get stringified because model value was not parsed
+ return value;
+ }
+ return JSON.stringify(value);
+ }
+
+ // else treat as value, with array/map special
+
+ try {
+ if (definition.widgetMode === 'array') {
+ return value.map(item => {
+ if (item instanceof Dsl) {
+ return item.toString();
+ } else if (item instanceof Array || item instanceof Object) {
+ throw 'not simple json in array';
+ } else {
+ return item;
+ }
+ });
+ } else if (definition.widgetMode === 'map') {
+ let object = {};
+ for (let keyObject in value) {
+ if (value[keyObject] instanceof Dsl) {
+ object[keyObject] = value[keyObject].toString();
+ } else if (value[keyObject] instanceof Array || value[keyObject] instanceof Object) {
+ throw 'not simple json in map';
+ } else {
+ object[keyObject] = value[keyObject];
+ }
+ }
+ return object;
+ } else if (!(value instanceof Dsl) && (value instanceof Object || value instanceof Array)) {
+ throw 'must use code editor';
+ }
+ } catch (hasComplexJson) {
+ // any map/array with complex json inside, or other value of complex json,
+ // will force the code editor
+ // (previously we did stringify on entries then tried to parse, but that was inconsistent)
+ scope.state.config.codeModeActive[key] = true;
+ // and the widget mode updated to be 'manual'
+ scope.getConfigWidgetMode(definition, value);
+
+ return JSON.stringify(value);
+ }
+
+ // if boolean, return as primitive type
+ if (typeof value === 'boolean') {
+ return value;
+ }
+
+ // all other primitives treat as string (as they will get a string-based widget)
+ return ""+value;
+ }
+
+ function getModelValueFromString(val) {
+ if (!scope.defined(val) || val==null || typeof val !== 'string') {
+ // only strings will have primitive inference applied
+ // (and this is only invoked when not in code mode)
+ return val;
+ }
+ try {
+ let prim = JSON.parse(val);
+ if (prim instanceof Object || prim instanceof Array || typeof prim === 'string') {
+ throw 'not primitive';
+ }
+ if (val.match(/^[0-9]*\.([0-9]*0)?$/)) {
+ // if user enters "3.0" or "1." we should treat it as a string to prevent
+ // yaml editor from simplifying it to be "3" or "1".
+ throw 'decimal ending with dot or 0, eg version, treat as string';
+ }
+ // numbers and booleans are unquoted
+ return prim;
+ } catch (ex) {
+ return val;
+ }
+ }
+
+ function setModelFromLocalConfig() {
+ let localConfig = scope.config;
+ let result = {};
+ for (let keyRef in localConfig) {
+ if (angular.isUndefined(localConfig[keyRef]) || localConfig[keyRef] === null || localConfig[keyRef].length < 1) {
+ continue;
+ }
+ if (ANY_MEMBERSPEC_REGEX.test(keyRef) && localConfig[keyRef].hasOwnProperty(REPLACED_DSL_ENTITYSPEC)) {
+ result[keyRef] = {};
+ result[keyRef][DSL_ENTITY_SPEC] = localConfig[keyRef][REPLACED_DSL_ENTITYSPEC];
+ continue;
+ }
+
+ let definition = scope.model.miscData.get('config').find(config => config.name === keyRef);
+ if (!definition) {
+ // odd, no def'n for this key; infer
+ definition = {};
+ scope.getConfigWidgetMode(definition, localConfig[keyRef])
+ }
+
+ // if JSON mode then parse
+ scope.state.config.codeModeError[keyRef] = null;
+ if (scope.state.config.codeModeActive && scope.state.config.codeModeActive[keyRef]) {
+ try {
+ result[keyRef] = JSON.parse(localConfig[keyRef]);
+ } catch (ex) {
+ scope.state.config.codeModeError[keyRef] = "Invalid JSON";
+ result[keyRef] = localConfig[keyRef];
+ }
+ continue;
+ }
+
+ // else return as is, or introspect for array/map
+
+ if (definition.widgetMode === 'array') {
+ result[keyRef] = localConfig[keyRef].map(getModelValueFromString);
+ continue;
+ }
+ if (definition.widgetMode === 'map') {
+ result[keyRef] = {};
+ for (let keyObject in localConfig[keyRef]) {
+ result[keyRef][keyObject] = getModelValueFromString(localConfig[keyRef][keyObject]);
+ }
+ continue;
+ }
+
+ result[keyRef] = getModelValueFromString(localConfig[keyRef]);
+ }
+
+ scope.model.setConfigFromJson(result);
+ }
+
+ function addConfigKey() {
+ let name = scope.state.config.add.value;
+ if (name) {
+ let allConfig = scope.model.miscData.get('config');
+ blueprintService.addConfigKeyDefinition(allConfig, name);
+ scope.model.addConfig(name, '');
+ loadLocalConfigFromModel();
+ scope.state.config.add.value = '';
+ scope.state.config.add.open = false;
+ scope.state.config.filter.values[ CONFIG_FILTERS[CONFIG_FILTERS.length - 1].id ] = true;
+ scope.state.config.focus = name;
+ }
+ }
+
+ function refreshCustomValidation(model) {
+ model.config.forEach((value, key) => {
+ let config = model.miscData.get('config').find(config => config.name === key);
+ // If we need an integer, check if the value is a number
+ if (config.type === 'java.lang.Integer' && !angular.isNumber(value) && !(value instanceof Dsl)) {
+ model.addIssue(Issue.builder().group('config').ref(key).message($sce.trustAsHtml(`<code>${value}</code> is not a number`)).build());
+ }
+ if (scope.state.config.codeModeError[key]) {
+ model.addIssue(Issue.builder().group('config').ref(key).message($sce.trustAsHtml(`<code>${value}</code> is not valid JSON`)).build());
+ }
+ });
+ }
+ }
+}
+
+export function specEditorConfigFilter() {
+ return function (input, filtersMapById, currentConfig) {
+ let filters = [];
+ Object.keys(filtersMapById).forEach( (k) => { if (filtersMapById[k]) filters.push(k); } );
+
+ if (!filters.every(filterId => CONFIG_FILTERS.some(filter => filter.id === filterId))) {
+ return input;
+ }
+ if (!(input instanceof Array)) {
+ return input;
+ }
+ return input.filter((item)=> {
+ return filters.some(filterId => CONFIG_FILTERS.find(filter => filter.id === filterId).filter(item, currentConfig));
+ });
+ }
+}
+
+export function specEditorTypeFilter() {
+ return function (input, search) {
+ if (search.length < 1) {
+ return input;
+ }
+ return input.filter((item)=> {
+ return item.miscData.get('typeName').indexOf(search) > -1 || item.type.indexOf(search) > -1;
+ });
+ }
+}