| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| import angular from 'angular'; |
| import yaml from 'js-yaml'; |
| import brAutofocus from '../autofocus/autofocus'; |
| import brYamlEditor from '../yaml-editor/yaml-editor'; |
| import template from './quick-launch.html'; |
| |
| const MODULE_NAME = 'brooklyn.components.quick-launch'; |
| |
| angular.module(MODULE_NAME, [brAutofocus, brYamlEditor]) |
| .directive('brooklynQuickLaunch', quickLaunchDirective); |
| |
| export default MODULE_NAME; |
| |
| const BROOKLYN_CONFIG = 'brooklyn.config'; |
| |
| export function quickLaunchDirective() { |
| return { |
| restrict: 'E', |
| template: template, |
| scope: { |
| app: '=', |
| locations: '=', |
| args: '=?', |
| callback: '=?', |
| }, |
| controller: ['$scope', '$http', '$location', 'brSnackbar', controller] |
| }; |
| |
| function controller($scope, $http, $location, brSnackbar) { |
| $scope.deploying = false; |
| $scope.model = { |
| newConfigFormOpen: false |
| }; |
| $scope.args = $scope.args || {}; |
| if ($scope.args.location) { |
| $scope.model.location = $scope.args.location; |
| } |
| $scope.toggleNewConfigForm = toggleNewConfigForm; |
| $scope.addNewConfigKey = addNewConfigKey; |
| $scope.deleteConfigField = deleteConfigField; |
| $scope.deployApp = deployApp; |
| $scope.showEditor = showEditor; |
| $scope.openComposer = openComposer; |
| $scope.hideEditor = hideEditor; |
| $scope.clearError = clearError; |
| |
| $scope.$watch('app', () => { |
| $scope.clearError(); |
| $scope.editorYaml = $scope.app.plan.data; |
| var parsedPlan = null; |
| try { |
| parsedPlan = yaml.safeLoad($scope.editorYaml); |
| } catch (e) { /* ignore, it's an unparseable template */ } |
| // enable wizard if it's parseble and doesn't specify a location |
| // (if it's not parseable, or it specifies a location, then the YAML view is displayed) |
| $scope.appHasWizard = parsedPlan!=null && !checkForLocationTags(parsedPlan); |
| $scope.yamlViewDisplayed = !$scope.appHasWizard; |
| $scope.entityToDeploy = { |
| type: $scope.app.symbolicName + ($scope.app.version ? ':' + $scope.app.version : ''), |
| }; |
| if ($scope.app.config) { |
| $scope.configMap = $scope.app.config.reduce((result, config) => { |
| result[config.name] = config; |
| if (config.pinned || (angular.isArray(config.contraints) && config.constraints.indexOf('required') > -1 && (!config.defaultValue === undefined || config.defaultValue === ''))) { |
| if (!$scope.entityToDeploy.hasOwnProperty(BROOKLYN_CONFIG)) { |
| $scope.entityToDeploy[BROOKLYN_CONFIG] = {}; |
| } |
| |
| $scope.entityToDeploy[BROOKLYN_CONFIG][config.name] = config.defaultValue || null; |
| } |
| return result; |
| }, {}); |
| } else { |
| $scope.configMap = {}; |
| } |
| }); |
| $scope.$watch('editorYaml', () => { |
| $scope.clearError(); |
| }); |
| $scope.$watch('entityToDeploy', () => { |
| $scope.clearError(); |
| }, true); |
| $scope.$watch('model.name', () => { |
| $scope.clearError(); |
| }); |
| $scope.$watch('model.location', () => { |
| $scope.clearError(); |
| }); |
| |
| function deployApp() { |
| $scope.deploying = true; |
| let appYaml; |
| if ($scope.yamlViewDisplayed) { |
| appYaml = angular.copy($scope.editorYaml); |
| } else { |
| appYaml = buildYaml(); |
| } |
| $http({ |
| method: 'POST', |
| url: '/v1/applications', |
| data: appYaml |
| }).then((response) => { |
| if ($scope.callback) { |
| $scope.callback.apply({}, [{state: 'SUCCESS', data: response.data}]); |
| } else { |
| brSnackbar.create('Application Deployed'); |
| } |
| $scope.deploying = false; |
| }, (response) => { |
| $scope.model.deployError = response.data.message; |
| $scope.deploying = false; |
| }); |
| } |
| |
| function toggleNewConfigForm() { |
| $scope.model.newConfigFormOpen = !$scope.model.newConfigFormOpen; |
| if ($scope.model.newConfigFormOpen) { |
| delete $scope.model.newKey; |
| } |
| } |
| |
| function deleteConfigField(key) { |
| delete $scope.entityToDeploy[BROOKLYN_CONFIG][key]; |
| if (Object.keys($scope.entityToDeploy[BROOKLYN_CONFIG]).length === 0) { |
| delete $scope.entityToDeploy[BROOKLYN_CONFIG]; |
| } |
| } |
| |
| function addNewConfigKey() { |
| if ($scope.model.newKey && $scope.model.newKey.length > 0) { |
| let newConfigValue = null; |
| if ($scope.configMap.hasOwnProperty($scope.model.newKey) && |
| $scope.configMap[$scope.model.newKey].hasOwnProperty('defaultValue')) { |
| newConfigValue = $scope.configMap[$scope.model.newKey].defaultValue; |
| } |
| if ($scope.configMap.hasOwnProperty($scope.model.newKey) && |
| $scope.configMap[$scope.model.newKey].type === 'java.lang.Boolean' && |
| newConfigValue === null) { |
| newConfigValue = false; |
| } |
| |
| if (!$scope.entityToDeploy.hasOwnProperty(BROOKLYN_CONFIG)) { |
| $scope.entityToDeploy[BROOKLYN_CONFIG] = {}; |
| } |
| $scope.entityToDeploy[BROOKLYN_CONFIG][$scope.model.newKey] = newConfigValue; |
| $scope.focus = $scope.model.newKey; |
| } |
| $scope.model.newConfigFormOpen = false; |
| } |
| |
| function buildYaml() { |
| let newApp = { |
| name: $scope.model.name || $scope.app.displayName, |
| location: $scope.model.location || '<REPLACE>', |
| services: [ |
| angular.copy($scope.entityToDeploy) |
| ] |
| }; |
| return yaml.safeDump(newApp); |
| } |
| |
| function buildComposerYaml(validate) { |
| if ($scope.yamlViewDisplayed) { |
| return angular.copy($scope.editorYaml); |
| } else { |
| 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; |
| } |
| } |
| } |
| |
| 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; |
| } |
| } |
| |
| 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; |
| } |
| } |
| |
| function showEditor() { |
| $scope.editorYaml = buildYaml(); |
| $scope.yamlViewDisplayed = true; |
| } |
| |
| function hideEditor() { |
| $scope.yamlViewDisplayed = false; |
| } |
| |
| function openComposer() { |
| 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() { |
| delete $scope.model.deployError; |
| } |
| } |
| |
| function checkForLocationTags(parsedPlan) { |
| return reduceFunction(false, parsedPlan); |
| |
| function reduceFunction(locationFound, entity) { |
| if (entity.hasOwnProperty('location') || entity.hasOwnProperty('location')) { |
| return true; |
| } |
| if (!locationFound && entity.hasOwnProperty('brooklyn.children')) { |
| entity['brooklyn.children'].reduce(reduceFunction, locationFound); |
| } else if (!locationFound && entity.hasOwnProperty('services')) { |
| entity['services'].reduce(reduceFunction, locationFound); |
| } |
| return locationFound; |
| } |
| } |
| } |