Merge pull request #165 from nakomis/fix-build

Bumps npm, node, and webpack versions
diff --git a/Jenkinsfile b/Jenkinsfile
index 05aca28..7fe54bb 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -18,6 +18,14 @@
  */
 
 node(label: 'ubuntu') {
+    if (env.CHANGE_ID) {
+        properties([
+            pipelineTriggers([
+                issueCommentTrigger('.*test this please.*')
+            ])
+        ])
+    }
+
     catchError {
         def environmentDockerImage
 
@@ -36,21 +44,10 @@
             }
 
             stage('Run tests') {
-                environmentDockerImage.inside('-i --name brooklyn-${DOCKER_TAG} --mount type=bind,source="${HOME}/.m2/settings.xml",target=/var/maven/.m2/settings.xml,readonly -v ${WORKSPACE}:/usr/build -w /usr/build') {
+                environmentDockerImage.inside('-i --name brooklyn-${DOCKER_TAG} -v ${WORKSPACE}/.m2:/var/maven/.m2 --mount type=bind,source="${HOME}/.m2/settings.xml",target=/var/maven/.m2/settings.xml,readonly -v ${WORKSPACE}:/usr/build -w /usr/build') {
                     sh 'mvn clean install -Duser.home=/var/maven -Duser.name=jenkins'
                 }
             }
-
-            // Conditional stage to deploy artifacts, when not building a PR
-            if (env.CHANGE_ID == null) {
-                stage('Deploy artifacts') {
-                    environmentDockerImage.inside('-i --name brooklyn-${DOCKER_TAG} --mount type=bind,source="${HOME}/.m2/settings.xml",target=/var/maven/.m2/settings.xml,readonly -v ${WORKSPACE}:/usr/build -w /usr/build') {
-                        sh 'mvn deploy -DskipTests -Duser.home=/var/maven -Duser.name=jenkins'
-                    }
-                }
-
-                // TODO: Publish docker image to https://hub.docker.com/r/apache/brooklyn/ ?
-            }
         }
     }
 
diff --git a/pom.xml b/pom.xml
index f214890..538b55f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -25,8 +25,7 @@
         <groupId>org.apache.brooklyn</groupId>
         <artifactId>brooklyn-parent</artifactId>
         <version>1.0.0-SNAPSHOT</version>  <!-- BROOKLYN_VERSION -->
-        <!-- do not link to sibling directory as that breaks builds where this code is embedded, e.g. for branding;
-             we require that brooklyn-server is built first (this is needed anyway for the modularity java code) -->
+        <relativePath>${brooklyn.ui.relativePath.to.brooklyn.server.parent}</relativePath>
     </parent>
 
     <groupId>org.apache.brooklyn.ui</groupId>
@@ -85,6 +84,8 @@
         <brooklyn.version>1.0.0-SNAPSHOT</brooklyn.version><!-- BROOKLYN_VERSION -->
         <build.version>${revision}</build.version>
         <build.name>Apache Brooklyn</build.name>
+        <brooklyn.ui.relativePath.to.brooklyn.server.parent>../brooklyn-server/parent/</brooklyn.ui.relativePath.to.brooklyn.server.parent>
+
         <buildnumber-maven-plugin.version>1.4</buildnumber-maven-plugin.version>
 
         <!-- versions from brooklyn server which have a different var name here -->
@@ -98,7 +99,7 @@
         <maven-compiler-plugin.version>3.5.1</maven-compiler-plugin.version>
         <maven-resources-plugin.version>3.0.1</maven-resources-plugin.version>
         <maven-war-plugin.version>3.0.0</maven-war-plugin.version>
-        <frontend-maven-plugin.version>1.3</frontend-maven-plugin.version>
+        <frontend-maven-plugin.version>1.9.0</frontend-maven-plugin.version>
         <pax-web.version>7.2.3</pax-web.version>
         <pax-web-extender-whiteboard.version>${pax-web.version}</pax-web-extender-whiteboard.version>
 
diff --git a/ui-modules/app-inspector/app/components/entity-tree/entity-tree.directive.js b/ui-modules/app-inspector/app/components/entity-tree/entity-tree.directive.js
index 76183e7..f6d3426 100644
--- a/ui-modules/app-inspector/app/components/entity-tree/entity-tree.directive.js
+++ b/ui-modules/app-inspector/app/components/entity-tree/entity-tree.directive.js
@@ -43,11 +43,14 @@
     return {
         restrict: 'E',
         template: entityTreeTemplate,
-        controller: ['$scope', '$state', 'applicationApi', 'iconService', 'brWebNotifications', controller],
+        scope: {
+           sortReverse: '=',
+        },
+        controller: ['$scope', '$state', 'applicationApi', 'entityApi', 'iconService', 'brWebNotifications', controller],
         controllerAs: 'vm'
     };
 
-    function controller($scope, $state, applicationApi, iconService, brWebNotifications) {
+    function controller($scope, $state, applicationApi, entityApi, iconService, brWebNotifications) {
         $scope.$emit(HIDE_INTERSTITIAL_SPINNER_EVENT);
 
         let vm = this;
diff --git a/ui-modules/app-inspector/app/components/entity-tree/entity-tree.html b/ui-modules/app-inspector/app/components/entity-tree/entity-tree.html
index a6c1a01..67c0ce1 100644
--- a/ui-modules/app-inspector/app/components/entity-tree/entity-tree.html
+++ b/ui-modules/app-inspector/app/components/entity-tree/entity-tree.html
@@ -16,7 +16,7 @@
   specific language governing permissions and limitations
   under the License.
 -->
-<entity-node ng-repeat="application in vm.applications track by application.id" entity="application" application-id="application.id"></entity-node>
+<entity-node ng-repeat="application in vm.applications | orderBy: sortReverse? '-creationTimeUtc': 'creationTimeUtc' track by application.id" entity="application" application-id="application.id"></entity-node>
 <p class="expand-tree-message text-center" ng-if="vm.applications.length > 0"><small><kbd>shift</kbd> + <kbd>{{navigator.appVersion.indexOf("Mac") !== -1 ? '⌘' : '⊞'}}</kbd> + click to expand all children</small></p>
 <div class="empty-tree text-muted text-center" ng-if="vm.applications.length === 0">
     <hr />
diff --git a/ui-modules/app-inspector/app/views/main/main.controller.js b/ui-modules/app-inspector/app/views/main/main.controller.js
index 07280f8..c691a10 100644
--- a/ui-modules/app-inspector/app/views/main/main.controller.js
+++ b/ui-modules/app-inspector/app/views/main/main.controller.js
@@ -27,11 +27,16 @@
     controllerAs: 'ctrl'
 };
 
+const savedSortReverse = 'app-inspector-sort-reverse';
+
 export function mainController($scope, $q, brWebNotifications) {
     $scope.$emit(HIDE_INTERSTITIAL_SPINNER_EVENT);
 
     let ctrl = this;
 
+    ctrl.sortReverse = localStorage && localStorage.getItem(savedSortReverse) !== null ?
+        JSON.parse(localStorage.getItem(savedSortReverse)) :
+        true;
     brWebNotifications.supported.then(() => {
         ctrl.isNotificationsSupported = true;
     }).catch(() => {
@@ -48,6 +53,17 @@
         ctrl.isNotificationsBlocked = permission === 'denied';
     });
 
+    ctrl.toggleSortOrder = () => {
+        ctrl.sortReverse = !ctrl.sortReverse;
+        if (localStorage) {
+            try {
+                localStorage.setItem(savedSortReverse, JSON.stringify(ctrl.sortReverse));
+            } catch (ex) {
+                $log.error('Cannot save app sort preferences: ' + ex.message);
+            }
+        }
+    }
+
     ctrl.toggleNotifications = () => {
         brWebNotifications.isEnabled().then(() => {
             return brWebNotifications.setEnable(false);
diff --git a/ui-modules/app-inspector/app/views/main/main.less b/ui-modules/app-inspector/app/views/main/main.less
index d14fed7..527554f 100644
--- a/ui-modules/app-inspector/app/views/main/main.less
+++ b/ui-modules/app-inspector/app/views/main/main.less
@@ -20,11 +20,9 @@
 
   .entity-tree-header {
     display: flex;
+    justify-content: space-between;
 
-    .entity-tree-title {
-      flex-grow: 1;
-    }
-    .entity-tree-action {
+    .entity-tree-title, .entity-tree-sort-action, .entity-tree-action {
       flex-shrink: 1;
       vertical-align: middle;
       margin-left: 0.5em;
diff --git a/ui-modules/app-inspector/app/views/main/main.template.html b/ui-modules/app-inspector/app/views/main/main.template.html
index 0911647..c7a90cd 100644
--- a/ui-modules/app-inspector/app/views/main/main.template.html
+++ b/ui-modules/app-inspector/app/views/main/main.template.html
@@ -22,29 +22,36 @@
             <br-card>
                 <br-card-content class="entity-tree">
                     <div class="entity-tree-header">
-                        <h2 class="entity-tree-title">Applications</h2>
-
-                        <button class="btn btn-link entity-tree-action"
+                        <div class="entity-tree-header-section">
+                            <h2 class="entity-tree-title">Applications</h2>
+                            <button class="btn btn-sm btn-default entity-tree-sort-action" ng-click="ctrl.toggleSortOrder($event)">
+                                <div>
+                                    <span class="glyphicon" ng-class="ctrl.sortReverse ? 'fa fa-sort-amount-desc' : 'fa fa-sort-amount-asc'"></span>
+                                </div>
+                            </button>
+                        </div>
+                        <div class="entity-tree-header-section">
+                            <button class="btn btn-link entity-tree-action"
                                 ng-class="{'btn-sm': !ctrl.isNotificationsBlocked, 'btn-xs': ctrl.isNotificationsBlocked}"
                                 ng-if="ctrl.isNotificationsSupported"
                                 ng-disabled="ctrl.isNotificationsBlocked"
                                 ng-click="ctrl.toggleNotifications()">
-                            <i class="fa fa-bell notifications" ng-if="!ctrl.isNotificationsBlocked"
-                                ng-class="{'active': ctrl.isNotificationsEnabled}"></i>
-                            <span class="fa-stack" ng-if="ctrl.isNotificationsBlocked"
-                                  uib-tooltip="Notifications are currently blocked. You can allow them in your browser settings"
-                                  tooltip-placement="left">
-                                <i class="fa fa-bell fa-stack-1x"></i>
-                                <i class="fa fa-ban fa-stack-2x text-danger"></i>
-                            </span>
-                        </button>
-
-                        <a href="/brooklyn-ui-blueprint-composer" class="btn btn-sm btn-default entity-tree-action">
-                            <i class="fa fa-plus"></i>
-                        </a>
+                                <i class="fa fa-bell notifications" ng-if="!ctrl.isNotificationsBlocked"
+                                   ng-class="{'active': ctrl.isNotificationsEnabled}"></i>
+                                <span class="fa-stack" ng-if="ctrl.isNotificationsBlocked"
+                                      uib-tooltip="Notifications are currently blocked. You can allow them in your browser settings"
+                                      tooltip-placement="left">
+                                    <i class="fa fa-bell fa-stack-1x"></i>
+                                    <i class="fa fa-ban fa-stack-2x text-danger"></i>
+                                </span>
+                            </button>
+                            <a href="/brooklyn-ui-blueprint-composer" class="btn btn-sm btn-default entity-tree-action">
+                                <i class="fa fa-plus"></i>
+                            </a>
+                        </div>
                     </div>
 
-                    <entity-tree></entity-tree>
+                    <entity-tree sort-reverse="ctrl.sortReverse"></entity-tree>
                 </br-card-content>
             </br-card>
 
diff --git a/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.directive.js b/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.directive.js
index 3d90b92..1b2fbdc 100644
--- a/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.directive.js
+++ b/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.directive.js
@@ -97,7 +97,6 @@
 
         $scope.viewModes = PALETTE_VIEW_MODES;
         $scope.viewOrders = PALETTE_VIEW_ORDERS;
-
         if (!$scope.state) $scope.state = {};
         if (!$scope.state.viewMode) $scope.state.viewMode = PALETTE_VIEW_MODES.normal;
 
@@ -345,14 +344,11 @@
     function repaginate($scope, $element) {
         let rowsPerPage = $scope.rowsPerPage;
         if (!rowsPerPage) {
-            let main = angular.element($element[0].querySelector(".catalog-palette-main"));
-            if (!main || main[0].offsetHeight == 0) {
-                // no main, or hidden, or items per page fixed
-                return;
-            }
-            let header = angular.element(main[0].querySelector(".catalog-palette-header"));
-            let footer = angular.element(main[0].querySelector(".catalog-palette-footer"));
-            rowsPerPage = Math.max(MIN_ROWS_PER_PAGE, Math.floor((main[0].offsetHeight - header[0].offsetHeight - footer[0].offsetHeight - 16) / ($scope.state.viewMode.rowHeightPx || 96)));
+            let palette = angular.element(document.querySelector(".page-main-area"));
+            let toolbar = angular.element(document.querySelector(".navbar-mode"));
+            let header = angular.element($element[0].querySelector(".catalog-palette-header"));
+            let footer = angular.element($element[0].querySelector(".catalog-palette-footer"));
+            rowsPerPage = Math.max(MIN_ROWS_PER_PAGE, Math.floor((palette[0].offsetHeight - (toolbar[0].offsetHeight + header[0].offsetHeight + footer[0].offsetHeight + 16)) / ($scope.state.viewMode.rowHeightPx || 96)));
         }
         $scope.$apply(() => $scope.pagination.itemsPerPage = rowsPerPage * $scope.state.viewMode.itemsPerRow);
     }
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 5e43ab7..eec14e5 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
@@ -700,7 +700,7 @@
         });
 
         relationships = Array.from(entity.config.values())
-            .filter(config => config[DSL_ENTITY_SPEC] && config[DSL_ENTITY_SPEC] instanceof Entity)
+            .filter(config => config && config[DSL_ENTITY_SPEC] && config[DSL_ENTITY_SPEC] instanceof Entity)
             .map(config => config[DSL_ENTITY_SPEC])
             .reduce((relationships, spec) => {
             return relationships.concat(getRelationships(spec));
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 3cc94b9..00abd58 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
@@ -91,6 +91,9 @@
     .entity-type-header {
       font-size: 105%;
       margin-right: 1em;
+      // allow this to wrap, as it's important to be able to read it;
+      // break anywhere in case it's of the form my.type.HasGotAVeryVeryVeryLongName
+      word-break: break-all;
     }
     .label.version {
       vertical-align: 2px;
diff --git a/ui-modules/blueprint-composer/app/views/main/main.controller.js b/ui-modules/blueprint-composer/app/views/main/main.controller.js
index 886b4a1..fc1ac1a 100644
--- a/ui-modules/blueprint-composer/app/views/main/main.controller.js
+++ b/ui-modules/blueprint-composer/app/views/main/main.controller.js
@@ -131,9 +131,28 @@
         
         yaml = edit.type.plan.data;
     }
+    
+    vm.isGraphicalMode = () => {
+        return $state.includes(graphicalState.name);
+    };
+    vm.isYamlMode = () => {
+        return $state.includes(yamlState.name);
+    };
 
     if (yaml) {
-        blueprintService.setFromYaml(yaml);
+        if (vm.isYamlMode()) {
+            // don't set blueprint; yaml mode will take from "initial yaml" 
+            blueprintService.reset();
+            $scope.initialYaml = yaml;
+        } else {
+            try {
+                blueprintService.setFromYaml(yaml);
+            } catch (e) {
+                console.warn("YAML supplied for editing is not valid for a blueprint. It will be ignored unless opened in the YAML editor:", e);
+                blueprintService.reset();
+                $scope.initialYaml = yaml;
+            }
+        }
     } else {
         blueprintService.reset();
     }
@@ -147,10 +166,6 @@
         deployApplication();
     };
 
-    vm.isGraphicalMode = () => {
-        return $state.includes(graphicalState.name);
-    };
-
     vm.getAllActions = () => {
         return actionService.getActions();
     }
diff --git a/ui-modules/blueprint-composer/app/views/main/yaml/yaml.state.js b/ui-modules/blueprint-composer/app/views/main/yaml/yaml.state.js
index bfd47f2..f9390ae 100644
--- a/ui-modules/blueprint-composer/app/views/main/yaml/yaml.state.js
+++ b/ui-modules/blueprint-composer/app/views/main/yaml/yaml.state.js
@@ -39,6 +39,11 @@
         brSnackbar.create(`Cannot load blueprint: ${ex.message}`);
         vm.yaml = '';
     }
+    if ($scope.initialYaml && !vm.yaml) {
+        // either yaml was supplied and yaml mode requested, skipping blueprint setup,
+        // or the yaml was invalid, an error logged, and this was recorded
+        vm.yaml = $scope.initialYaml; 
+    }
 
     if (!CodeMirror.lint.hasOwnProperty('yaml-composer')) {
         CodeMirror.registerGlobalHelper('lint', 'yaml-composer', mode => mode.name === 'yaml', (text, options, cm) => {
diff --git a/ui-modules/home/app/views/main/deploy/deploy.controller.js b/ui-modules/home/app/views/main/deploy/deploy.controller.js
index 896ec7e..cab9db2 100644
--- a/ui-modules/home/app/views/main/deploy/deploy.controller.js
+++ b/ui-modules/home/app/views/main/deploy/deploy.controller.js
@@ -22,6 +22,7 @@
 import brooklynApi from 'brooklyn-ui-utils/brooklyn.api/brooklyn.api';
 import {HIDE_INTERSTITIAL_SPINNER_EVENT} from 'brooklyn-ui-utils/interstitial-spinner/interstitial-spinner';
 import modalTemplate from './modal.template.html';
+import {filterCatalogQuickLaunch} from '../main.controller.js';  // this really should be handled by angular DI 
 
 const MODULE_NAME = 'states.main.deploy';
 
@@ -54,9 +55,7 @@
             entitySpec: ['catalogApi', (catalogApi) => {
                 return catalogApi.getBundleType($stateParams.bundleSymbolicName, $stateParams.bundleVersion, $stateParams.typeSymbolicName, $stateParams.typeVersion);
             }],
-            locations: ['locationApi', (locationApi) => {
-                return locationApi.getLocations();
-            }]
+            locations: ['locationApi', locationApi => locationApi.getLocations()],
         }
     });
 
@@ -80,9 +79,20 @@
 
     function modalController($scope, $location, entitySpec, locations) {
         $scope.app = entitySpec;
-        $scope.locations = locations;
-        // can optionally add: { noEditButton: true, noComposerButton: true }, or pass in URL
-        $scope.args = angular.extend({}, $location.search());
+        $scope.locations = filterCatalogQuickLaunch(locations, (t) => {
+                $scope.usingLocationCatalogQuickLaunchTags = t.length > 0;
+            });
+        
+        // also supports { noEditButton: true, noComposerButton: true }
+        // see quick-launch.js for more info
+        $scope.args = angular.extend({
+                // disable "create location" is admin has configured locations with tags.
+                // called out in docs in https://github.com/apache/brooklyn-docs/pull/299.
+                // a better approach would be to add config on the server to configure this.
+                noCreateLocationLink: $scope.usingLocationCatalogQuickLaunchTags
+            },
+            $location.search());
+        
     }
 }
 
diff --git a/ui-modules/home/app/views/main/main.controller.js b/ui-modules/home/app/views/main/main.controller.js
index e967953..a3bdff1 100644
--- a/ui-modules/home/app/views/main/main.controller.js
+++ b/ui-modules/home/app/views/main/main.controller.js
@@ -41,20 +41,30 @@
             return brooklynUiModulesApi.getUiModules();
         }],
         catalogApps: ['catalogApi', (catalogApi) => {
-            return catalogApi.getTypes({params: {supertype: 'org.apache.brooklyn.api.entity.Application'}}).then(applications => {
-                // optionally tag things with 'catalog_quick_launch': if any apps are so tagged, 
-                // then only apps with such tags will be shown;
-                // in all cases only show those marked as templates
-                var appsWithTag = applications.filter(application => application.tags && application.tags.indexOf("catalog_quick_launch")>=0);
-                if (appsWithTag.length) {
-                    applications = appsWithTag;
-                }
-                return applications.filter(application => application.template);
-            });
+            return catalogApi.getTypes({params: {supertype: 'org.apache.brooklyn.api.entity.Application'}}).then(
+                applications => filterCatalogQuickLaunch(applications.filter(application => application.template))
+            );
         }]
     }
 };
 
+export function filterCatalogQuickLaunch(list, callbackForFiltered) {
+    // optionally tag things with 'catalog_quick_launch': if any apps are so tagged, 
+    // then only apps with such tags will be shown; otherwise show all marked as templates.
+    
+    // the callback is used for clients who wish to adjust their behaviour if tags are used,
+    // eg in deploy.controller where noCreateLocationLink is set on the quick launch if there are tagged locations
+    
+    if (!list) { 
+        list = [];
+    }
+    let tagged = list.filter(i => i && i.tags && i.tags.indexOf("catalog_quick_launch")>=0);
+    if (callbackForFiltered) {
+        callbackForFiltered(tagged, list);
+    }
+    return tagged.length ? tagged : list;
+}
+
 export function mainStateConfig($stateProvider) {
     $stateProvider.state(mainState);
 }
diff --git a/ui-modules/location-manager/app/views/wizard/cloud/cloud.controller.js b/ui-modules/location-manager/app/views/wizard/cloud/cloud.controller.js
index 996a156..9309e50 100644
--- a/ui-modules/location-manager/app/views/wizard/cloud/cloud.controller.js
+++ b/ui-modules/location-manager/app/views/wizard/cloud/cloud.controller.js
@@ -71,6 +71,11 @@
         return disabled.indexOf(vm.provider) > -1;
     };
 
+    vm.isEndpointRequired = function () {
+        let required = ['jclouds:azurecompute-arm'];
+        return required.indexOf(vm.provider) > -1;
+    }
+
     vm.save = function () {
         let config = angular.copy(vm.config);
         if (vm.region) {
diff --git a/ui-modules/location-manager/app/views/wizard/cloud/cloud.template.html b/ui-modules/location-manager/app/views/wizard/cloud/cloud.template.html
index 418e2fb..7d22072 100644
--- a/ui-modules/location-manager/app/views/wizard/cloud/cloud.template.html
+++ b/ui-modules/location-manager/app/views/wizard/cloud/cloud.template.html
@@ -67,7 +67,10 @@
 
                     <div class="form-group" ng-class="{'has-error': form.endpoint.$invalid && form.endpoint.$touched}">
                         <label class="control-label" for="endpoint">Cloud endpoint</label>
-                        <input ng-model="vm.endpoint" type="text" class="form-control" id="endpoint" name="endpoint" ng-disabled="vm.isEndpointDisabled()" placeholder="If using a private cloud, the URL to connect to it is required">
+                        <input ng-model="vm.endpoint" type="text" class="form-control" id="endpoint" name="endpoint" ng-required="vm.isEndpointRequired()" ng-disabled="vm.isEndpointDisabled()" placeholder="If using a private cloud, the URL to connect to it is required">
+                        <p class="help-block" ng-show="form.$submitted || form.endpoint.$touched">
+                            <span ng-show="form.endpoint.$error.required">You must specify an endpoint with this provider</span>
+                        </p>
                     </div>
 
                     <div class="form-group" ng-class="{'has-error': form.identity.$invalid && form.identity.$touched}">
diff --git a/ui-modules/utils/catalog-uploader/catalog-uploader.html b/ui-modules/utils/catalog-uploader/catalog-uploader.html
index 7288484..0ee103d 100644
--- a/ui-modules/utils/catalog-uploader/catalog-uploader.html
+++ b/ui-modules/utils/catalog-uploader/catalog-uploader.html
@@ -23,7 +23,7 @@
             <div class="col-sm-12 text-center">
                 <p><i class="fa fa-3x fa-upload"></i></p>
                 <p>
-                    <input type="file" name="files" id="files" multiple onchange="angular.element(this).scope().filesChanged(this)" />
+                    <input type="file" name="files" id="files" multiple custom-on-change="filesChanged" />
                     <label for="files" ng-click="choose()"><strong>Choose files</strong><span class="drag-upload"> or drag & drop them here</span>.</label>
                 </p>
             </div>
diff --git a/ui-modules/utils/catalog-uploader/catalog-uploader.js b/ui-modules/utils/catalog-uploader/catalog-uploader.js
index 9e4c47f..d7480a3 100644
--- a/ui-modules/utils/catalog-uploader/catalog-uploader.js
+++ b/ui-modules/utils/catalog-uploader/catalog-uploader.js
@@ -33,6 +33,7 @@
  */
 angular.module(MODULE_NAME, [catalogApi])
     .service('brooklynCatalogUploader', ['$q', 'catalogApi', catalogUploaderService])
+    .directive('customOnChange', customOnChangeDirective)
     .directive('brooklynCatalogUploader', ['$compile', 'brooklynCatalogUploader', catalogUploaderDirective]);
 
 export default MODULE_NAME;
@@ -98,8 +99,8 @@
             element.removeClass('br-drag-active');
         };
 
-        scope.filesChanged = (target)=> {
-            scope.upload(target.files);
+        scope.filesChanged = (event)=> {
+            scope.upload(event.target.files);
         };
 
         scope.upload = (files)=> {
@@ -120,7 +121,9 @@
         };
 
         scope.getCatalogItemUrl = (item)=> {
-            return item.supertypes.includes('org.apache.brooklyn.api.location.Location')
+            let itemTraits = item.tags? item.tags.find(item => item.hasOwnProperty("traits")) : {"traits":[]};
+            return (item.supertypes ? item.supertypes : itemTraits.traits)
+                .includes('org.apache.brooklyn.api.location.Location')
                 ? `/brooklyn-ui-location-manager/#!/location?symbolicName=${item.symbolicName}&version=${item.version}`
                 : `/brooklyn-ui-catalog/#!/bundles/${item.containingBundle.split(':')[0]}/${item.containingBundle.split(':')[1]}/types/${item.symbolicName}/${item.version}`;
         };
@@ -208,3 +211,18 @@
         return defer.promise;
     }
 }
+
+export function customOnChangeDirective() {
+    return {
+        restrict: 'A',
+        link: function (scope, element, attrs) {
+            element.on('change', x => {
+                var onChangeHandler = scope.$eval(attrs.customOnChange);
+                onChangeHandler(x);
+            });
+            element.on('$destroy', function() {
+                element.off();
+            });
+        }
+    };
+}
\ No newline at end of file
diff --git a/ui-modules/utils/quick-launch/quick-launch.html b/ui-modules/utils/quick-launch/quick-launch.html
index 99f4d5e..a2a6a4c 100644
--- a/ui-modules/utils/quick-launch/quick-launch.html
+++ b/ui-modules/utils/quick-launch/quick-launch.html
@@ -39,7 +39,8 @@
             <h3 class="quick-launch-section-title">Name</h3>
             <div class="quick-launch-section-content">
                 <div class="form-group">
-                    <input class="form-control" type="text" ng-model="model.name" ng-disabled="deploying" name="name" placeholder="Choose a name for this application (Optional)" autofocus />
+                    <input class="form-control" type="text" ng-model="model.name" ng-disabled="deploying" name="name" 
+                        placeholder="{{ 'Choose a name for this application (Optional)' }}" />
                 </div>
             </div>
         </section>
@@ -55,7 +56,7 @@
                            required />
                     <small class="help-block">
                         <span ng-if="deploy.location.$error.required && (deploy.$submitted || deploy.location.$touched)">You must select a location.</span>
-                        <span>Alternatively, you can <a href="/brooklyn-ui-location-manager/#!/wizard">create a new location</a></span>
+                        <span ng-if="!args.noCreateLocationLink">Alternatively, you can <a href="/brooklyn-ui-location-manager/#!/wizard">create a new location</a></span>
                     </small>
                 </div>
             </div>
diff --git a/ui-modules/utils/quick-launch/quick-launch.js b/ui-modules/utils/quick-launch/quick-launch.js
index e4b1a67..a114aae 100644
--- a/ui-modules/utils/quick-launch/quick-launch.js
+++ b/ui-modules/utils/quick-launch/quick-launch.js
@@ -38,7 +38,7 @@
         scope: {
             app: '=',
             locations: '=',
-            args: '=?',
+            args: '=?', // default behaviour of code is: { noEditButton: false, noComposerButton: false, noCreateLocationLink: false, location: null }
             callback: '=?',
         },
         controller: ['$scope', '$http', '$location', 'brSnackbar', controller]
@@ -47,11 +47,21 @@
     function controller($scope, $http, $location, brSnackbar) {
         $scope.deploying = false;
         $scope.model = {
-            newConfigFormOpen: false
+            newConfigFormOpen: false,
+            
+            // should never be null, so the placeholder in UI for model.name will never be used;
+            // hence autofocus is disabled
+            name: ($scope.app && ($scope.app.name || $scope.app.symbolicName)) || null, 
         };
         $scope.args = $scope.args || {};
         if ($scope.args.location) {
             $scope.model.location = $scope.args.location;
+        } else {
+            if ($scope.locations) {
+                if ($scope.locations.length == 1) {
+                    $scope.model.location = $scope.locations[0];
+                }
+            } 
         }
         $scope.toggleNewConfigForm = toggleNewConfigForm;
         $scope.addNewConfigKey = addNewConfigKey;
@@ -74,7 +84,7 @@
             $scope.appHasWizard = parsedPlan!=null && !checkForLocationTags(parsedPlan);
             $scope.yamlViewDisplayed = !$scope.appHasWizard;
             $scope.entityToDeploy = {
-                type: $scope.app.symbolicName
+                type: $scope.app.symbolicName + ($scope.app.version ? ':' + $scope.app.version : ''),
             };
             if ($scope.app.config) {
                 $scope.configMap = $scope.app.config.reduce((result, config) => {
@@ -177,21 +187,87 @@
             return yaml.safeDump(newApp);
         }
 
-        function buildComposerYaml() {
+        function buildComposerYaml(validate) {
             if ($scope.yamlViewDisplayed) {
                 return angular.copy($scope.editorYaml);
             } else {
-                let newApp = {
-                    name: $scope.model.name || $scope.app.displayName,
-                };
-                if ($scope.model.location) {
-                    newApp.location = $scope.model.location;
+                let planText = $scope.app.plan.data || "{}";
+                let result = {};
+                
+                // this is set if we're able to parse the plan's text definition, and then:
+                // - we've had to override a field from the plan's text definition, because a value is set _and_ different; or
+                // - the plan's text definition is indented or JSON rather than YAML (not outdented yaml)
+                // and in either case we use the result _object_ ... 
+                // unless we didn't actually change anything, in which case this is ignored
+                let cannotUsePlanText = false;
+                
+                if (validate) {
+                    result = yaml.safeLoad(planText);
+                    if (typeof result !== 'object') {
+                        throw "The plan is not a YAML map, but of type "+(typeof result);
+                    }
+                    if (!result.services) {
+                        throw "The plan does not have any services.";
+                    }
+                    for (const [k,v] of Object.entries(result) ) {
+                       if (planText.indexOf(k)!=0 && planText.indexOf('\n'+k+':')<0) {
+                          // plan is not outdented yaml, can't use its text mode
+                          cannotUsePlanText = true;
+                          break;
+                       }
+                    }
                 }
-                if ($scope.entityToDeploy[BROOKLYN_CONFIG]) {
-                    newApp[BROOKLYN_CONFIG] = $scope.entityToDeploy[BROOKLYN_CONFIG]
+                
+                let newApp = {};
+                
+                let newName = $scope.model.name || $scope.app.displayName;
+                if (newName && newName != result.name) {
+                    newApp.name = newName;
+                    if (result.name) {
+                        delete result.name;
+                        cannotUsePlanText = true;
+                    }
                 }
-                // TODO if plan data has config in the root (unlikely) this will have errors
-                return yaml.safeDump(newApp) + "\n" + $scope.app.plan.data;
+                
+                let newLocation = $scope.model.location;
+                if (newLocation && newLocation != result.location) {
+                    newApp.location = newLocation;
+                    if (result.location) {
+                        delete result.location;
+                        cannotUsePlanText = true;
+                    }
+                }
+
+                let newConfig = $scope.entityToDeploy[BROOKLYN_CONFIG];
+                if (newConfig) {
+                    if (result[BROOKLYN_CONFIG]) {
+                        let oldConfig = result[BROOKLYN_CONFIG];
+                        let mergedConfig = angular.copy(oldConfig);
+                        for (const [k,v] of Object.entries(newConfig) ) {
+                            if (mergedConfig[k] != v) {
+                                cannotUsePlanText = true;
+                                mergedConfig[k] = v;
+                            }
+                        }
+                        if (cannotUsePlanText) {
+                            newApp[BROOKLYN_CONFIG] = mergedConfig;
+                            delete result[BROOKLYN_CONFIG];
+                        }
+                    } else {
+                        newApp[BROOKLYN_CONFIG] = newConfig;
+                    }
+                }
+                
+                // prefer to use the actual yaml input, but if it's not possible
+                let tryMergeByConcatenate = 
+                    Object.keys(newApp).length ?
+                        (yaml.safeDump(newApp) + "\n" + ((validate && cannotUsePlanText) ? yaml.safeDump(result) : planText))
+                        : planText;
+                if (validate) {
+                    // don't think there's any way we'd wind up with invalid yaml but check to be sure
+                    yaml.safeLoad(tryMergeByConcatenate);
+                }
+                return tryMergeByConcatenate;
             }
         }
 
@@ -205,8 +281,19 @@
         }
 
         function openComposer() {
-            window.location.href = '/brooklyn-ui-blueprint-composer/#!/graphical?'+
-                'yaml='+encodeURIComponent(buildComposerYaml());
+            try {
+              window.location.href = '/brooklyn-ui-blueprint-composer/#!/graphical?'+
+                'yaml='+encodeURIComponent(buildComposerYaml(true));
+            } catch (error) {
+              console.warn("Opening composer in YAML text editor mode because we cannot generate a model for this configuration:", error);
+              window.location.href = '/brooklyn-ui-blueprint-composer/#!/yaml?'+
+                'yaml='+encodeURIComponent(
+                    "# This plan may have items which require attention so is being opened in YAML text editor mode.\n"+
+                    "# The YAML was autogenerated by merging the plan with any values provided in UI, but issues were\n"+
+                    "# detected that mean it might not be correct. Please check the blueprint below carefully.\n"+
+                    "\n"+
+                    buildComposerYaml(false));
+            }
         }
 
         function clearError() {
diff --git a/ui-modules/utils/server-status/server-status.js b/ui-modules/utils/server-status/server-status.js
index 9d25c1a..e82d544 100644
--- a/ui-modules/utils/server-status/server-status.js
+++ b/ui-modules/utils/server-status/server-status.js
@@ -36,14 +36,15 @@
 export function BrServerStatusDirective() {
     return {
         restrict: 'A',
-        controller: ['$rootScope', '$scope', '$http', '$cookies', '$interval', '$uibModal', controller]
+        controller: ['$rootScope', '$scope', '$http', '$cookies', '$interval', '$uibModal', '$log', controller]
     };
 
-    function controller($rootScope, $scope, $http, $cookies, $interval, $uibModal) {
+    function controller($rootScope, $scope, $http, $cookies, $interval, $uibModal, $log) {
         let cookie = DEFAULT_COOKIE;
         let intervalId = $interval(checkStatus, REFRESH_INTERVAL);
         $scope.$on('$destroy', () => ($interval.cancel(intervalId)));
         let modalInstance = null;
+        var previousState = null;
 
         function checkStatus() {
             cookie = $cookies.getObject(COOKIE_KEY) || DEFAULT_COOKIE;
@@ -55,7 +56,28 @@
             let state = BrServerStatusModalController.STATES.OK;
             let stateData = null;
             if (error) {
-                state = BrServerStatusModalController.STATES.NO_CONNECTION;
+                stateData = response.data;
+
+                if (stateData && stateData.SESSION_AGE_EXCEEDED) {
+                    state = BrServerStatusModalController.STATES.SESSION_AGE_EXCEEDED;
+                } else if (stateData && stateData.SESSION_INVALIDATED) {
+                    state = BrServerStatusModalController.STATES.SESSION_INVALIDATED;
+                }else if(response.status === 404) {
+                    state = BrServerStatusModalController.STATES.NO_CONNECTION;
+                }else if(response.status === 401 || response.status === 403 ) {
+                    state = BrServerStatusModalController.STATES.USER_NOT_AUTHORIZED;
+                }else {
+                    if (previousState === null || previousState == BrServerStatusModalController.STATES.OK){
+                        state = BrServerStatusModalController.STATES.OTHER_ERROR;
+                    } else {
+                        // we're now getting a new server error, possibly because the old error has expired
+                        // but changing the message for the user would be confusing so don't do that!
+                        // eg we get a 405 after a 307 (which the browser handles automatically) if redirected to Google for login
+                        $log.info("Server responded \"" + stateData + "\" after previous problem \"" + previousState + "\"");
+                        // no update
+                        state = previousState;
+                    }
+                }
                 stateData = response;
             } else {
                 stateData = response.data;
@@ -69,6 +91,7 @@
                     state = BrServerStatusModalController.STATES.UNHEALTHY;
                 }
             }
+            previousState = state;
             $rootScope.$broadcast('br-server-state-update', {state: state, stateData: stateData});
             if (state !== BrServerStatusModalController.STATES.OK && !cookie.dismissed && cookie.dismissedSate !== state) {
                 openModal(state, stateData);
@@ -114,7 +137,11 @@
                 STOPPING: 'STOPPING',
                 NOT_HA_MASTER: 'NOT-HA-MASTER',
                 NO_CONNECTION: 'NO-CONNECTION',
-                UNHEALTHY: 'UNHEALTHY'
+                UNHEALTHY: 'UNHEALTHY',
+                SESSION_INVALIDATED: 'SESSION_INVALIDATED',
+                SESSION_AGE_EXCEEDED: 'SESSION_AGE_EXCEEDED',
+                OTHER_ERROR: 'OTHER_ERROR',
+                USER_NOT_AUTHORIZED: 'USER_NOT_AUTHORIZED'
             };
             static $inject = ['$scope', '$uibModalInstance', 'state', 'stateData'];
 
diff --git a/ui-modules/utils/server-status/server-status.template.html b/ui-modules/utils/server-status/server-status.template.html
index eb54018..2de6b42 100644
--- a/ui-modules/utils/server-status/server-status.template.html
+++ b/ui-modules/utils/server-status/server-status.template.html
@@ -21,7 +21,9 @@
     <h3 class="modal-title" ng-switch-when="STARTING">Server starting up&hellip;</h3>
     <h3 class="modal-title" ng-switch-when="STOPPING">Server shutting down&hellip;</h3>
     <h3 class="modal-title" ng-switch-when="NOT-HA-MASTER">This server is not the high availability master</h3>
-    <h3 class="modal-title" ng-switch-when="NO-CONNECTION">Connecting to server&hellip;</h3>
+    <h3 class="modal-title" ng-switch-when="NO-CONNECTION|OTHER_ERROR" ng-switch-when-separator="|">Connecting to server&hellip;</h3>
+    <h3 class="modal-title" ng-switch-when="SESSION_INVALIDATED|SESSION_AGE_EXCEEDED" ng-switch-when-separator="|">Session invalid</h3>
+    <h3 class="modal-title" ng-switch-when="USER_NOT_AUTHORIZED">User not authorized</h3>
     <h3 class="modal-title" ng-switch-when="OK">Server up and running</h3>
     <h3 class="modal-title" ng-switch-default>This server has errors</h3>
 </div>
@@ -68,13 +70,27 @@
             </tbody>
         </table>
     </div>
-    <div ng-switch-when="NO-CONNECTION">
+    <div ng-switch-when="NO-CONNECTION|OTHER_ERROR" ng-switch-when-separator="|">
         <p>Cannot connect to API server. Please check your network connection. Try closing and re-opening the window and login again in the app. If the problem persists, please contact your administrator.</p>
     </div>
     <div ng-switch-when="UNHEALTHY">
         <p>Your server has errors and is not healthy.</p>
         <p>Please check with your system administrator.</p>
     </div>
+    <div ng-switch-when="USER_NOT_AUTHORIZED">
+        <p>The current user is not authorized.</p>
+    </div>
+    <div ng-switch-when="SESSION_INVALIDATED|SESSION_AGE_EXCEEDED" ng-switch-when-separator="|">
+        <p>Your last session has expired.</p>
+        <p>Please login again.</p>
+    </div>
+    <div ng-switch-when="OTHER_ERROR">
+        <p>An unexpected error has occurred.</p>
+        <p>Please try to login again and if the problem persists, please check with your system administrator.</p>
+        <form action="/brooklyn-ui-logout" method="post">
+            <button type="submit" class="btn btn-success"><i class="fa fa-sign-out fa-fw"></i> Logout</button>
+        </form>
+    </div>
     <div ng-switch-when="OK">
         <p>The server is now ready to use.</p>
     </div>
diff --git a/ui-modules/utils/yaml-editor/addon/lint/lint-yaml-brooklyn.js b/ui-modules/utils/yaml-editor/addon/lint/lint-yaml-brooklyn.js
index 2d5e424..e4cf85c 100644
--- a/ui-modules/utils/yaml-editor/addon/lint/lint-yaml-brooklyn.js
+++ b/ui-modules/utils/yaml-editor/addon/lint/lint-yaml-brooklyn.js
@@ -29,38 +29,31 @@
 import catalogVersionSchema from '../schemas/catalog-version.json';
 import rootSchema from '../schemas/root.json';
 
-CodeMirror.registerGlobalHelper('lint', 'yamlBlueprint', (mode, cm) => (mode.name === 'yaml' && mode.type === 'blueprint'), (text, options, cm) => {
-    let validator = new Validator();
+let blueprintValidator = new Validator();
+let catalogValidator = new Validator();
+let rootValidator = new Validator();
 
+[ blueprintValidator, catalogValidator, rootValidator ].forEach(validator => {
     validator.addSchema(JSON.parse(blueprintSchema), '/Blueprint');
     validator.addSchema(JSON.parse(blueprintEntitySchema), '/Blueprint/Entity');
     validator.addSchema(JSON.parse(blueprintLocationSchema), '/Blueprint/Location');
-
-    return lint(validator, blueprintSchema, text, options, cm);
 });
-CodeMirror.registerGlobalHelper('lint', 'yamlCatalog', (mode, cm) => (mode.name === 'yaml' && mode.type === 'catalog'), (text, options, cm) => {
-    let validator = new Validator();
 
+[ catalogValidator, rootValidator ].forEach(validator => {
     validator.addSchema(JSON.parse(catalogSchema), '/Catalog');
     validator.addSchema(JSON.parse(catalogItemReferenceSchema), '/Catalog/Item/Reference');
     validator.addSchema(JSON.parse(catalogItemInlineSchema), '/Catalog/Item/Inline');
     validator.addSchema(JSON.parse(catalogVersionSchema), '/Catalog/Version');
-
-    return lint(validator, catalogSchema, text, options, cm);
 });
-CodeMirror.registerGlobalHelper('lint', 'yamlBrooklyn', (mode, cm) => (mode.name === 'yaml' && mode.type === 'brooklyn'), (text, options, cm) => {
-    let validator = new Validator();
 
-    validator.addSchema(JSON.parse(blueprintSchema), '/Blueprint');
-    validator.addSchema(JSON.parse(blueprintEntitySchema), '/Blueprint/Entity');
-    validator.addSchema(JSON.parse(blueprintLocationSchema), '/Blueprint/Location');
-    validator.addSchema(JSON.parse(catalogSchema), '/Catalog');
-    validator.addSchema(JSON.parse(catalogItemReferenceSchema), '/Catalog/Item/Reference');
-    validator.addSchema(JSON.parse(catalogItemInlineSchema), '/Catalog/Item/Inline');
-    validator.addSchema(JSON.parse(catalogVersionSchema), '/Catalog/Version');
-
-    return lint(validator, rootSchema, text, options, cm);
-});
+CodeMirror.registerGlobalHelper('lint', 'yamlBlueprint', (mode, cm) => (mode.name === 'yaml' && mode.type === 'blueprint'), 
+    (text, options, cm) => lint(blueprintValidator, blueprintSchema, text, options, cm));
+    
+CodeMirror.registerGlobalHelper('lint', 'yamlCatalog', (mode, cm) => (mode.name === 'yaml' && mode.type === 'catalog'), 
+    (text, options, cm) => lint(catalogValidator, catalogSchema, text, options, cm));
+    
+CodeMirror.registerGlobalHelper('lint', 'yamlBrooklyn', (mode, cm) => (mode.name === 'yaml' && mode.type === 'brooklyn'), 
+    (text, options, cm) => lint(rootValidator, rootSchema, text, options, cm));
 
 function lint(validator, baseSchema, text, options, cm) {
     let issues = [];