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…</h3>
<h3 class="modal-title" ng-switch-when="STOPPING">Server shutting down…</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…</h3>
+ <h3 class="modal-title" ng-switch-when="NO-CONNECTION|OTHER_ERROR" ng-switch-when-separator="|">Connecting to server…</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 = [];