Merge pull request #125 from ahgittin/fix-locations

Fix errors around locations in composer
diff --git a/ui-modules/blueprint-composer/app/components/providers/blueprint-service.provider.js b/ui-modules/blueprint-composer/app/components/providers/blueprint-service.provider.js
index 38604a3..7808a98 100644
--- a/ui-modules/blueprint-composer/app/components/providers/blueprint-service.provider.js
+++ b/ui-modules/blueprint-composer/app/components/providers/blueprint-service.provider.js
@@ -265,15 +265,28 @@
         return deferred.promise;
     }
 
+    function locationType(location) {
+        if (!location || typeof location === 'string') return location;
+        if (typeof location === 'object' && Object.keys(location).length==1) return Object.keys(location)[0];
+        return null;
+    }
+    
     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));
-            });
+            let type = locationType(entity.location);
+            if (type.startsWith("jclouds:")) {
+                // types eg jclouds:aws-ec2 are low-level, not in the catalog
+                deferred.resolve(populateLocationFromApiSuccess(entity, { yamlHere: entity.location }));
+            } else {
+                paletteApi.getLocation(locationType(entity.location)).then((location)=> {
+                    let loc = Object.assign({}, location.catalog || location, { yamlHere: entity.location });
+                    deferred.resolve(populateLocationFromApiSuccess(entity, loc));
+                }).catch(function () {
+                    deferred.resolve(populateLocationFromApiError(entity));
+                });
+            }
         } else {
             deferred.resolve(entity);
         }
@@ -601,18 +614,30 @@
         return entity;
     }
 
-    function populateLocationFromApiSuccess(entity, data) {
+    function populateLocationFromApiCommon(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));
+        entity.location = data.yamlHere || data.symbolicName;
+        
+        let name = data.name || data.displayName;
+        if (!name && data.yamlHere) {
+            name = typeof data.yamlHere === 'object' ? Object.keys(data.yamlHere)[0] : data.yamlHere;
+        }
+        if (!name) name =  data.symbolicName;
+        entity.miscData.set('locationName', name);
+        
+        // use icon on item, but if none then generate using *yaml* to distinguish when someone has changed it
+        // (especially for things like jclouds:aws-ec2 -- the config is more interesting than the type name)
+        entity.miscData.set('locationIcon', data==null ? null : data.iconUrl || iconGenerator(data.yamlHere ? JSON.stringify(data.yamlHere) : data.symbolicName));
         return entity;
     }
+    
+    function populateLocationFromApiSuccess(entity, data) {
+        populateLocationFromApiCommon(entity, data);
+    }
 
     function populateLocationFromApiError(entity) {
-        entity.clearIssues({group: 'location'});
+        populateLocationFromApiCommon(entity, { yamlHere: entity.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;
     }
diff --git a/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.directive.js b/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.directive.js
index 475952c..95c59e4 100644
--- a/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.directive.js
+++ b/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.directive.js
@@ -254,6 +254,8 @@
         }
         scope.nonempty = (o) => o && Object.keys(o).length;
         scope.defined = specEditor.defined = (o) => (typeof o !== 'undefined');
+        specEditor.isInstance = (x, type) => (typeof x === type);
+                
         specEditor.advanceOutToFormGroupInPanel = (element, event) => {
             focusIfPossible(event, findAncestor(element, "form-group", "panel-body")) || element[0].blur();
         };
diff --git a/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.less b/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.less
index 591e046..15dd9d3 100644
--- a/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.less
+++ b/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.less
@@ -22,9 +22,6 @@
 @hide-info-button-when-not-hovered: false;
 @hide-unset-undefault-values: true;
 
-@label-gray: darken(@gray-light, 10%);
-@gray-lightest: #f8f8f9;
-
 spec-editor {
   display: block;
   margin-top: 15px;
diff --git a/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.template.html b/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.template.html
index 080c680..fe00172 100644
--- a/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.template.html
+++ b/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.template.html
@@ -366,7 +366,12 @@
             <p ng-repeat="issue in state.issues | filter:{group:'location'}" class="alert alert-{{issue.severity}}">
                 <em ng-bind-html="issue.message"></em>
             </p>
-            <p>Will be deployed to: <strong>{{model.miscData.get('locationName')}}</strong></p>
+            <p>
+                Targeted at: 
+                <strong ng-if="specEditor.isInstance(model.location, 'string')">{{ model.miscData.get('locationName') }}</strong>
+                <pre ng-if="!specEditor.isInstance(model.location, 'string')">{{ model.location | json }}</pre>
+            </p>
+            <br/>
             <a class="btn btn-default" ui-sref="main.graphical.edit.add({entityId: model._id, family: 'location'})">Change location</a>
             <button class="btn btn-danger btn-link" ng-click="model.clearIssues({group: 'location'}).removeLocation()">Remove</button>
         </div>
diff --git a/ui-modules/blueprint-composer/app/components/util/d3-blueprint.js b/ui-modules/blueprint-composer/app/components/util/d3-blueprint.js
index 0e2d9d0..b199ab0 100755
--- a/ui-modules/blueprint-composer/app/components/util/d3-blueprint.js
+++ b/ui-modules/blueprint-composer/app/components/util/d3-blueprint.js
@@ -618,11 +618,13 @@
             .duration(_configHolder.transition)
             .attr('opacity', (d)=>(d.data.hasLocation() ? 1 : 0));
         appendElements(location, _configHolder.nodes.location);
+        
         nodeData.select('g.node-location image')
             .transition()
             .duration(_configHolder.transition)
-            .attr('opacity', (d)=>(d.data.miscData.get('locationIcon') ? 1 : 0))
-            .attr('xlink:href', (d)=>(d.data.miscData.get('locationIcon')));
+            .attr('opacity', (d)=>(d.data.miscData.get('locationIcon') ? 1 : 0));
+        nodeData.select('g.node-location image')
+            .attr('xlink:href', (d)=>d.data.miscData.get('locationIcon'));
 
         // Draw important adjuncts (i.e policies/enrichers)
         // -----------------------------------------------------
diff --git a/ui-modules/blueprint-composer/app/components/util/model/entity.model.js b/ui-modules/blueprint-composer/app/components/util/model/entity.model.js
index 603eaa9..6f4ad40 100644
--- a/ui-modules/blueprint-composer/app/components/util/model/entity.model.js
+++ b/ui-modules/blueprint-composer/app/components/util/model/entity.model.js
@@ -272,6 +272,7 @@
      */
     set location(location) {
         LOCATIONS.set(this, location);
+        this.miscData.delete('locationRemoved');
         this.touch();
     }
 
@@ -281,6 +282,13 @@
      */
     removeLocation() {
         LOCATIONS.delete(this);
+        this.miscData.delete('locationName');
+        this.miscData.delete('locationIcon');
+        
+        // this field provides a way for consumers to detect if the location was explicitly removed;
+        // this can be useful to prevent default locations from being applied
+        this.miscData.set('locationRemoved', true);
+        
         this.touch();
     }
 
diff --git a/ui-modules/blueprint-composer/app/index.less b/ui-modules/blueprint-composer/app/index.less
index 1562b40..0ab3f4d 100644
--- a/ui-modules/blueprint-composer/app/index.less
+++ b/ui-modules/blueprint-composer/app/index.less
@@ -36,9 +36,6 @@
 @import "components/catalog-saver/catalog-saver.less";
 @import "components/dsl-editor/dsl-editor";
 
-@gray-lighter: #E1E5E7;
-@gray-light: #818899;
-
 .make-icon(@size) {
   width: auto;
   height: auto;
diff --git a/ui-modules/utils/br-core/style/buttons.less b/ui-modules/utils/br-core/style/buttons.less
index e87dbc3..baefe44 100644
--- a/ui-modules/utils/br-core/style/buttons.less
+++ b/ui-modules/utils/br-core/style/buttons.less
@@ -23,6 +23,9 @@
 .btn-accent {
     .button-variant(@btn-accent-color; @btn-accent-bg; @btn-accent-border);
 }
+.btn-light {
+  .button-variant(@gray-dark; @gray-lightest; @gray-lightest);
+}
 
 .btn-outline {
     &.btn-primary:not(:hover) {
@@ -49,4 +52,9 @@
         background-color: transparent;
         color: @brand-danger;
     }
+    
+    &.btn-light:not(:hover) {
+        background-color: transparent;
+        color: @gray-lightest;
+    }
 }
diff --git a/ui-modules/utils/br-core/style/variables.less b/ui-modules/utils/br-core/style/variables.less
index efe673a..439ee28 100644
--- a/ui-modules/utils/br-core/style/variables.less
+++ b/ui-modules/utils/br-core/style/variables.less
@@ -21,18 +21,18 @@
 @brand-accent: #bf3727;
 
 
-// Create path variables for fonts and images
-@br-core-path-font: '../fonts';
-@br-core-path-img: '../img';
-// Override the font-awesome path to use our custom one
-@fa-font-path: @br-core-path-font;
+// bootstrap colours at
+// https://github.com/twbs/bootstrap-sass/blob/master/assets/stylesheets/bootstrap/_variables.scss
 
+@gray-light: #818899;   // override bootstrap default of #777
+@gray-lighter: #E1E5E7; // override bootstrap default of #eee
+@gray-lightest: #f8f8f9;
+@label-gray: darken(@gray-light, 10%);  // between @gray and @gray-light
 
-@body-bg: hsl(223,30%,97%);
+@body-bg: hsl(223,30%,97%);  // override bootstrap default, #333, same as @gray-dark, used for the button bar, body default bg colour from bootstrap scaffolding
+
 @card-border-color: mix(black, @body-bg, 7%);
 
-@font-family-sans-serif: "myriad-pro-1", Helvetica, Arial, sans-serif;
-
 /* Colors in pattern lab */
 .brand-primary {
   background-color: @brand-primary;
@@ -54,3 +54,13 @@
 @state-accent-text:             @accent-500;
 @state-accent-bg:               @accent-30;
 @state-accent-border:           darken(spin(@state-accent-bg, -10), 5%);
+
+
+// Create path variables for fonts and images
+
+@br-core-path-font: '../fonts';
+@br-core-path-img: '../img';
+// Override the font-awesome path to use our custom one
+@fa-font-path: @br-core-path-font;
+
+@font-family-sans-serif: "myriad-pro-1", Helvetica, Arial, sans-serif;
diff --git a/ui-modules/utils/yaml-editor/addon/schemas/blueprint.json b/ui-modules/utils/yaml-editor/addon/schemas/blueprint.json
index bbf118f..fcc8ebc 100644
--- a/ui-modules/utils/yaml-editor/addon/schemas/blueprint.json
+++ b/ui-modules/utils/yaml-editor/addon/schemas/blueprint.json
@@ -22,7 +22,6 @@
   "title": "Blueprint",
   "description": "A blueprint that is composed of one or more entities",
   "type": "object",
-  "required": [ "services" ],
   "properties": {
     "name": {
       "title": "Blueprint name",