Browse Source

AMBARI-16940. VDF: support for selecting enabled + default Stacks. (Oleg, Joe, Andrii and Jaimin)

Jaimin Jetly 9 years ago
parent
commit
9f93ea6bc5
42 changed files with 2468 additions and 1038 deletions
  1. 2 0
      ambari-admin/src/main/resources/ui/admin-web/app/index.html
  2. 251 54
      ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/stackVersions/StackVersionsCreateCtrl.js
  3. 11 2
      ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/stackVersions/StackVersionsEditCtrl.js
  4. 147 58
      ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/stackVersions/StackVersionsListCtrl.js
  5. 18 1
      ambari-admin/src/main/resources/ui/admin-web/app/scripts/i18n.config.js
  6. 172 0
      ambari-admin/src/main/resources/ui/admin-web/app/scripts/services/AddVersionModal.js
  7. 2 1
      ambari-admin/src/main/resources/ui/admin-web/app/scripts/services/ConfirmationModal.js
  8. 43 22
      ambari-admin/src/main/resources/ui/admin-web/app/scripts/services/Stack.js
  9. 70 0
      ambari-admin/src/main/resources/ui/admin-web/app/scripts/services/Utility.js
  10. 180 11
      ambari-admin/src/main/resources/ui/admin-web/app/styles/main.css
  11. 52 0
      ambari-admin/src/main/resources/ui/admin-web/app/views/modals/AddVersionModal.html
  12. 1 1
      ambari-admin/src/main/resources/ui/admin-web/app/views/modals/ConfirmationModal.html
  13. 23 0
      ambari-admin/src/main/resources/ui/admin-web/app/views/modals/publicRepoDisabled.html
  14. 96 27
      ambari-admin/src/main/resources/ui/admin-web/app/views/stackVersions/list.html
  15. 158 156
      ambari-admin/src/main/resources/ui/admin-web/app/views/stackVersions/stackVersionPage.html
  16. 1 0
      ambari-web/app/assets/test/tests.js
  17. 32 35
      ambari-web/app/controllers/installer.js
  18. 2 2
      ambari-web/app/controllers/wizard/step0_controller.js
  19. 372 45
      ambari-web/app/controllers/wizard/step1_controller.js
  20. 2 1
      ambari-web/app/controllers/wizard/step3_controller.js
  21. 1 1
      ambari-web/app/controllers/wizard/step4_controller.js
  22. 1 1
      ambari-web/app/controllers/wizard/step7_controller.js
  23. 5 5
      ambari-web/app/mappers/stack_mapper.js
  24. 10 2
      ambari-web/app/messages.js
  25. 16 2
      ambari-web/app/models/repository.js
  26. 33 14
      ambari-web/app/models/stack.js
  27. 1 1
      ambari-web/app/models/stack_version/service_simple.js
  28. 2 2
      ambari-web/app/routes/installer.js
  29. 40 25
      ambari-web/app/styles/application.less
  30. 43 0
      ambari-web/app/styles/common.less
  31. 169 157
      ambari-web/app/templates/wizard/step1.hbs
  32. 25 0
      ambari-web/app/templates/wizard/step1/public_option_disabled_window_body.hbs
  33. 44 0
      ambari-web/app/templates/wizard/step1/vdf_upload.hbs
  34. 32 0
      ambari-web/app/utils/array_utils.js
  35. 97 196
      ambari-web/app/views/wizard/step1_view.js
  36. 5 3
      ambari-web/karma.conf.js
  37. 19 0
      ambari-web/test/controllers/installer_test.js
  38. 212 0
      ambari-web/test/controllers/wizard/step1_test.js
  39. 2 2
      ambari-web/test/mixins/common/configs/config_recommendation_parser_test.js
  40. 43 0
      ambari-web/test/utils/array_utils_test.js
  41. 29 14
      ambari-web/test/views/common/host_progress_popup_body_view_test.js
  42. 4 197
      ambari-web/test/views/wizard/step1_view_test.js

+ 2 - 0
ambari-admin/src/main/resources/ui/admin-web/app/index.html

@@ -155,6 +155,7 @@
 <script src="scripts/directives/PasswordVerify.js"></script>
 <script src="scripts/directives/disabledTooltip.js"></script>
 <script src="scripts/directives/editableList.js"></script>
+<script src="scripts/services/Utility.js"></script>
 <script src="scripts/services/UserConstants.js"></script>
 <script src="scripts/services/User.js"></script>
 <script src="scripts/services/Group.js"></script>
@@ -171,6 +172,7 @@
 <script src="scripts/services/UnsavedDialog.js"></script>
 <script src="scripts/services/Stack.js"></script>
 <script src="scripts/services/AddRepositoryModal.js"></script>
+<script src="scripts/services/AddVersionModal.js"></script>
 <script src="scripts/services/RoleDetailsModal.js"></script>
 <!-- endbuild -->
 </body>

+ 251 - 54
ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/stackVersions/StackVersionsCreateCtrl.js

@@ -18,13 +18,17 @@
 'use strict';
 
 angular.module('ambariAdminConsole')
-.controller('StackVersionsCreateCtrl', ['$scope', 'Stack', '$routeParams', '$location', 'Alert', '$translate', 'Cluster', 'AddRepositoryModal', 'ConfirmationModal', function($scope, Stack, $routeParams, $location, Alert, $translate, Cluster, AddRepositoryModal, ConfirmationModal) {
+.controller('StackVersionsCreateCtrl', ['$scope', 'Stack', 'Utility', '$routeParams', '$location', '$timeout' ,'Alert', '$translate', 'Cluster', 'AddRepositoryModal', 'AddVersionModal', 'ConfirmationModal',
+    function($scope, Stack, Utility, $routeParams, $location, $timeout, Alert, $translate, Cluster, AddRepositoryModal, AddVersionModal, ConfirmationModal) {
   var $t = $translate.instant;
   $scope.constants = {
     os: $t('versions.os')
   };
   $scope.createController = true;
   $scope.osList = [];
+  $scope.stackIds = [];
+  $scope.allVersions = [];
+  $scope.networkLost = false;
   $scope.skipValidation = false;
   $scope.useRedhatSatellite = false;
 
@@ -66,18 +70,34 @@ angular.module('ambariAdminConsole')
   /**
    * User can select ONLY one option to upload version definition file
    */
-  $scope.togglePublicLocalOptionSelect = function () {
-    $scope.option1.hasError = false;
-    $scope.option2.hasError = false;
-  };
   $scope.toggleOptionSelect = function () {
     $scope.option1.hasError = false;
     $scope.option2.hasError = false;
   };
+  $scope.isPublicRepoSelected = function () {
+    if ($scope.selectedOption.index == $scope.publicOption.index) return true;
+  };
   $scope.togglePublicLocalOptionSelect = function () {
-    if ($scope.selectedOption.index == $scope.publicOption.index && $scope.selectedPublicRepoVersion) {
-      $scope.setVersionSelected($scope.selectedPublicRepoVersion);
+    if ($scope.selectedOption.index == $scope.publicOption.index) {
+      $scope.setInitialPublicRepoVersions();
+    } else {
+      $scope.clearRepoVersions();
     }
+    $scope.validateRepoUrl();
+  };
+  $scope.setInitialPublicRepoVersions = function () {
+    angular.forEach($scope.osList, function (os) {
+      os.repositories.forEach(function(repo) {
+        repo.Repositories.base_url = repo.Repositories.initial_base_url;
+      });
+    });
+  };
+  $scope.clearRepoVersions = function () {
+    angular.forEach($scope.osList, function (os) {
+      os.repositories.forEach(function(repo) {
+        repo.Repositories.base_url = '';
+      });
+    });
   };
   $scope.clearOptionsError = function () {
     $scope.option1.hasError = false;
@@ -116,6 +136,13 @@ angular.module('ambariAdminConsole')
     }
   };
 
+  /**
+   * On click handler for adding a new version
+   */
+  $scope.addVersion = function() {
+    AddVersionModal.show($scope);
+  };
+
   /**
    * Load selected file to current page content
    */
@@ -154,21 +181,30 @@ angular.module('ambariAdminConsole')
   $scope.afterStackVersionRead = function () {
     Stack.getSupportedOSList($scope.upgradeStack.stack_name, $scope.upgradeStack.stack_version)
       .then(function (data) {
+        var existingOSHash = {};
+        angular.forEach($scope.osList, function (os) {
+          if (angular.isUndefined(os.selected)) {
+            os.selected = true;
+          }
+          existingOSHash[os.OperatingSystems.os_type] = os;
+
+        });
         var operatingSystems = data.operating_systems;
-        operatingSystems.map(function (os) {
-          var existingOSHash = {};
-          angular.forEach($scope.osList, function (os) {
-            existingOSHash[os.OperatingSystems.os_type] = os;
-          });
+        angular.forEach(operatingSystems, function (stackOs) {
           // if os not in the list, mark as un-selected, add this to the osList
-          if (!existingOSHash[os.OperatingSystems.os_type]) {
-            os.selected = false;
-            os.repositories.forEach(function(repo) {
+          if (!existingOSHash[stackOs.OperatingSystems.os_type]) {
+            stackOs.selected = false;
+            stackOs.repositories.forEach(function(repo) {
               repo.Repositories.base_url = '';
+              repo.Repositories.initial_base_url = '';
             });
-            $scope.osList.push(os);
+            $scope.osList.push(stackOs);
           }
         });
+        if ($scope.selectedOption.index == $scope.localOption.index) {
+          $scope.clearRepoVersions();
+          $scope.validateRepoUrl();
+        }
       })
       .catch(function (data) {
         Alert.error($t('versions.alerts.osListError'), data.message);
@@ -192,7 +228,13 @@ angular.module('ambariAdminConsole')
   /**
    * On click handler for adding new OS
    */
-  $scope.addOS = function() {
+  $scope.addOS = function($event) {
+    var dropdownEl = $event.target.parentElement.parentElement;
+    // close the dopdown when an OS is added.
+    $timeout(function () {
+      dropdownEl.click();
+    });
+
     this.os.selected = true;
     if (this.os.repositories) {
       this.os.repositories.forEach(function(repo) {
@@ -208,6 +250,23 @@ angular.module('ambariAdminConsole')
     AddRepositoryModal.show($scope.osList, $scope.upgradeStack.stack_name, $scope.upgradeStack.stack_version, $scope.id);
   };
 
+  $scope.validBaseUrlsExist = function () {
+    var validBaseUrlsExist = true;
+    if ($scope.osList) {
+      $scope.osList.forEach(function(os) {
+        if (os.repositories && os.selected) {
+          os.repositories.forEach(function(repo) {
+            if (repo.invalidBaseUrl) {
+              validBaseUrlsExist = false;
+            }
+          })
+        }
+      });
+    }
+    return validBaseUrlsExist;
+  };
+
+
   $scope.isSaveButtonDisabled = function() {
     var enabled = false;
     $scope.osList.forEach(function(os) {
@@ -215,7 +274,8 @@ angular.module('ambariAdminConsole')
         enabled = true
       }
     });
-    return !enabled;
+    var isRedhatSatelliteSelected = $scope.useRedhatSatellite;
+    return !(isRedhatSatelliteSelected || (enabled && $scope.validBaseUrlsExist()));
   };
 
   $scope.defaulfOSRepos = {};
@@ -239,42 +299,47 @@ angular.module('ambariAdminConsole')
         updateRepoUrl = true;
       }
     });
-    if ($scope.isPublicVersion) {
-      var skip = $scope.skipValidation || $scope.useRedhatSatellite;
-      return Stack.validateBaseUrls(skip, $scope.osList, $scope.upgradeStack).then(function (invalidUrls) {
-        if (invalidUrls.length === 0) {
+
+    var skip = $scope.skipValidation || $scope.useRedhatSatellite;
+    return Stack.validateBaseUrls(skip, $scope.osList, $scope.upgradeStack).then(function (invalidUrls) {
+      if (invalidUrls.length === 0) {
+        if ($scope.isPublicVersion) {
           var data = {
             "VersionDefinition": {
               "available": $scope.id
             }
           };
-          var repoUpdate = {
-            operating_systems: $scope.updateObj.operating_systems
-          };
-          Stack.postVersionDefinitionFile(false, data).then(function (versionInfo) {
-            if (versionInfo.id && versionInfo.stackName && versionInfo.stackVersion) {
-              Stack.updateRepo(versionInfo.stackName, versionInfo.stackVersion, versionInfo.id, repoUpdate).then(function () {
-                Alert.success($t('versions.alerts.versionEdited', {
-                  stackName: $scope.upgradeStack.stack_name,
-                  versionName: $scope.actualVersion,
-                  displayName: $scope.displayName
-                }));
-                $location.path('/stackVersions');
-              }).catch(function (data) {
-                Alert.error($t('versions.alerts.versionUpdateError'), data.message);
-              });
-            }
-          })
-          .catch(function (data) {
-            Alert.error($t('versions.alerts.readVersionInfoError'), data.message);
-          });
+          var isXMLdata = false;
         } else {
-          Stack.highlightInvalidUrls(invalidUrls);
+          var data = $scope.data;
+          var isXMLdata = $scope.isXMLdata;
         }
-      });
-    } else {
-      $scope.updateRepoVersions();
-    }
+
+        var repoUpdate = {
+          operating_systems: $scope.updateObj.operating_systems
+        };
+        Stack.postVersionDefinitionFile(isXMLdata, data, false).then(function (response) {
+          var versionInfo = response.resources[0].VersionDefinition;
+          if (versionInfo.id && versionInfo.stack_name && versionInfo.stack_version) {
+            Stack.updateRepo(versionInfo.stack_name, versionInfo.stack_version, versionInfo.id, repoUpdate).then(function () {
+              Alert.success($t('versions.alerts.versionEdited', {
+                stackName: $scope.upgradeStack.stack_name,
+                versionName: $scope.actualVersion,
+                displayName: $scope.displayName
+              }));
+              $location.path('/stackVersions');
+            }).catch(function (data) {
+              Alert.error($t('versions.alerts.versionUpdateError'), data.message);
+            });
+          }
+        })
+        .catch(function (data) {
+          Alert.error($t('versions.alerts.readVersionInfoError'), data.message);
+        });
+      } else {
+        Stack.highlightInvalidUrls(invalidUrls);
+      }
+    });
   };
 
   $scope.updateRepoVersions = function () {
@@ -324,8 +389,47 @@ angular.module('ambariAdminConsole')
     }
   };
 
-  $scope.clearError = function() {
-    this.repository.hasError = false;
+  $scope.showPublicRepoDisabledDialog = function() {
+    ConfirmationModal.show(
+      $t('versions.networkIssues.publicDisabledHeader'),
+      {
+        "url": 'views/modals/publicRepoDisabled.html'
+      },
+      $t('common.controls.ok'),
+      $t('common.controls.cancel'),
+      true
+    )
+  };
+
+  $scope.onRepoUrlChange = function (repository) {
+    $scope.clearError(repository);
+    $scope.setInvalidUrlError(repository);
+  };
+
+  $scope.undoChange = function(repo) {
+    if ($scope.selectedOption.index == 1) {
+      repo.Repositories.base_url = repo.Repositories.initial_base_url;
+    } else {
+      repo.Repositories.base_url = '';
+    }
+  };
+
+  $scope.clearError = function(repository) {
+    repository.hasError = false;
+  };
+
+  $scope.setInvalidUrlError = function (repository) {
+    repository.invalidBaseUrl =  !$scope.isValidRepoBaseUrl(repository.Repositories.base_url);
+  };
+  /**
+   * Validate base URL
+   * @param {string} value
+   * @returns {boolean}
+   */
+  $scope.isValidRepoBaseUrl = function (value) {
+    var remotePattern = /^(?:(?:https?|ftp):\/{2})(?:\S+(?::\S*)?@)?(?:(?:(?:[\w\-.]))*)(?::[0-9]+)?(?:\/\S*)?$/,
+      localPattern = /^file:\/{2,3}([a-zA-Z][:|]\/){0,1}[\w~!*'();@&=\/\\\-+$,?%#.\[\]]+$/;
+    return remotePattern.test(value) || localPattern.test(value);
   };
 
   $scope.hasValidationErrors = function() {
@@ -372,10 +476,8 @@ angular.module('ambariAdminConsole')
       };
     });
     $scope.repoVersionFullName = response.repoVersionFullName;
-    angular.forEach(response.osList, function (os) {
-      os.selected = true;
-    });
     $scope.osList = response.osList;
+
     // load supported os type base on stack version
     $scope.afterStackVersionRead();
   };
@@ -385,11 +487,106 @@ angular.module('ambariAdminConsole')
     $scope.setVersionSelected(this.version);
   };
 
+  $scope.onStackIdChange = function () {
+    $scope.setStackIdActive(this.stack);
+    $scope.setVisibleStackVersions($scope.allVersions);
+    $scope.setVersionSelected($scope.activeStackVersion);
+  };
+
+  $scope.setStackIdActive =  function (stack) {
+    angular.forEach($scope.stackIds, function(_stack){
+      _stack.isSelected = false;
+    });
+    stack.isSelected = true;
+  };
+
+  $scope.setStackIds = function(stacks) {
+    var stackIds = [];
+    // sort stacks as per per {stack_name}-{stack_version}
+    stacks.sort(function(a,b){
+      if (a.stackName === b.stackName) {
+        var aStackVersion = parseFloat(a.stackVersion);
+        var bStackVersion = parseFloat(b.stackVersion);
+        if (aStackVersion === bStackVersion) {
+          // sort numerically as per per {repository_version}
+          return Utility.compareVersions(a.repositoryVersion, b.repositoryVersion);
+        } else {
+          //sort numerically as per per {stack_version}
+          return aStackVersion > bStackVersion;
+        }
+      } else {
+        //sort lexicographically as per per {stack_name}
+        return  (a.stackName > b.stackName);
+      }
+    }).reverse();
+    angular.forEach(stacks, function (stack) {
+      stackIds.push(stack.stackNameVersion);
+    });
+    $scope.stackIds = stackIds.filter(function(item, index, self){
+      return self.indexOf(item) === index;
+    }).map(function(item){
+      return {
+        stackNameVersion: item,
+        isSelected: false
+      };
+    });
+    $scope.stackIds[0].isSelected = true;
+  };
+
+  $scope.setActiveVersion = function () {
+    $scope.activeStackVersion = this.version;
+    $scope.setVersionSelected($scope.activeStackVersion);
+  };
+
+  $scope.setVisibleStackVersions = function (versions) {
+    var activeStackId = $scope.stackIds.find(function(item){
+      return item.isSelected === true;
+    });
+    angular.forEach(versions, function (item, index) {
+      var isPublicVersionsExist = false;
+      // If public VDF exists for a stack then default base stack version should be hidden
+      if (item.stackDefault) {
+        isPublicVersionsExist = versions.find(function(_version){
+          return (item.stackNameVersion === _version.stackNameVersion && !_version.stackDefault);
+        });
+      }
+      item.visible = (item.stackNameVersion === activeStackId.stackNameVersion) && !isPublicVersionsExist;
+    });
+    $scope.activeStackVersion = versions.filter(function(item){
+      return item.visible;
+    })[0];
+  };
+
+  $scope.setNetworkIssues = function (versions) {
+   $scope.networkLost = !versions.find(function(_version){
+     return !_version.stackDefault;
+   });
+    if ($scope.networkLost) {
+      $scope.selectedOption.index = 2;
+      $scope.clearRepoVersions();
+    }
+  };
+
+  $scope.validateRepoUrl = function () {
+    angular.forEach($scope.osList,function(os){
+      if (os.repositories) {
+        os.repositories.forEach(function(repo) {
+          $scope.onRepoUrlChange(repo);
+        });
+      }
+    });
+  };
+
   $scope.fetchPublicVersions = function () {
     return Stack.allPublicStackVersions().then(function (versions) {
       if (versions && versions.length) {
-        $scope.selectedPublicRepoVersion = versions[0];
-        $scope.setVersionSelected(versions[0]);
+        $scope.setStackIds(versions);
+        $scope.setVisibleStackVersions(versions);
+        $scope.allVersions = versions;
+        $scope.selectedPublicRepoVersion = $scope.activeStackVersion;
+        $scope.setVersionSelected($scope.activeStackVersion);
+        $scope.setNetworkIssues(versions);
+        $scope.validateRepoUrl();
         $scope.availableStackRepoList = versions.length == 1 ? [] : versions;
       }
     });

+ 11 - 2
ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/stackVersions/StackVersionsEditCtrl.js

@@ -36,6 +36,7 @@ angular.module('ambariAdminConsole')
 
   $scope.loadStackVersionInfo = function () {
     return Stack.getRepo($routeParams.versionId, $routeParams.stackName).then(function (response) {
+      $scope.activeStackVersion = response;
       $scope.id = response.id;
       $scope.isPatch = response.type == 'PATCH';
       $scope.stackNameVersion = response.stackNameVersion || $t('common.NA');
@@ -98,6 +99,9 @@ angular.module('ambariAdminConsole')
         operatingSystems.map(function (os) {
           var existingOSHash = {};
           angular.forEach($scope.osList, function (os) {
+            os.repositories.forEach(function(repo) {
+              repo.Repositories.initial_base_url = repo.Repositories.base_url;
+            });
             existingOSHash[os.OperatingSystems.os_type] = os;
           });
           // if os not in the list, mark as un-selected, add this to the osList
@@ -215,7 +219,6 @@ angular.module('ambariAdminConsole')
           });
       });
   };
-  $scope.loadStackVersionInfo();
 
   /**
    * On click handler for removing OS
@@ -281,13 +284,17 @@ angular.module('ambariAdminConsole')
       }
     });
     return !enabled;
-  }
+  };
 
   $scope.cancel = function () {
     $scope.editVersionDisabled = true;
     $location.path('/stackVersions');
   };
 
+  $scope.undoChange = function(repo) {
+    repo.Repositories.base_url = repo.Repositories.initial_base_url;
+  };
+
   $scope.clearErrors = function() {
     if ($scope.osList) {
       $scope.osList.forEach(function(os) {
@@ -388,4 +395,6 @@ angular.module('ambariAdminConsole')
       $scope.stackVersions = stackVersions;
     });
   };
+
+  $scope.loadStackVersionInfo();
 }]);

+ 147 - 58
ambari-admin/src/main/resources/ui/admin-web/app/scripts/controllers/stackVersions/StackVersionsListCtrl.js

@@ -18,66 +18,155 @@
 'use strict';
 
 angular.module('ambariAdminConsole')
-.controller('StackVersionsListCtrl', ['$scope', 'Cluster', 'Stack', '$routeParams', '$translate', function ($scope, Cluster, Stack, $routeParams, $translate) {
-  var $t = $translate.instant;
-  $scope.getConstant = function (key) {
-    return $t('common.' + key).toLowerCase();
-  }
-  $scope.clusterName = $routeParams.clusterName;
-
-  $scope.filter = {
-    version: '',
-    cluster: {
-      options: [],
-      current: null
-    }
-  };
-
-  $scope.pagination = {
-    totalRepos: 100,
-    maxVisiblePages: 1,
-    itemsPerPage: 100,
-    currentPage: 1
-  };
-  $scope.allRepos = [];
-  $scope.stackVersions = [];
-
-  /**
-   *  Formatted object to display all repos:
-   *
-   *  [{ 'name': 'HDP-2.3',
-   *     'repos': ['2.3.6.0-2343', '2.3.4.1', '2.3.4.0-56']
-   *   },
-   *   { 'name': 'HDP-2.2',
-   *     'repos': ['2.2.6.0', '2.2.4.5', '2.2.4.0']
-   *   }
-   *  ]
-   *
-   */
-  $scope.fetchRepos = function () {
-    return Stack.allRepos($scope.filter, $scope.pagination).then(function (repos) {
-      $scope.allRepos = repos.items.sort(function(a, b){return a.repository_version < b.repository_version});
-      var existingStackHash = {};
-      var stackVersions = [];
-      angular.forEach($scope.allRepos, function (repo) {
-        var stackVersionName = repo.stack_name + '-' + repo.stack_version;
-        if (!existingStackHash[stackVersionName]) {
-          existingStackHash[stackVersionName] = true;
-          stackVersions.push({
-            'name': stackVersionName,
-            'isOpened': true,
-            'repos': [repo]
+  .controller('StackVersionsListCtrl', ['$scope', 'Cluster', 'Stack', '$routeParams', '$translate', function ($scope, Cluster, Stack, $routeParams, $translate) {
+    var $t = $translate.instant;
+    $scope.getConstant = function (key) {
+      return $t('common.' + key).toLowerCase();
+    };
+    $scope.clusterName = $routeParams.clusterName;
+    $scope.filter = {
+      name: '',
+      version: '',
+      cluster: {
+        options: [],
+        current: null
+      },
+      stack: {
+        options: [],
+        current: null
+      }
+    };
+    $scope.isNotEmptyFilter = true;
+
+    $scope.pagination = {
+      totalRepos: 10,
+      maxVisiblePages: 20,
+      itemsPerPage: 10,
+      currentPage: 1
+    };
+
+    $scope.tableInfo = {
+      total: 0,
+      showed: 0,
+      filtered: 0
+    };
+
+    $scope.repos = [];
+    $scope.dropDownClusters = [];
+    $scope.selectedCluster = $scope.dropDownClusters[0];
+
+    $scope.resetPagination = function () {
+      $scope.pagination.currentPage = 1;
+      $scope.loadAllData();
+    };
+
+    $scope.pageChanged = function () {
+      $scope.loadAllData();
+    };
+
+    $scope.goToCluster = function() {
+      window.location.replace('/#/main/admin/stack/versions');
+    };
+
+    $scope.clearFilters = function () {
+      $scope.filter.name = '';
+      $scope.filter.version = '';
+      $scope.filter.cluster.current = $scope.filter.cluster.options[0];
+      $scope.filter.stack.current = $scope.filter.stack.options[0];
+      $scope.resetPagination();
+    };
+
+    $scope.fetchRepoClusterStatus = function () {
+      var clusterName = ($scope.clusters && $scope.clusters.length > 0) ? $scope.clusters[0].Clusters.cluster_name : null; // only support one cluster at the moment
+      if (clusterName) {
+        angular.forEach($scope.repos, function (repo) {
+          Cluster.getRepoVersionStatus(clusterName, repo.id).then(function (response) {
+            repo.status = response.status;
+            repo.totalHosts = response.totalHosts;
+            repo.currentHosts = response.currentHosts;
+            repo.installedHosts = response.installedHosts;
+            repo.stackVersionId = response.stackVersionId;
+            repo.cluster = (repo.status == 'current' || repo.status == 'installed') ? clusterName : '';
           });
-        } else {
-          if (stackVersions[stackVersions.length -1].repos) {
-            stackVersions[stackVersions.length -1].repos.push(repo);
-          }
+        });
+      }
+    };
+
+    $scope.fetchRepos = function () {
+      return Stack.allRepos($scope.filter, $scope.pagination).then(function (repos) {
+        $scope.pagination.totalRepos = repos.itemTotal;
+        $scope.repos = repos.items;
+        $scope.tableInfo.total = repos.itemTotal;
+        $scope.tableInfo.showed = repos.showed;
+      });
+    };
+
+    $scope.fillClusters = function (clusters) {
+      $scope.dropDownClusters = [].concat(clusters);
+      var options = [{label: $t('common.all'), value: ''}];
+      angular.forEach(clusters, function (cluster) {
+        options.push({
+          label: cluster.Clusters.cluster_name,
+          value: cluster.Clusters.cluster_name
+        });
+      });
+      $scope.filter.cluster.options = options;
+      if (!$scope.filter.cluster.current) {
+        $scope.filter.cluster.current = options[0];
+      }
+    };
+
+    $scope.fetchClusters = function () {
+      return Cluster.getAllClusters().then(function (clusters) {
+        if (clusters && clusters.length > 0) {
+          $scope.clusters = clusters;
+          $scope.fillClusters(clusters);
         }
       });
-      $scope.stackVersions = stackVersions;
-    });
-  };
+    };
+
+    $scope.fetchStacks = function () {
+      return Stack.allStackVersions().then(function (clusters) {
+        if (clusters && clusters.length > 0) {
+          $scope.stacks = clusters;
+          $scope.fillStacks(clusters);
+        }
+      });
+    };
+
+    $scope.fillStacks = function() {
+      var options = [{label: $t('common.all'), value: ''}];
+      angular.forEach($scope.stacks, function (stack) {
+        options.push({
+          label: stack.displayName,
+          value: stack.displayName
+        });
+      });
+      $scope.filter.stack.options = options;
+      if (!$scope.filter.stack.current) {
+        $scope.filter.stack.current = options[0];
+      }
+    };
+
+    $scope.loadAllData = function () {
+      $scope.fetchStacks()
+        .then(function () {
+          return $scope.fetchClusters();
+        })
+        .then(function () {
+          return $scope.fetchRepos();
+        })
+        .then(function () {
+          $scope.fetchRepoClusterStatus();
+        });
+    };
 
-  $scope.fetchRepos();
+    $scope.loadAllData();
 
-}]);
+    $scope.$watch('filter', function (filter) {
+      $scope.isNotEmptyFilter = Boolean(filter.name
+        || filter.version
+        || (filter.cluster.current && filter.cluster.current.value)
+        || (filter.stack.current && filter.stack.current.value));
+    }, true);
+  }]);

+ 18 - 1
ambari-admin/src/main/resources/ui/admin-web/app/scripts/i18n.config.js

@@ -38,6 +38,7 @@ angular.module('ambariAdminConsole')
       'users': 'Users',
       'groups': 'Groups',
       'versions': 'Versions',
+      'stack': 'Stack',
       'details': 'Details',
       'goToDashboard': 'Go to Dashboard',
       'noClusters': 'No Clusters',
@@ -89,6 +90,7 @@ angular.module('ambariAdminConsole')
       'listViewLabel': 'LIST',
       'rbac': 'Role Based Access Control',
       'important': 'Important',
+      'undo': 'Undo',
 
       'clusterNameChangeConfirmation': {
         'title': 'Confirm Cluster Name Change',
@@ -372,9 +374,19 @@ angular.module('ambariAdminConsole')
 
     'versions': {
       'current': 'Current',
+      'addVersion': 'Add Version',
+      'defaultVersion': '(Default Version Definition)',
       'inUse': 'In Use',
       'installed': 'Installed',
       'usePublic': "Use Public Repository",
+      'networkIssues': {
+        'networkLost': "Why is this disabled?",
+        'publicDisabledHeader': "Public Repository Option Disabled",
+        'publicRepoDisabledMsg': 'Ambari does not have access to the Internet and cannot use the Public Repository for installing the software. Your Options:',
+        'publicRepoDisabledMsg1': 'Configure your hosts for access to the Internet.',
+        'publicRepoDisabledMsg2': 'If you are using an Internet Proxy, refer to the Ambari Documentation on how to configure Ambari to use the Internet Proxy.',
+        'publicRepoDisabledMsg3': 'Use the Local Repositoy option.'
+      },
       'selectVersion': "Select Version",
       'selectVersionEmpty': "No other repositories",
       'useLocal': "Use Local Repository",
@@ -406,11 +418,15 @@ angular.module('ambariAdminConsole')
         'actualVersion': 'Actual Version',
         'releaseNotes': 'Release Notes'
       },
+      'repository': {
+        'placeholder': 'Enter Base URL or remove this OS'
+      },
       'useRedhatSatellite': {
         'title': 'Use RedHat Satellite/Spacewalk',
         'warning': 'By selecting to <b>"Use RedHat Satellite/Spacewalk"</b> for the software repositories, ' +
         'you are responsible for configuring the repository channel in Satellite/Spacewalk and confirming the repositories for the selected <b>stack version</b> are available on the hosts in the cluster. ' +
-        'Refer to the Ambari documentation for more information.'
+        'Refer to the Ambari documentation for more information.',
+        'disabledMsg': 'Use of RedHat Satellite/Spacewalk is not available when using Public Repositories'
       },
       'changeBaseURLConfirmation': {
         'title': 'Confirm Base URL Change',
@@ -425,6 +441,7 @@ angular.module('ambariAdminConsole')
         'filterListError': 'Fetch stack version filter list error',
         'versionCreated': 'Created version <a href="#/stackVersions/{{stackName}}/{{versionName}}/edit">{{stackName}}-{{versionName}}</a>',
         'versionCreationError': 'Version creation error',
+        'allOsAdded': 'All Operating Systems have been added',
         'osListError': 'getSupportedOSList error',
         'readVersionInfoError': 'Version Definition read error',
         'versionEdited': 'Edited version <a href="#/stackVersions/{{stackName}}/{{versionName}}/edit">{{displayName}}</a>',

+ 172 - 0
ambari-admin/src/main/resources/ui/admin-web/app/scripts/services/AddVersionModal.js

@@ -0,0 +1,172 @@
+/**
+ * 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.
+ */
+'use strict';
+
+angular.module('ambariAdminConsole')
+  .factory('AddVersionModal', ['$modal', '$q', function($modal, $q) {
+    var modalObject = {};
+
+    modalObject.repoExists = function(existingRepos, repoId) {
+      for(var i = existingRepos.length - 1; i >= 0; --i) {
+        if (existingRepos[i].Repositories.repo_id === repoId) {
+          return true;
+        }
+      }
+      return false;
+    };
+
+    modalObject.getRepositoriesForOS = function (osList, selectedOS) {
+      // Get existing list of repositories for selectedOS
+      for (var i = osList.length - 1; i >= 0; --i) {
+        if (osList[i].OperatingSystems.os_type === selectedOS) {
+          osList[i].repositories = osList[i].repositories || [];
+          return osList[i].repositories;
+        }
+      }
+      return null;
+    };
+
+    modalObject.show = function (parentScope) {
+      var deferred = $q.defer();
+      var modalInstance = $modal.open({
+        templateUrl: 'views/modals/AddVersionModal.html',
+        controller: ['$scope', '$modalInstance', '$translate', 'Stack', 'Alert', function ($scope, $modalInstance, $translate, Stack, Alert) {
+          var $t = $translate.instant;
+          $scope.selectedLocalOption = {
+            index: 1
+          };
+          $scope.option1 = {
+            index: 1,
+            displayName: $t('versions.uploadFile'),
+            file: ''
+          };
+          $scope.option2 = {
+            index: 2,
+            displayName: $t('versions.enterURL'),
+            url: "",
+            placeholder: "Enter URL to Version Definition File",
+          };
+          $scope.readInfoButtonDisabled = function () {
+            return $scope.option1.index == $scope.selectedLocalOption.index ? !$scope.option1.file : !$scope.option2.url;
+          };
+          $scope.onFileSelect = function(e){
+            if (e.files && e.files.length == 1) {
+              var file = e.files[0];
+              var reader = new FileReader();
+              reader.onload = (function () {
+                return function (e) {
+                  $scope.option1.file = e.target.result;
+                };
+              })(file);
+              reader.readAsText(file);
+            } else {
+              $scope.option1.file = '';
+            }
+          };
+          /**
+           * Load selected file to current page content
+           */
+          $scope.readVersionInfo = function(){
+            var data = {};
+            var isXMLdata = false;
+            if ($scope.option2.index == $scope.selectedLocalOption.index) {
+              var url = $scope.option2.url;
+              data = {
+                "VersionDefinition": {
+                  "version_url": url
+                }
+              };
+            } else if ($scope.option1.index == $scope.selectedLocalOption.index) {
+              isXMLdata = true;
+              // load from file browser
+              data = $scope.option1.file;
+            }
+            parentScope.isXMLdata = isXMLdata;
+            parentScope.data = data;
+
+            return Stack.postVersionDefinitionFile(isXMLdata, data, true).then(function (versionInfo) {
+              var repo = versionInfo.resources[0];
+              var response = {
+                id : repo.VersionDefinition.id,
+                stackVersion : repo.VersionDefinition.stack_version,
+                stackName: repo.VersionDefinition.stack_name,
+                type: repo.VersionDefinition.release? repo.VersionDefinition.release.type: null,
+                stackNameVersion: repo.VersionDefinition.stack_name + '-' + repo.VersionDefinition.stack_version, /// HDP-2.3
+                stackNameRepositoryVersion: repo.VersionDefinition.stack_name + '-' + repo.VersionDefinition.repository_version,
+                actualVersion: repo.VersionDefinition.repository_version, /// 2.3.4.0-3846
+                version: repo.VersionDefinition.release ? repo.VersionDefinition.release.version: null, /// 2.3.4.0
+                releaseNotes: repo.VersionDefinition.release ? repo.VersionDefinition.release.release_notes: null,
+                displayName: repo.VersionDefinition.stack_name + '-' + repo.VersionDefinition.repository_version, //HDP-2.3.4.0
+                repoVersionFullName : repo.VersionDefinition.stack_name + '-' + repo.VersionDefinition.release ? repo.VersionDefinition.release.version: repo.VersionDefinition.repository_version,
+                ambari_managed_repositories: repo.operating_systems[0].OperatingSystems.ambari_managed_repositories !== false,
+                osList: repo.operating_systems,
+                updateObj: repo
+              };
+              var services = [];
+              angular.forEach(repo.VersionDefinition.stack_services, function (service) {
+                var servicesToExclude = ['GANGLIA', 'KERBEROS', 'MAPREDUCE2'];
+                if (servicesToExclude.indexOf(service.name) === -1) {
+                  services.push({
+                    name: service.name,
+                    version: service.versions[0],
+                    displayName: service.display_name
+                  });
+                }
+              });
+              response.services = services.sort(function(a, b){return a.name.localeCompare(b.name)});
+              response.osList.forEach(function (os) {
+                os.repositories.forEach(function(repo) {
+                  repo.Repositories.initial_base_url = repo.Repositories.base_url;
+                });
+              });
+
+              angular.forEach(parentScope.stackIds, function(stack){
+                if (stack.stackNameVersion == response.stackNameVersion) {
+                  parentScope.setStackIdActive(stack);
+                }
+              });
+              angular.forEach(parentScope.allVersions, function(version) {
+                version.visible = (version.stackNameVersion === response.stackNameVersion);
+              });
+              parentScope.allVersions.push(response);
+              parentScope.activeStackVersion = response;
+              parentScope.selectedPublicRepoVersion = response;
+              parentScope.setVersionSelected(response);
+              $modalInstance.close();
+              deferred.resolve();
+            }).catch(function (data) {
+              Alert.error($t('versions.alerts.readVersionInfoError'), data.message);
+            });
+          };
+          $scope.cancel = function () {
+            $modalInstance.dismiss();
+            deferred.reject();
+          };
+        }]
+      });
+      modalInstance.result.then(function () {
+        // Gets triggered on close
+      }, function () {
+        // Gets triggered on dismiss
+        deferred.reject();
+      });
+      return deferred.promise;
+    };
+
+    return modalObject;
+  }]);

+ 2 - 1
ambari-admin/src/main/resources/ui/admin-web/app/scripts/services/ConfirmationModal.js

@@ -23,7 +23,7 @@ angular.module('ambariAdminConsole')
   var $t = $translate.instant;
 
 	return {
-		show: function(header, body, confirmText, cancelText) {
+		show: function(header, body, confirmText, cancelText, hideCancelButton) {
 			var deferred = $q.defer();
 
 			var modalInstance = $modal.open({
@@ -35,6 +35,7 @@ angular.module('ambariAdminConsole')
           $scope.innerScope = body.scope;
           $scope.confirmText = confirmText || $t('common.controls.ok');
           $scope.cancelText = cancelText || $t('common.controls.cancel');
+					$scope.showCancelButton = !hideCancelButton;
 
 					$scope.ok = function() {
 						$modalInstance.close();

+ 43 - 22
ambari-admin/src/main/resources/ui/admin-web/app/scripts/services/Stack.js

@@ -81,8 +81,8 @@ angular.module('ambariAdminConsole')
     },
 
     allPublicStackVersions: function() {
-      var url = '/version_definitions?fields=operating_systems/repositories/Repositories/*,VersionDefinition/stack_services,VersionDefinition/repository_version' +
-        '&VersionDefinition/show_available=true&VersionDefinition/stack_name=HDP';
+      var url = '/version_definitions?fields=VersionDefinition/stack_default,operating_systems/repositories/Repositories/*,VersionDefinition/stack_services,VersionDefinition/repository_version' +
+        '&VersionDefinition/show_available=true';
       var deferred = $q.defer();
       $http.get(Settings.baseUrl + url, {mock: 'version/versions.json'})
         .success(function (data) {
@@ -92,22 +92,34 @@ angular.module('ambariAdminConsole')
               id: version.VersionDefinition.id,
               stackName: version.VersionDefinition.stack_name,
               stackVersion: version.VersionDefinition.stack_version,
+              stackDefault: version.VersionDefinition.stack_default,
               stackNameVersion:  version.VersionDefinition.stack_name + '-' + version.VersionDefinition.stack_version,
               displayName: version.VersionDefinition.stack_name + '-' + version.VersionDefinition.repository_version.split('-')[0], //HDP-2.3.4.0
               displayNameFull: version.VersionDefinition.stack_name + '-' + version.VersionDefinition.repository_version, //HDP-2.3.4.0-23
               repositoryVersion: version.VersionDefinition.repository_version,
+              stackNameRepositoryVersion: version.VersionDefinition.stack_name + '-' + version.VersionDefinition.repository_version,
               showAvailable: version.VersionDefinition.show_available,
               osList: version.operating_systems,
               updateObj: version
             };
             var services = [];
             angular.forEach(version.VersionDefinition.stack_services, function (service) {
-              services.push({
-                name: service.name,
-                version: service.versions[0]
+              // services that should not be shown on UI
+              var servicesToExclude = ['GANGLIA', 'KERBEROS', 'MAPREDUCE2'];
+              if (servicesToExclude.indexOf(service.name) === -1) {
+                services.push({
+                  name: service.name,
+                  displayName: service.display_name,
+                  version: service.versions[0]
+                });
+              }
+            });
+            versionObj.services = services.sort(function(a, b){return a.name.localeCompare(b.name)});
+            versionObj.osList.forEach(function (os) {
+              os.repositories.forEach(function(repo) {
+                repo.Repositories.initial_base_url = repo.Repositories.base_url;
               });
             });
-            versionObj.services = services;
             versions.push(versionObj);
           });
           deferred.resolve(versions)
@@ -120,9 +132,21 @@ angular.module('ambariAdminConsole')
 
     allRepos: function (filter, pagination) {
       var versionFilter = filter.version;
+      var nameFilter = filter.name;
+      var stackFilter = filter.stack && filter.stack.current && filter.stack.current.value;
       var url = '/stacks?fields=versions/repository_versions/RepositoryVersions';
       if (versionFilter) {
-        url += '&versions/repository_versions/RepositoryVersions/display_name.matches(.*' + versionFilter + '.*)';
+        url += '&versions/repository_versions/RepositoryVersions/repository_version.matches(.*' + versionFilter + '.*)';
+      }
+      if (nameFilter) {
+        url += '&versions/repository_versions/RepositoryVersions/display_name.matches(.*' + nameFilter + '.*)';
+      }
+      if (stackFilter) {
+        var stack = filter.stack.current.value.split('-'),
+          stackNameFilter = stack[0],
+          stackVersionFilter = stack[1];
+        url += '&versions/repository_versions/RepositoryVersions/stack_name=' + stackNameFilter;
+        url += '&versions/repository_versions/RepositoryVersions/stack_version=' + stackVersionFilter;
       }
       var deferred = $q.defer();
       $http.get(Settings.baseUrl + url, {mock: 'version/versions.json'})
@@ -213,8 +237,7 @@ angular.module('ambariAdminConsole')
           actualVersion: data.repository_versions[0].RepositoryVersions.repository_version, /// 2.3.4.0-3846
           version: data.repository_versions[0].RepositoryVersions.release ? data.repository_versions[0].RepositoryVersions.release.version: null, /// 2.3.4.0
           releaseNotes: data.repository_versions[0].RepositoryVersions.release ? data.repository_versions[0].RepositoryVersions.release.release_notes: null,
-          displayName: data.repository_versions[0].RepositoryVersions.release ? data.Versions.stack_name + '-' + data.repository_versions[0].RepositoryVersions.release.version :
-            data.Versions.stack_name + '-' + data.repository_versions[0].RepositoryVersions.repository_version.split('-')[0], //HDP-2.3.4.0
+          displayName: data.repository_versions[0].RepositoryVersions.display_name, //HDP-2.3.4.0
           repoVersionFullName : data.Versions.stack_name + '-' + data.repository_versions[0].RepositoryVersions.repository_version,
           ambari_managed_repositories: data.repository_versions[0].operating_systems[0].OperatingSystems.ambari_managed_repositories !== false,
           osList: data.repository_versions[0].operating_systems,
@@ -222,11 +245,14 @@ angular.module('ambariAdminConsole')
         };
         var services = [];
         angular.forEach(data.repository_versions[0].RepositoryVersions.stack_services, function (service) {
-          services.push({
-            name: service.name,
-            version: service.versions[0],
-            displayName: service.display_name
-          });
+          var servicesToExclude = ['GANGLIA', 'KERBEROS', 'MAPREDUCE2'];
+          if (servicesToExclude.indexOf(service.name) === -1) {
+            services.push({
+              name: service.name,
+              version: service.versions[0],
+              displayName: service.display_name
+            });
+          }
         });
         response.services = services.sort(function(a, b){return a.name.localeCompare(b.name)});
         deferred.resolve(response);
@@ -237,20 +263,15 @@ angular.module('ambariAdminConsole')
       return deferred.promise;
     },
 
-    postVersionDefinitionFile: function (isXMLdata, data) {
+    postVersionDefinitionFile: function (isXMLdata, data, isDryRun) {
       var deferred = $q.defer(),
-        url = Settings.baseUrl + '/version_definitions',
+        url = Settings.baseUrl + '/version_definitions' + (isDryRun ? '?dry_run=true' : ''),
         configs = isXMLdata? { headers: {'Content-Type': 'text/xml'}} : null;
 
       $http.post(url, data, configs)
         .success(function (response) {
           if (response.resources.length && response.resources[0].VersionDefinition) {
-            deferred.resolve(
-              {
-                stackName: response.resources[0].VersionDefinition.stack_name,
-                id: response.resources[0].VersionDefinition.id,
-                stackVersion: response.resources[0].VersionDefinition.stack_version
-              });
+            deferred.resolve(response);
           }
         })
         .error(function (data) {

+ 70 - 0
ambari-admin/src/main/resources/ui/admin-web/app/scripts/services/Utility.js

@@ -0,0 +1,70 @@
+/**
+ * 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.
+ */
+'use strict';
+/**
+ * This service should be used to keep all utility functions in one place that can be used in any controller
+ */
+angular.module('ambariAdminConsole')
+  .factory('Utility', [function() {
+    return {
+      /**
+       *  if version1>= version2 then return true
+       *     version1 < version2 then return false
+       * @param version1 {String}
+       * @param version2 {String}
+       * @return boolean
+       */
+      compareVersions: function(version1, version2) {
+        version1 = version1 || '0';
+        version2 = version2 || '0';
+        var version1Arr = version1.split('.').map(function(item){
+          return parseInt(item);
+        }).filter(function(item){
+          return !!item || item === 0;
+        });
+        var version2Arr = version2.split('.').map(function(item){
+          return parseInt(item);
+        }).filter(function(item){
+          return !!item || item === 0;
+        });
+        var totalLength = Math.max(version1Arr.length, version2Arr.length);
+        var result = true, i;
+        for (i = 0; i <=totalLength; i++) {
+          if (version2Arr[i] === undefined) {
+            // Example: version1 = "2.3.2.2" and version2 = 2.3.2
+            result = true;
+            break;
+          } else if (version1Arr[i] === undefined) {
+            // Example: version1 = "2.3.2" and version2 = "2.3.2.2"
+            result = false;
+            break;
+          } else if (version1Arr[i] > version2Arr[i]) {
+            // Example: version1 = "2.3.2.2" and version2 = "2.3.2.1"
+            result = true;
+            break;
+          } else if (version1Arr[i] < version2Arr[i]) {
+            // Example: version1 = "2.3.1.2" and version2 = "2.3.2.1"
+            result = false;
+            break;
+          }
+        }
+        return result;
+      }
+    };
+  }
+]);

+ 180 - 11
ambari-admin/src/main/resources/ui/admin-web/app/styles/main.css

@@ -1372,9 +1372,48 @@ accordion .panel-group .panel{
   text-align: right;
 }
 
-.name-label-adjust {
+.repo-table-title {
+  padding-left: 5px;
+  padding-bottom: 10px;
+  border-bottom: 1px solid #ebebeb;
+}
+
+.repo-table-title label {
+  margin-top: 5px;
+}
+
+.repo-table-title #os-label {
+  padding-left: 7px;
+}
+
+.repo-table-title .add-os-button.disabled {
+  cursor: not-allowed;
+}
+
+.advanced-radio-buttons {
+  margin-top: 15px;
+}
+
+.advanced-radio-buttons i {
+  color: #0572ff;
+}
+
+.advanced-radio-buttons span.disabled {
+  opacity: 0.7;
+  cursor: default;
+}
+
+.repo-table-title #name-label-adjust {
   width: 20.7%;
+  padding-left:0px;
+  right:5px;
+}
+
+.repo-table-title #repo-base-url-label {
+  padding-left:0px;
+  right:3px;
 }
+
 .verison-label-row .label {
   font-size: 100%;
 }
@@ -1477,15 +1516,85 @@ thead.view-permission-header > tr > th {
 .left-menu-all-repos .repos-table .repos-td.active > a {
   color: white;
 }
-#upload-definition-file-panel {
-  padding: 5px 10px 5px 10px;
-  margin-bottom: 15px;
+#list-stack-id {
+  padding-left: 0px;
+  padding-right: 0px;
+  margin-left: 0px;
+}
+.tabs-left, .tabs-right {
+  border-bottom: none;
+  padding-top: 2px;
+}
+.tabs-left {
+  border-right: 1px solid #ddd;
+}
+.tabs-right {
+  border-left: 1px solid #ddd;
+}
+.tabs-left>li, .tabs-right>li {
+  float: none;
+  margin-bottom: 2px;
+}
+.tabs-left>li {
+  margin-right: -1px;
+}
+.tabs-right>li {
+  margin-left: -1px;
+}
+.tabs-left>li.active>a,
+.tabs-left>li.active>a:hover,
+.tabs-left>li.active>a:focus {
+  border-bottom-color: #ddd;
+  border-right-color: transparent;
+}
+
+.tabs-right>li.active>a,
+.tabs-right>li.active>a:hover,
+.tabs-right>li.active>a:focus {
+  border-bottom: 1px solid #ddd;
+  border-left-color: transparent;
+}
+.tabs-left>li>a {
+  border-radius: 4px 0 0 4px;
+  margin-right: 0;
+  display:block;
+}
+.tabs-right>li>a {
+  border-radius: 0 4px 4px 0;
+  margin-right: 0;
+}
+
+.public-disabled-option {
+  padding: 5px;
+  padding-left: 15px;
+}
+
+#upload-definition-file-panel .big-radio {
+  font-weight: bold;
+  padding: 5px 15px;
+  margin-left: -18px;
 }
 
-#upload-definition-file-panel .option-radio {
-  padding: 15px 5px;
-  font-weight: bold;;
+#upload-definition-file-panel .big-radio #public-disabled-link {
+  margin-left: 10px;
+  font-weight: normal;
+  cursor: pointer;
 }
+
+#current-stack-details {
+  bottom: 20px;
+  padding-left: 0px;
+}
+
+#current-stack-details .table-borderless {
+ border: transparent;
+}
+
+#current-stack-details .table-borderless tbody tr td, .table-borderless tbody tr th, .table-borderless thead tr th {
+  border: none;
+}
+
+
 #upload-definition-file-panel .stack-version-selection {
    padding-left: 25px;
  }
@@ -1510,9 +1619,9 @@ thead.view-permission-header > tr > th {
 .register-version-options .option-radio-button label {
   font-weight: normal;
 }
-.register-version-options .choose-file-input {
-  padding-top: 6px;
-  padding-bottom: 20px;
+.register-version-options .choose-file-input input {
+  padding-top: 3px;
+  padding-bottom: 3px;
 }
 .register-version-form .details-panel .patch-icon {
   color: #ff4500;
@@ -1525,9 +1634,43 @@ thead.view-permission-header > tr > th {
   margin-top: 0;
   margin-bottom: 0;
 }
+
+.details-panel {
+  border: 1px solid #e5e5e5;
+}
+
+.details-panel .version-contents-section {
+  border: 1px solid #ddd;
+  max-height: 200px;
+  overflow: auto;
+}
+
+.details-panel .version-contents-section-register-version {
+  margin: 8px 0;
+}
+
+.details-panel .version-contents-section .table {
+  margin-bottom: 0;
+}
+
+.details-panel .version-contents-section .table tr:first-child td {
+  border-top: none;
+}
+
 .register-version-form .details-panel .version-info-section {
   margin-top: 10px;
 }
+
+.register-version-options .stack-url-input input.disabled {
+  background-color: #eee;
+}
+
+.register-version-options .choose-file-input input.disabled {
+  font-weight: normal;
+  cursor: default;
+  background-color: #eee;
+}
+
 .register-version-form .details-panel .version-contents-section {
   max-height: 200px;
   overflow: auto;
@@ -1553,6 +1696,7 @@ thead.view-permission-header > tr > th {
   padding: 0;
   text-align: center;
   cursor: pointer;
+  width: 5.982905982905983%;
 }
 .register-version-form .repos-panel .remove-icon.disabled{
   color: grey;
@@ -1560,11 +1704,32 @@ thead.view-permission-header > tr > th {
 
 .register-version-form .repos-panel .repo-name-label {
   text-align: left;
+  padding-top: 10px;
+  padding-left: 0px;
+}
+
+.register-version-form .repos-panel .repo-name-url {
+  padding: 8px 0px;
+  margin-bottom: 0px;
 }
 
 .register-version-form .repos-panel .os-type-label {
-  margin-top: 27px;;
+  margin-top: 27px;
+  padding-left: 10px;
 }
+
+.register-version-form .repos-panel .repo-url {
+  padding-right: 5px;
+}
+
+.register-version-form .repos-panel .repo-url input {
+  padding: 4px;
+}
+
+.register-version-form .repos-panel .fa-undo {
+  margin-top: 10px;
+}
+
 .register-version-form .dropdown-menu li a {
   cursor: pointer;
 }
@@ -1588,6 +1753,10 @@ thead.view-permission-header > tr > th {
   color: #5ab400;
 }
 
+.orange-icon {
+  color: #f3b20b;
+}
+
 .cursor-pointer {
   cursor: pointer;
 }

+ 52 - 0
ambari-admin/src/main/resources/ui/admin-web/app/views/modals/AddVersionModal.html

@@ -0,0 +1,52 @@
+<!--
+* 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.
+-->
+<div class="modal-header">
+    <h3 class="modal-title">Add Version</h3>
+</div>
+<br>
+<div class="clearfix register-version-options" ng-click="selectedLocalOption.index=1">
+    <div class="option-radio-button">
+        <label class="option-label">
+            <input type="radio" ng-model="selectedLocalOption.index" value="1"> {{'versions.uploadFile' | translate}}
+        </label>
+    </div>
+    <div class="col-sm-7 choose-file-input">
+        <input type="file" ng-model="option1.file" ng-class="selectedLocalOption.index!='1' ? 'disabled' : ''"
+               onchange="angular.element(this).scope().onFileSelect(this)"/>
+    </div>
+</div>
+<div class="clearfix register-version-options bottom-margin" ng-click="selectedLocalOption.index=2">
+    <div class="option-radio-button">
+        <label class="option-label">
+            <input type="radio" ng-model="selectedLocalOption.index" value="2"> {{'versions.enterURL' | translate}}
+        </label>
+    </div>
+    <div class="col-sm-9">
+        <div class="form-group {{option2.name}}" ng-class="{'has-error': option2.url.hasError }">
+            <div class="stack-url-input">
+                <input type="text" class="form-control" ng-model="option2.url"
+                       placeholder="{{option2.placeholder}}" ng-class="selectedLocalOption.index!='2' ? 'disabled' : ''">
+            </div>
+        </div>
+    </div>
+</div>
+<div class="modal-footer">
+    <button class="btn btn-default" ng-click="cancel()">Cancel</button>
+    <button class="btn btn-primary" ng-model="button" ng-click="readVersionInfo()"
+            ng-disabled="readInfoButtonDisabled()">{{'versions.readInfo' | translate}}</button>
+</div>

+ 1 - 1
ambari-admin/src/main/resources/ui/admin-web/app/views/modals/ConfirmationModal.html

@@ -23,6 +23,6 @@
   <div ng-show="isTempalte" ng-include="body.url"></div>
 </div>
 <div class="modal-footer">
-    <button class="btn btn-default" ng-click="cancel()">{{cancelText}}</button>
+    <button class="btn btn-default" ng-if="showCancelButton" ng-click="cancel()">{{cancelText}}</button>
     <button class="btn btn-primary" ng-click="ok()">{{confirmText}}</button>
 </div>

+ 23 - 0
ambari-admin/src/main/resources/ui/admin-web/app/views/modals/publicRepoDisabled.html

@@ -0,0 +1,23 @@
+<!--
+* 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.
+-->
+<div class="public-disabled-message" translate="versions.networkIssues.publicRepoDisabledMsg"></div>
+<ul>
+  <li class="public-disabled-option" translate="versions.networkIssues.publicRepoDisabledMsg1"></li>
+  <li class="public-disabled-option" translate="versions.networkIssues.publicRepoDisabledMsg2"></li>
+  <li class="public-disabled-option" translate="versions.networkIssues.publicRepoDisabledMsg3"></li>
+</ul>

+ 96 - 27
ambari-admin/src/main/resources/ui/admin-web/app/views/stackVersions/list.html

@@ -29,34 +29,103 @@
     </div>
   </div>
   <hr/>
-
-  <accordion close-others="false" class="col-sm-2 left-menu-all-repos">
-    <accordion-group ng-repeat="stackVersion in stackVersions" is-open="stackVersion.isOpened">
-      <accordion-heading>
-        <div class="row stack-version-title">
-          <i class="glyphicon glyphicon-chevron-right" ng-class="{'opened': stackVersion.isOpened}"></i>
-          {{stackVersion.name}}
+  <table class="table table-striped table-hover">
+    <thead>
+    <tr>
+      <th class="col-small">
+        <label>{{'common.stack' | translate}}</label>
+        <select class="form-control"
+                ng-change="resetPagination()"
+                ng-model="filter.stack.current"
+                ng-options="item.label for item in filter.stack.options track by item.value"
+          ></select>
+      </th>
+      <th class="col-medium">
+        <label>{{'common.name' | translate}}</label>
+        <input type="text" class="form-control" ng-change="resetPagination()" ng-model="filter.name" placeholder="{{'common.any' | translate}}">
+      </th>
+      <th class="col-medium">
+        <label>{{'common.version' | translate}}</label>
+        <input type="text" class="form-control" ng-change="resetPagination()" ng-model="filter.version" placeholder="{{'common.any' | translate}}">
+      </th>
+      <th class="col-small">
+        <label>{{'common.cluster' | translate}}</label>
+        <select class="form-control"
+                ng-change="resetPagination()"
+                ng-model="filter.cluster.current"
+                ng-options="item.label for item in filter.cluster.options track by item.value"
+          ></select>
+      </th>
+      <th></th>
+    </tr>
+    </thead>
+    <tbody>
+    <tr ng-repeat="repo in repos">
+      <td class="col-small">
+        <span>{{repo.stack_name}}-{{repo.stack_version}}</span>
+      </td>
+      <td class="col-medium">
+        <a href="#/stackVersions/{{repo.stack_name}}/{{repo.repository_version}}/edit">{{repo.display_name}}</a>
+      </td>
+      <td class="col-medium">
+        <span>{{repo.repository_version}}</span>
+      </td>
+      <td class="col-small">
+        <a href="/#/main/admin/stack/versions" ng-show="repo.cluster">
+          {{repo.cluster}}
+        </a>
+        <span ng-show="!repo.cluster">
+          {{'common.none' | translate}}
+        </span>
+      </td>
+      <td class="verison-label-row">
+        <div ng-show="repo.status == 'current'">
+          <span class="label {{'status-' + repo.status}}">{{'versions.current' | translate}}:&nbsp;{{repo.currentHosts}}/{{repo.totalHosts}}</span>
+        </div>
+        <div ng-show="repo.status == 'installed'">
+          <span class="label {{'status-' + repo.status}}">{{'versions.installed' | translate}}:&nbsp;{{repo.installedHosts}}/{{repo.totalHosts}}</span>
         </div>
-      </accordion-heading>
-      <table class="table repos-table">
-        <tbody>
-        <tr ng-repeat="repo in stackVersion.repos">
-          <td class="repos-td">
-            <a href="#/stackVersions/{{repo.stack_name}}/{{repo.repository_version}}/edit">{{repo.repository_version}}</a>
-          </td>
-        </tr>
-        </tbody>
-        </table>
-    </accordion-group>
-    <div class="alert alert-info" ng-show="stackVersions && !stackVersions.length">
-      {{'versions.contents.empty' | translate}}
+        <div ng-show="!repo.cluster">
+          <div class="btn-group display-inline-block" dropdown is-open="viewsdropdown.isopen" ng-mouseover="viewsdropdown.isopen=true" ng-mouseout="viewsdropdown.isopen=false" ng-init="viewsdropdown.isopen=false">
+            <a class="btn dropdown-toggle">
+              <span>{{'versions.installOn' | translate}}</span>
+            </a>
+            <ul class="dropdown-menu" ng-show="viewsdropdown.isopen">
+              <li ng-repeat="cluster in dropDownClusters">
+                <a href="javascript:void(null)" ng-click="goToCluster()">
+                  <span>{{cluster.Clusters.cluster_name}}</span>
+                </a>
+              </li>
+            </ul>
+          </div>
+        </div>
+      </td>
+    </tr>
+    </tbody>
+  </table>
+  <div class="alert alert-info col-sm-12" ng-show="!repos.length">
+    {{'common.alerts.nothingToDisplay' | translate: '{term: getConstant("version")}'}}
+  </div>
+  <div class="col-sm-12 table-bar">
+    <div class="pull-left filtered-info">
+      <span>{{'common.filterInfo' | translate: '{showed: tableInfo.showed, total: tableInfo.total, term: getConstant("versions")}'}}</span>
+      <span ng-show="isNotEmptyFilter">- <a href ng-click="clearFilters()">{{'common.controls.clearFilters' | translate}}</a></span>
     </div>
-  </accordion>
-
-  <form class="col-sm-10 form-horizontal" role="form"novalidate>
-    <div class="alert alert-info no-version-alert">
-      {{'versions.noVersions' | translate}}
+    <div class="pull-right left-margin">
+      <pagination class="paginator"
+                  total-items="pagination.totalRepos"
+                  max-size="pagination.maxVisiblePages"
+                  items-per-page="pagination.itemsPerPage"
+                  ng-model="pagination.currentPage"
+                  ng-change="pageChanged()"
+        ></pagination>
     </div>
-  </form>
-
+    <div class="pull-right">
+      <select class="form-control"
+              ng-model="pagination.itemsPerPage"
+              ng-options="item for item in [10, 25, 50, 100]"
+              ng-change="resetPagination()"
+        ></select>
+    </div>
+  </div>
 </div>

+ 158 - 156
ambari-admin/src/main/resources/ui/admin-web/app/views/stackVersions/stackVersionPage.html

@@ -19,193 +19,194 @@
 <div class="clearfix">
   <ol class="breadcrumb pull-left">
     <li><a href="#/stackVersions">{{'common.versions' | translate}}</a></li>
-    <li class="active" ng-if="editController">{{displayName}}&nbsp;<span class="sub-text">({{repoVersionFullName}})</span></li>
+    <li class="active" ng-if="editController">{{displayName}}&nbsp;<span
+            class="sub-text">({{repoVersionFullName}})</span></li>
     <li class="active" ng-if="createController">{{'versions.register' | translate}}</li>
   </ol>
 
-  <div class="pull-right top-margin-4" ng-if="editController">
-    <a href="#/stackVersions/create" class="btn btn-primary">
-      <span class="glyphicon glyphicon-plus"></span>
-        {{'versions.register' | translate}}
-    </a>
+  <div class="pull-right deregister-button" ng-switch="deleteEnabled"  ng-if="editController">
+    <button ng-switch-when="false" class="btn disabled btn-default" tooltip="Cannot delete version already installed.">{{'versions.deregister' | translate}}</button>
+    <button ng-switch-when="true" class="btn btn-danger" ng-click="delete()">{{'versions.deregister' | translate}}</button>
   </div>
 </div>
 <hr>
 
 
 
-<div id="upload-definition-file-panel" ng-if="createController">
+<!-- left tabs begins -->

+<div class="col-sm-2" id="list-stack-id" ng-if="createController">

+  <ul class="nav nav-tabs tabs-left"> 

+    <li ng-repeat="stack in stackIds" ng-click="onStackIdChange()" ng-class="{'active' : stack.isSelected}">
+      <a href="javascript:void(0);">{{stack.stackNameVersion}}</a>
+    </li>

+  </ul>

+</div>

 
-  <div class="col-sm-12 option-radio clearfix">
-    <input type="radio" ng-model="selectedOption.index" value="1" ng-change="togglePublicLocalOptionSelect()">   {{'versions.usePublic' | translate}}
-  </div>
+<div ng-class="{'col-sm-10': createController}">
+  <div class="tab-content">
+    <div class="panel panel-default details-panel">
+      <div class="panel-body">
+        <div class="col-sm-3 .pull-left" id="current-stack-details" ng-if="editController">

+          <table class='table table-borderless alert alert-info'>
+            <tr>
+              <td>{{'common.stack' | translate}}</td>
+              <td><strong>{{activeStackVersion.stackNameVersion}}</strong></td>
+            </tr>
+            <tr>
+              <td>{{'common.name' | translate}}</td>
+              <td><strong>{{activeStackVersion.displayName}}</strong></td>
+            </tr>
+            <tr>
+              <td>{{'common.version' | translate}}</td>
+              <td><strong>{{activeStackVersion.actualVersion}}</strong></td>
+            </tr>
+          </table>
+        </div>
 
-  <div class="clearfix stack-version-selection">
-    <div class="col-sm-3 select-version-label" ng-class="{'disabled': selectedOption.index==2}">
-      {{'versions.selectVersion' | translate}}
-    </div>
-    <div class="col-sm-7 right-stack-info">
-      <div class="repo-list-button btn-group" dropdown>
-        <button class="btn btn-primary dropdown-toggle" data-toggle="dropdown" ng-disabled="(selectedOption.index==2)">{{selectedPublicRepoVersion.displayNameFull}} &nbsp;<span class="caret"></span></button>
-        <ul class="dropdown-menu available-repos-dropdown"  ng-if="availableStackRepoList">
-          <li ng-repeat="version in availableStackRepoList" class="" ng-if="version.displayNameFull !== selectedPublicRepoVersion.displayNameFull">
-            <a ng-click="selectRepoInList()">{{version.displayNameFull}}</a></li>
-        </ul>
-        <ul class="dropdown-menu available-repos-dropdown"  ng-if="!availableStackRepoList || availableStackRepoList.length == 0">
-          <li class="disabled"><a>{{'versions.selectVersionEmpty' | translate}}</a></li>
-        </ul>
+        <div class="version-info" ng-if="createController">
+          <div class="btn-group" dropdown>
+            <button type="button" data-toggle="dropdown" class="btn dropdown-toggle btn-info">
+              {{activeStackVersion.stackNameRepositoryVersion}}
+              <span class="caret"></span>
+            </button>
+            <ul class="dropdown-menu">
+              <li ng-repeat="version in allVersions" ng-click="setActiveVersion()" ng-if="version.visible">
+                <a href="javascript:void(0);">
+                  {{version.stackNameRepositoryVersion}}
+                  <span ng-if="version.stackDefault">{{'versions.defaultVersion' | translate}}</span>
+                </a>
+              </li>
+              <li>
+                <a href="javascript:void(0);" ng-click="addVersion()">
+                  {{'versions.addVersion' | translate}} ...
+                </a>
+              </li>
+            </ul>
+          </div>
+        </div>
+        <div class="version-contents-section" ng-class="{'version-contents-section-register-version': createController}">
+          <table class="table table-striped table-condensed">
+            <tr ng-repeat="service in activeStackVersion.services">
+              <td class="col-sm-4">{{service.displayName}}</td>
+              <td class="col-sm-8">{{service.version}}</td>
+            </tr>
+          </table>
+        </div>
       </div>
     </div>
   </div>
+</div>
 
-  <div class="col-sm-12 option-radio clearfix">
-    <input type="radio" ng-model="selectedOption.index" value="2" ng-change="togglePublicLocalOptionSelect()">   {{'versions.useLocal' | translate}}
-  </div>
+<div id="upload-definition-file-panel" ng-if="createController">
 
-  <div class="clearfix register-version-options">
-    <div class="col-sm-5 option-radio-button">
-      <label class="option-label">
-        <input type="radio" ng-model="selectedLocalOption.index" value="3" ng-change="toggleOptionSelect()" ng-disabled="(selectedOption.index==1)"> {{'versions.uploadFile' | translate}}
-      </label>
-    </div>
-    <div class="col-sm-7">
-      <input type="file" class="choose-file-input" ng-model="option1.file" ng-disabled="(selectedOption.index==1) || !(selectedLocalOption.index==3)"
-             onchange="angular.element(this).scope().onFileSelect(this)"/>
-    </div>
+  <div class="col-sm-12 big-radio clearfix">
+    <input type="radio" ng-model="selectedOption.index" value="1" ng-change="togglePublicLocalOptionSelect()" ng-disabled="networkLost || useRedhatSatellite">
+    {{'versions.usePublic' | translate}}
+    <a id="public-disabled-link" href="javascript:void(0);" ng-if="networkLost" ng-click="showPublicRepoDisabledDialog()">{{'versions.networkIssues.networkLost'| translate}}</a>
   </div>
-  <div class="clearfix register-version-options bottom-margin">
-    <div class="col-sm-5 option-radio-button">
-      <label class="option-label">
-        <input type="radio" ng-model="selectedLocalOption.index" value="4" ng-change="toggleOptionSelect()" ng-disabled="(selectedOption.index==1)"> {{'versions.enterURL' | translate}}
-      </label>
-    </div>
-    <div class="col-sm-7">
-      <div class="form-group {{option2.name}}" ng-class="{'has-error': option2.url.hasError }">
-        <div class=""><input type="text" class="form-control" ng-model="option2.url" ng-change="clearOptionsError()" ng-disabled="(selectedOption.index==1) || !(selectedLocalOption.index==4)"></div>
-      </div>
-    </div>
-    <div class="col-sm-12 read-info-button">
-      <button class="btn btn-primary pull-right" ng-model="button" ng-click="readVersionInfo()"
-            ng-disabled="readInfoButtonDisabled()">{{'versions.readInfo' | translate}}</button>
-    </div>
+  <div class="col-sm-12 big-radio clearfix">
+    <input type="radio" ng-model="selectedOption.index" value="2" ng-change="togglePublicLocalOptionSelect()">
+    {{'versions.useLocal' | translate}}
   </div>
 </div>
 
-<accordion close-others="false" class="col-sm-2 left-menu-all-repos" ng-if="editController">
-  <accordion-group ng-repeat="stackVersion in stackVersions" is-open="stackVersion.isOpened">
-    <accordion-heading>
-      <div class="row stack-version-title">
-        <i class="glyphicon glyphicon-chevron-right" ng-class="{'opened': stackVersion.isOpened}"></i>
-          {{stackVersion.name}}
-      </div>
-    </accordion-heading>
-    <table class="table repos-table">
-      <tbody>
-      <tr ng-repeat="repo in stackVersion.repos">
-        <td class="repos-td" ng-class="{'active': repo.isActive}">
-            <a href="#/stackVersions/{{repo.stack_name}}/{{repo.repository_version}}/edit">{{repo.repository_version}}</a>
-        </td>
-      </tr>
-      </tbody>
-    </table>
-  </accordion-group>
-  <div class="alert alert-info" ng-show="stackVersions && !stackVersions.length">
-    {{'versions.contents.empty' | translate}}
-  </div>
-</accordion>
+<div class="clearfix bottom-margin"></div>
 
-<form ng-class="{'col-sm-10': editController, 'visible': !allInfoCategoriesBlank()}" class="form-horizontal register-version-form hide-soft" role="form" name="repoRegForm" novalidate>
-  <div class="panel panel-default details-panel">
-    <div class="panel-heading">
-      <h3 class="panel-title">{{'common.details' | translate}}</h3>
-        <div class="pull-right deregister-button" ng-switch="deleteEnabled"  ng-if="editController">
-            <button ng-switch-when="false" class="btn disabled btn-default" tooltip="Cannot delete version already installed.">{{'versions.deregister' | translate}}</button>
-            <button ng-switch-when="true" class="btn btn-danger" ng-click="delete()">{{'versions.deregister' | translate}}</button>
-        </div>
-    </div>
-    <div class="panel-body">
-      <div class="col-sm-5 version-info-section">
-        <div class="clearfix">
-          <label class="control-label col-sm-7">{{'versions.details.stackName' | translate}}</label>
-          <div class="version-info col-sm-3">{{stackNameVersion}}</div>
-          <div class="col-sm-2 patch-icon" ng-if="isPatch"><span class="glyphicon glyphicon-tree-deciduous"></span>{{'versions.patch' | translate}}</div>
-        </div>
-        <div class="clearfix">
-          <label class="control-label col-sm-7">{{'versions.details.displayName' | translate}}</label>
-          <div class="version-info col-sm-5">{{displayName}}</div>
-        </div>
-        <div class="clearfix">
-          <label class="control-label col-sm-7">{{'versions.details.version' | translate}}</label>
-          <div class="version-info col-sm-5">{{actualVersion}}</div>
-        </div>
-      </div>
-      <div class="col-sm-6 version-contents-section">
-        <div class="alert alert-info hide-soft" ng-class="{'visible' : !services || !services.length}" role="alert">{{'versions.contents.empty' | translate}}</div>
-        <div class="clearfix" ng-repeat="service in services">
-          <div class="version-info col-sm-9">{{service.displayName}} ({{service.version}})</div>
-        </div>
-      </div>
-    </div>
-  </div>
+<form ng-class="{'visible': !allInfoCategoriesBlank()}" class="form-horizontal register-version-form hide-soft"
+      role="form" name="repoRegForm" novalidate>
   <div class="panel panel-default repos-panel">
     <div class="panel-heading">
       <h3 class="panel-title">
-          {{'versions.repos' | translate}}
-          <button ng-show="supports.addingNewRepository" class="btn btn-primary pull-right btn-xs pull-up" ng-click="addRepository()">
-              <span class="glyphicon glyphicon-plus"></span>
-              {{'common.add' | translate:'{ term: "Repository" }'}}
-          </button>
+        {{'versions.repos' | translate}}
+        <button ng-show="supports.addingNewRepository" class="btn btn-primary pull-right btn-xs pull-up"
+                ng-click="addRepository()">
+          <span class="glyphicon glyphicon-plus"></span>
+          {{'common.add' | translate:'{ term: "Repository" }'}}
+        </button>
       </h3>
     </div>
     <div class="panel-body">
-      <div class="alert alert-info" role="alert">{{'versions.alerts.baseURLs' | translate}}</div>
-      <div class="alert alert-warning hide-soft" ng-class="{'visible' : hasValidationErrors()}" role="alert">{{'versions.alerts.validationFailed' | translate}}</div>
-      <div class="border-bottom bottom-margin clearfix">
-        <div class="col-sm-2"><h5><label>{{'versions.os' | translate}}</label></h5></div>
-        <div class="name-label-adjust col-sm-2"><h5><label>{{'common.name' | translate}}</label></h5></div>
-        <div class="col-sm-7"><h5><label >{{'versions.baseURL' | translate}}</label></h5></div>
-      </div>
-      <div class="alert alert-info hide-soft" ng-class="{'visible' : !osList || !osList.length}" role="alert">{{'versions.contents.empty' | translate}}</div>
-      <div class="" ng-repeat="os in osList">
-        <div ng-if="os.selected==true">
-          <div class="clearfix border-bottom bottom-margin">
-            <!-- show selected os in list table-->
-            <div class="col-sm-2 os-type-label">
-              <label>{{os.OperatingSystems.os_type}}</label>
-            </div>
-            <div class="col-sm-9">
-              <div class="form-group {{repository.Repositories.repo_name}}" ng-class="{'has-error': repository.hasError }" ng-repeat="repository in os.repositories">
-                <label class="repo-name-label control-label col-sm-3">{{repository.Repositories.repo_name}}</label>
-                <div class="col-sm-9"><input type="text" class="form-control" ng-model="repository.Repositories.base_url"
-                                         ng-change="clearError()" ng-disabled="useRedhatSatellite"></div>
-              </div>
-            </div>
-            <div class="col-sm-1 remove-icon" ng-click="removeOS()" ng-class="{'disabled' : useRedhatSatellite}"><span class="glyphicon glyphicon-minus"></span>{{'common.controls.remove' | translate}}</div>
+      <div class="panel-inner">
+        <div class="alert alert-info" role="alert">{{'versions.alerts.baseURLs' | translate}}</div>
+        <div class="alert alert-warning hide-soft" ng-class="{'visible' : hasValidationErrors()}" role="alert">
+          {{'versions.alerts.validationFailed' | translate}}
+        </div>
+        <div class="clearfix repo-table-title row-fluid">
+          <div class="col-sm-2" id="os-label"><label>{{'versions.os' | translate}}</label></div>
+          <div class="col-sm-2" id="name-label-adjust"><label>{{'common.name' | translate}}</label></div>
+          <div class="col-sm-6" id="repo-base-url-label"><label>{{'versions.baseURL' | translate}} </label></div>
+          <div class="btn-group pull-right tooltip-wrapper"
+               tooltip="{{(isAddOsButtonDisabled() && !useRedhatSatellite)? ('versions.alerts.allOsAdded' | translate) : ''}}"
+               dropdown>
+            <button class="btn add-os-button dropdown-toggle" ng-disabled="isAddOsButtonDisabled()">
+              <i class="fa fa-plus" aria-hidden="true"></i> {{'common.add' | translate}} &nbsp;<span
+                    class="caret"></span></button>
+            <ul class="dropdown-menu" ng-class="{'hidden': hasNotDeletedRepo()}">
+              <li ng-repeat="os in osList"><a ng-if="os.selected==false" ng-click="addOS($event)">{{os.OperatingSystems.os_type}}</a>
+              </li>
+            </ul>
           </div>
+
         </div>
-      </div>
-        <div class="btn-group pull-right" dropdown>
-          <button class="btn dropdown-toggle" ng-disabled="isAddOsButtonDisabled()">
-            <span class="glyphicon glyphicon-plus"></span> {{'common.add' | translate: '{term: constants.os}'}} &nbsp;<span class="caret"></span></button>
-          <ul class="dropdown-menu" ng-class="{'hidden': hasNotDeletedRepo()}">
-            <li ng-repeat="os in osList"><a ng-if="os.selected==false" ng-click="addOS()">{{os.OperatingSystems.os_type}}</a></li>
-          </ul>
+        <div class="alert alert-info hide-soft" ng-class="{'visible' : !osList || !osList.length}" role="alert">
+          {{'versions.contents.empty' | translate}}
         </div>
-      <div class="clearfix">
-        <div class="col-sm-9" id="skip-validation">
-          <div class="checkbox">
-            <label>
-              <input type="checkbox" ng-model="skipValidation" ng-change="clearErrors()" ng-disabled="useRedhatSatellite">
-              {{'versions.skipValidation' | translate}} <span class="glyphicon glyphicon-question-sign" tooltip-html-unsafe="{{'versions.alerts.skipValidationWarning' | translate}}"></span>
-            </label>
+        <div class="" ng-repeat="os in osList">
+          <div ng-if="os.selected==true">
+            <div class="clearfix border-bottom">
+              <!-- show selected os in list table-->
+              <div class="col-sm-2 os-type-label">
+                <label>{{os.OperatingSystems.os_type}}</label>
+              </div>
+              <div class="col-sm-9">
+                <div class="form-group repo-name-url {{repository.Repositories.repo_name}}"
+                     ng-class="{'has-error': repository.hasError }" ng-repeat="repository in os.repositories">
+                  <span class="repo-name-label control-label col-sm-3">{{repository.Repositories.repo_id}}</span>
+                  <div class="col-sm-7 repo-url">
+                    <input type="text" class="form-control"
+                           placeholder="{{(repository.Repositories.repo_name.indexOf('UTILS') < 0 )?('versions.repository.placeholder' | translate) : ''}}"
+                           ng-model="repository.Repositories.base_url"
+                           ng-change="onRepoUrlChange(repository)" ng-disabled="useRedhatSatellite">
+                  </div>
+                  <i class="fa fa-undo orange-icon cursor-pointer"
+                     ng-if="selectedOption.index == 1 && repository.Repositories.base_url != repository.Repositories.initial_base_url
+                     || selectedOption.index == 2 && repository.Repositories.base_url != ''
+                     || editController && repository.Repositories.base_url != repository.Repositories.initial_base_url"
+                     ng-click="undoChange(repository)"
+                     tooltip-html-unsafe="{{'common.undo' | translate}}"
+                     aria-hidden="true"></i>
+                </div>
+              </div>
+              <div class="col-sm-1 remove-icon" ng-click="removeOS()" ng-class="{'disabled' : useRedhatSatellite}"><i
+                      class="fa fa-minus" aria-hidden="true"></i>{{'common.controls.remove' | translate}}
+              </div>
+            </div>
           </div>
         </div>
-        <div class="col-sm-9" id="use-redhat">
-          <div class="checkbox">
-            <label>
-              <input type="checkbox" ng-model="useRedhatSatellite" ng-change="clearErrors()">
-              {{'versions.useRedhatSatellite.title' | translate}} <span class="glyphicon glyphicon-question-sign" tooltip-html-unsafe="{{'versions.alerts.useRedhatSatelliteWarning' | translate}}"></span>
-            </label>
+        <div class="clearfix advanced-radio-buttons">
+          <div class="col-sm-9" id="skip-validation">
+            <div class="checkbox">
+              <label>
+                <input type="checkbox" ng-model="skipValidation" ng-change="clearErrors()"
+                       ng-disabled="useRedhatSatellite">
+                <span ng-class="{'disabled' : useRedhatSatellite}">{{'versions.skipValidation' | translate}}</span>
+                <i class="fa fa-question-circle"
+                   tooltip-html-unsafe="{{'versions.alerts.skipValidationWarning' | translate}}" aria-hidden="true"></i>
+              </label>
+            </div>
+          </div>
+          <div class="col-sm-9" id="use-redhat">
+            <div class="checkbox">
+              <label>
+                <input type="checkbox" ng-model="useRedhatSatellite" ng-change="clearErrors()"
+                       ng-disabled="isPublicRepoSelected()">
+                <span ng-class="{'disabled' : isPublicRepoSelected()}"
+                      tooltip="{{(isPublicRepoSelected())? ('versions.useRedhatSatellite.disabledMsg' | translate) : ''}}">{{'versions.useRedhatSatellite.title' | translate}}</span>
+                <i class="fa fa-question-circle"
+                   tooltip-html-unsafe="{{'versions.alerts.useRedhatSatelliteWarning' | translate}}"
+                   aria-hidden="true"></i>
+              </label>
+            </div>
           </div>
         </div>
       </div>
@@ -213,7 +214,8 @@
   </div>
   <div class="col-sm-12">
     <button class="btn btn-primary pull-right left-margin" ng-click="save()"
-            ng-disabled="isSaveButtonDisabled()">{{'common.controls.save' | translate}}</button>
+            ng-disabled="isSaveButtonDisabled()">{{'common.controls.save' | translate}}
+    </button>
     <button class="btn btn-default pull-right" ng-click="cancel()">{{'common.controls.cancel' | translate}}</button>
   </div>
 </form>

+ 1 - 0
ambari-web/app/assets/test/tests.js

@@ -119,6 +119,7 @@ var files = [
   'test/controllers/experimental_test',
   'test/controllers/wizard_test',
   'test/controllers/wizard/step0_test',
+  'test/controllers/wizard/step1_test',
   'test/controllers/wizard/step2_test',
   'test/controllers/wizard/step3_test',
   'test/controllers/wizard/step4_test',

+ 32 - 35
ambari-web/app/controllers/installer.js

@@ -322,10 +322,12 @@ App.InstallerController = App.WizardController.extend({
   },
 
   mergeChanges: function (repos, stacks) {
-    repos.forEach(function (repo) {
+    var _repos = repos || [];
+    var _stacks = stacks || [];
+    _repos.forEach(function (repo) {
       App.Repository.find().findProperty('id', repo.id).set('baseUrl', repo.base_url);
     });
-    stacks.forEach(function (_stack) {
+    _stacks.forEach(function (_stack) {
       var stack = App.Stack.find().findProperty('id', _stack.id);
       if (stack) {
         stack.set('useRedhatSatellite', _stack.use_redhat_satellite);
@@ -335,12 +337,9 @@ App.InstallerController = App.WizardController.extend({
 
   setSelected: function (isStacksExistInDb) {
     if (!isStacksExistInDb) {
-      var defaultStackVersion = App.Stack.find().findProperty('stackNameVersion', App.defaultStackVersion);
-      if (defaultStackVersion) {
-        defaultStackVersion.set('isSelected', true)
-      } else {
-        App.Stack.find().objectAt(0).set('isSelected', true);
-      }
+      var stacks = App.Stack.find();
+      stacks.setEach('isSelected', false);
+      stacks.sortProperty('id').set('lastObject.isSelected', true);
     }
     this.set('content.stacks', App.Stack.find());
     App.set('currentStackVersion', App.Stack.find().findProperty('isSelected').get('stackNameVersion'));
@@ -414,11 +413,11 @@ App.InstallerController = App.WizardController.extend({
   },
   getServerVersionSuccessCallback: function (data) {
     var clientVersion = App.get('version');
-    var serverVersion = (data.RootServiceComponents.component_version).toString();
+    var serverVersion = data.RootServiceComponents.component_version.toString();
     this.set('ambariServerVersion', serverVersion);
     if (clientVersion) {
       this.set('versionConflictAlertBody', Em.I18n.t('app.versionMismatchAlert.body').format(serverVersion, clientVersion));
-      this.set('isServerClientVersionMismatch', clientVersion != serverVersion);
+      this.set('isServerClientVersionMismatch', clientVersion !== serverVersion);
     } else {
       this.set('isServerClientVersionMismatch', false);
     }
@@ -435,10 +434,6 @@ App.InstallerController = App.WizardController.extend({
     Em.assert('Stack model is not populated', stacks.get('length'));
     App.db.setStacks(stacks.slice());
     this.set('content.stacks', stacks);
-
-    App.OperatingSystem.find().filterProperty('isSelected', false).forEach(function (os) {
-      App.serviceMapper.deleteRecord(os);
-    });
     var repos = App.Repository.find() || [];
     App.db.setRepos(repos.slice());
   },
@@ -495,15 +490,15 @@ App.InstallerController = App.WizardController.extend({
     var props = this.getDBProperties(['masterComponentHosts', 'hosts']);
     var masterComponentHosts = props.masterComponentHosts,
       hosts = props.hosts || {},
-      host_names = Em.keys(hosts);
+      hostNames = Em.keys(hosts);
     if (Em.isNone(masterComponentHosts)) {
       masterComponentHosts = [];
     }
     else {
       masterComponentHosts.forEach(function (component) {
-        for (var i = 0; i < host_names.length; i++) {
-          if (hosts[host_names[i]].id === component.host_id) {
-            component.hostName = host_names[i];
+        for (var i = 0; i < hostNames.length; i++) {
+          if (hosts[hostNames[i]].id === component.host_id) {
+            component.hostName = hostNames[i];
             break;
           }
         }
@@ -527,13 +522,13 @@ App.InstallerController = App.WizardController.extend({
     var props = this.getDBProperties(['slaveComponentHosts', 'hosts']);
     var slaveComponentHosts = props.slaveComponentHosts,
       hosts = props.hosts || {},
-      host_names = Em.keys(hosts);
+      hostNames = Em.keys(hosts);
     if (!Em.isNone(slaveComponentHosts)) {
       slaveComponentHosts.forEach(function (component) {
         component.hosts.forEach(function (host) {
-          for (var i = 0; i < host_names.length; i++) {
-            if (hosts[host_names[i]].id === host.host_id) {
-              host.hostName = host_names[i];
+          for (var i = 0; i < hostNames.length; i++) {
+            if (hosts[hostNames[i]].id === host.host_id) {
+              host.hostName = hostNames[i];
               break;
             }
           }
@@ -596,9 +591,8 @@ App.InstallerController = App.WizardController.extend({
   postVersionDefinitionFileSuccessCallback: function (_data, request, dataInfo) {
     if (_data.resources.length && _data.resources[0].VersionDefinition) {
       var data = _data.resources[0];
-      var self = this;
       // load the data info to display for details and contents panel
-      data.VersionDefinition.id = dataInfo.data.VersionDefinition.available;
+      data.VersionDefinition.id = Em.get(dataInfo, 'data.VersionDefinition.available') || data.VersionDefinition.id;
       var response = {
         id : data.VersionDefinition.id,
         stackVersion : data.VersionDefinition.stack_version,
@@ -670,19 +664,20 @@ App.InstallerController = App.WizardController.extend({
   postVersionDefinitionFileErrorCallback: function (request, ajaxOptions, error, data, params) {
     params.dfd.reject(data);
     var header = Em.I18n.t('installer.step1.useLocalRepo.uploadFile.error.title');
-    var body = "";
-    if(request && request.responseText){
+    var body = '';
+    if(request && request.responseText) {
       try {
         var json = $.parseJSON(request.responseText);
         body = json.message;
       } catch (err) {}
     }
+    App.db.setLocalRepoVDFData(undefined);
     App.showAlertPopup(header, body);
   },
 
   getSupportedOSList: function (versionDefinition, stackInfo) {
     this.incrementProperty('loadStacksRequestsCounter');
-    return  App.ajax.send({
+    return App.ajax.send({
       name: 'wizard.step1.get_supported_os_types',
       sender: this,
       data: {
@@ -727,9 +722,11 @@ App.InstallerController = App.WizardController.extend({
           this.postVersionDefinitionFile(versionData.isXMLdata, versionData.data).done(function (versionInfo) {
             self.mergeChanges(data.stackInfo.repos, data.stackInfo.stacks);
             App.Stack.find().setEach('isSelected', false);
-            var stackId = versionData.data.VersionDefinition.available || versionInfo.stackNameVersion + "-" + versionInfo.actualVersion;
+            var stackId = Em.get(versionData, 'data.VersionDefinition.available') || versionInfo.stackNameVersion + "-" + versionInfo.actualVersion;
             App.Stack.find().findProperty('id', stackId).set('isSelected', true);
             self.setSelected(data.stackInfo.isStacksExistInDb);
+          }).fail(function () {
+            self.setSelected(data.stackInfo.isStacksExistInDb);
           });
         } else {
           this.setSelected(data.stackInfo.isStacksExistInDb);
@@ -781,12 +778,12 @@ App.InstallerController = App.WizardController.extend({
    */
   prepareRepoForSaving: function(repo) {
     var repoVersion = { "operating_systems": [] };
-    var ambari_managed_repositories = !repo.get('useRedhatSatellite');
+    var ambariManagedRepositories = !repo.get('useRedhatSatellite');
     repo.get('operatingSystems').forEach(function (os, k) {
       repoVersion.operating_systems.push({
         "OperatingSystems": {
           "os_type": os.get("osType"),
-          "ambari_managed_repositories": ambari_managed_repositories
+          "ambari_managed_repositories": ambariManagedRepositories
         },
         "repositories": []
       });
@@ -822,7 +819,7 @@ App.InstallerController = App.WizardController.extend({
             repo.setProperties({
               errorTitle: '',
               errorContent: '',
-              validation: App.Repository.validation['INPROGRESS']
+              validation: App.Repository.validation.INPROGRESS
             });
             this.set('content.isCheckInProgress', true);
             App.ajax.send({
@@ -860,7 +857,7 @@ App.InstallerController = App.WizardController.extend({
       var os = selectedStack.get('operatingSystems').findProperty('id', data.osId);
       var repo = os.get('repositories').findProperty('repoId', data.repoId);
       if (repo) {
-        repo.set('validation', App.Repository.validation['OK']);
+        repo.set('validation', App.Repository.validation.OK);
       }
     }
     this.set('validationCnt', this.get('validationCnt') - 1);
@@ -880,7 +877,7 @@ App.InstallerController = App.WizardController.extend({
       var repo = os.get('repositories').findProperty('repoId', params.repoId);
       if (repo) {
         repo.setProperties({
-          validation: App.Repository.validation['INVALID'],
+          validation: App.Repository.validation.INVALID,
           errorTitle: request.status + ":" + request.statusText,
           errorContent: $.parseJSON(request.responseText) ? $.parseJSON(request.responseText).message : ""
         });
@@ -1055,9 +1052,9 @@ App.InstallerController = App.WizardController.extend({
       if (stringUtils.compareVersions(currentJDKVersion, minJDKVersion) < 0 ||
           stringUtils.compareVersions(maxJDKVersion, currentJDKVersion) < 0) {
         // checks and process only minor part for now
-        var versionDistance = parseInt(maxJDKVersion.split('.')[1]) - parseInt(minJDKVersion.split('.')[1]);
+        var versionDistance = parseInt(maxJDKVersion.split('.')[1], 10) - parseInt(minJDKVersion.split('.')[1], 10);
         var versionsList = [minJDKVersion];
-        for (var i = 1; i < (versionDistance + 1); i++) {
+        for (var i = 1; i < versionDistance + 1; i++) {
           versionsList.push("" + minJDKVersion.split('.')[0] + '.' + (+minJDKVersion.split('.')[1] + i));
         }
         var versionsString = stringUtils.getFormattedStringFromArray(versionsList, t('or'));

+ 2 - 2
ambari-web/app/controllers/wizard/step0_controller.js

@@ -70,7 +70,7 @@ App.WizardStep0Controller = Em.Controller.extend({
    * @method submit
    */
   submit: function () {
-    if(App.router.nextBtnClickInProgress){
+    if(App.get('router.nextBtnClickInProgress')){
       return;
     }
     this.set('hasSubmitted', true);
@@ -78,7 +78,7 @@ App.WizardStep0Controller = Em.Controller.extend({
       App.clusterStatus.set('clusterName', this.get('content.cluster.name'));
       this.set('content.cluster.status', 'PENDING');
       this.set('content.cluster.isCompleted', false);
-      App.router.nextBtnClickInProgress = true;
+      App.set('router.nextBtnClickInProgress', true);
       App.router.send('next');
     }
   }

+ 372 - 45
ambari-web/app/controllers/wizard/step1_controller.js

@@ -17,19 +17,57 @@
  */
 
 var App = require('app');
+var arrayUtils = require('utils/array_utils');
+
+/**
+ * @typedef {Em.Object} StackType
+ * @property {string} stackName
+ * @property {App.Stack[]} stacks
+ * @property {boolean} isSelected
+ * @property {boolean} defaultStackAllowedToSelect
+ */
+
+/**
+ * @type {Em.Object}
+ */
+var StackType = Em.Object.extend({
+  stackName: '',
+  stacks: [],
+  isSelected: Em.computed.someBy('stacks', 'isSelected', true),
+  defaultStackAllowedToSelect: Em.computed.equal('stacks.length', 1),
+  visibleStacks: function () {
+    var stacks = this.get('stacks');
+    return this.get('defaultStackAllowedToSelect') ? stacks : stacks.filterProperty('stackDefault', false);
+  }.property('defaultStackAllowedToSelect', 'stacks.[]')
+});
 
 App.WizardStep1Controller = Em.Controller.extend({
 
   name: 'wizardStep1Controller',
+
   /**
    * Skip repo-validation
+   *
    * @type {bool}
    */
   skipValidationChecked: false,
 
-  selectedStack: function() {
-    return App.Stack.find().findProperty('isSelected');
-  }.property('content.stacks.@each.isSelected'),
+  /**
+   * @type {App.Stack}
+   */
+  selectedStack: Em.computed.findBy('content.stacks', 'isSelected', true),
+
+  /**
+   * @type {App.ServiceSimple[]}
+   */
+  servicesForSelectedStack: Em.computed.filterBy('selectedStack.stackServices', 'isHidden', false),
+
+  /**
+   * Some network issues exist if there is no stack with <code>stackDefault</code> = false
+   *
+   * @type {boolean}
+   */
+  networkIssuesExist: Em.computed.everyBy('content.stacks', 'stackDefault', true),
 
   optionsToSelect: {
     'usePublicRepo': {
@@ -50,7 +88,7 @@ App.WizardStep1Controller = Em.Controller.extend({
         index: 1,
         name: 'enterUrl',
         url: '',
-        placeholder: 'Enter URL to Version Definition File',
+        placeholder: Em.I18n.t('installer.step1.useLocalRepo.enterUrl.placeholder'),
         hasError: false,
         isSelected: false
       }
@@ -58,53 +96,75 @@ App.WizardStep1Controller = Em.Controller.extend({
   },
 
   /**
-   * Used to set version definition file from FileUploader
-   * @method setVDFFile
-   * @param {string} vdf
+   * Checks if user selected to input url or upload file but didn't do it
+   * true  - url-radio is checked but url-field is empty
+   *       - file-radio is checked but file is not selected
+   * false - otherwise
+   *
+   * @type {boolean}
    */
-  setVDFFile: function (vdf) {
-    this.set("optionsToSelect.useLocalRepo.uploadFile.file", vdf);
-  },
+  readInfoIsNotProvided: function () {
+    var useLocalRepo = this.get('optionsToSelect.useLocalRepo');
+    if(Em.get(useLocalRepo, 'uploadFile.isSelected')) {
+      return !Em.get(useLocalRepo, 'uploadFile.file');
+    }
+    if (Em.get(useLocalRepo, 'enterUrl.isSelected')) {
+      return !Em.get(useLocalRepo, 'enterUrl.url');
+    }
+    return false;
+  }.property('optionsToSelect.useLocalRepo.isSelected', 'optionsToSelect.useLocalRepo.uploadFile.isSelected',
+    'optionsToSelect.useLocalRepo.uploadFile.file', 'optionsToSelect.useLocalRepo.enterUrl.url'),
+
+  /**
+   * List of stacks grouped by <code>stackNameVersion</code>
+   *
+   * @type {StackType[]}
+   */
+  availableStackTypes: function () {
+    var stacks = this.get('content.stacks');
+    return stacks ? stacks.mapProperty('stackNameVersion').uniq().sort().reverse().map(function (stackName) {
+      return StackType.create({
+        stackName: stackName,
+        stacks: stacks.filterProperty('stackNameVersion', stackName).sort(arrayUtils.sortByIdAsVersion).reverse()
+      })
+    }) : [];
+  }.property('content.stacks.@each.stackNameVersion'),
+
+  /**
+   * @type {StackType}
+   */
+  selectedStackType: Em.computed.findBy('availableStackTypes', 'isSelected', true),
 
   /**
    * Load selected file to current page content
    */
-  readVersionInfo: function(){
+  readVersionInfo: function () {
     var data = {};
     var isXMLdata = false;
-    if (this.get("optionsToSelect.usePublicRepo.isSelected")) return;
-    if (this.get("optionsToSelect.useLocalRepo.isSelected") && this.get("optionsToSelect.useLocalRepo.enterUrl.isSelected")) {
+    if (this.get("optionsToSelect.useLocalRepo.enterUrl.isSelected")) {
       var url = this.get("optionsToSelect.useLocalRepo.enterUrl.url");
       data = {
-        "VersionDefinition": {
-          "version_url": url
+        VersionDefinition: {
+          version_url: url
         }
       };
       App.db.setLocalRepoVDFData(url);
-    } else if (this.get("optionsToSelect.useLocalRepo.uploadFile.isSelected")) {
-      isXMLdata = true;
-      // load from file browser
-      data = this.get("optionsToSelect.useLocalRepo.uploadFile.file");
-      App.db.setLocalRepoVDFData(data);
     }
-    var installerController = App.router.get('installerController');
-    var self = this;
-    installerController.postVersionDefinitionFile(isXMLdata, data).done(function (response) {
-      self.set('latestSelectedLocalRepoId', response.stackNameVersion + "-" + response.actualVersion);
-      // load successfully, so make this local stack repo as selectedStack
-      self.get('content.stacks').setEach('isSelected', false);
-      self.get('content.stacks').findProperty('id', response.stackNameVersion + "-" + response.actualVersion).set('isSelected', true);
-      Ember.run.next(function () {
-        $("[rel=skip-validation-tooltip]").tooltip({ placement: 'right'});
-        $("[rel=use-redhat-tooltip]").tooltip({ placement: 'right'});
-      });
-    });
+    else {
+      if (this.get("optionsToSelect.useLocalRepo.uploadFile.isSelected")) {
+        isXMLdata = true;
+        // load from file browser
+        data = this.get("optionsToSelect.useLocalRepo.uploadFile.file");
+        App.db.setLocalRepoVDFData(data);
+      }
+    }
+    return App.router.get('installerController').postVersionDefinitionFile(isXMLdata, data);
   },
 
   /**
    * On click handler for removing OS
    */
-  removeOS: function(event) {
+  removeOS: function (event) {
     if (this.get('selectedStack.useRedhatSatellite')) {
       return;
     }
@@ -115,23 +175,290 @@ App.WizardStep1Controller = Em.Controller.extend({
   /**
    * On click handler for adding new OS
    */
-  addOS: function(event) {
+  addOS: function (event) {
     var osToAdd = event.context;
     Em.set(osToAdd, 'isSelected', true);
   },
 
-  changeUseRedhatSatellite: function () {
-    if (App.router.get('installerController.currentStep') !== "1") {
-      return;
+  /**
+   * Use Local Repo if some network issues exist
+   */
+  onNetworkIssuesExist: function() {
+    if (this.get('networkIssuesExist')) {
+      this.useLocalRepo();
     }
-    if (this.get('selectedStack.useRedhatSatellite')) {
-      return App.ModalPopup.show({
-        header: Em.I18n.t('common.important'),
-        secondary: false,
-        bodyClass: Ember.View.extend({
-          template: Ember.Handlebars.compile(Em.I18n.t('installer.step1.advancedRepo.useRedhatSatellite.warning'))
-        })
+  }.observes('networkIssuesExist'),
+
+  /**
+   * Select stack with field equal to the value
+   * Example:
+   * <pre>
+   *   selectStackBy('id', 'HDP-2.5-2.5.0.0'); // select stack with id = 'HDP-2.5-2.5.0.0'
+   *   selectStackBy('stackNameVersion', 'HDP-2.5'); // select first stack with stackNameVersion = 'HDP-2.5'
+   * </pre>
+   *
+   * @param {string} field
+   * @param {string} value
+   */
+  selectStackBy: function (field, value) {
+    this.get('content.stacks').setEach('isSelected', false);
+    this.get('content.stacks').findProperty(field, value).set('isSelected', true);
+  },
+
+  /**
+   * Restore base urls for selected stack when user select to use public repository
+   */
+  usePublicRepo: function () {
+    var selectedStack = this.get('selectedStack');
+    if (selectedStack) {
+      selectedStack.setProperties({
+        useRedhatSatellite: false,
+        usePublicRepo: true,
+        useLocalRepo: false
+      });
+      selectedStack.restoreReposBaseUrls();
+    }
+  },
+
+  /**
+   * Clean base urls for selected stack when user select to use local repository
+   */
+  useLocalRepo: function () {
+    var selectedStack = this.get('selectedStack');
+    if (selectedStack) {
+      selectedStack.setProperties({
+        usePublicRepo: false,
+        useLocalRepo: true
       });
+      selectedStack.cleanReposBaseUrls();
     }
-  }.observes('selectedStack.useRedhatSatellite')
+  },
+
+  /**
+   * Restores url value to be its default value.
+   * @method doRestoreDefaultValue
+   */
+  doRestoreDefaultValue: function (event) {
+    var repo = event.contexts[0];
+    repo.set('baseUrl', repo.get('latestBaseUrl'));
+  },
+
+  /**
+   * Restores url value to empty string.
+   * @method doRestoreToEmpty
+   */
+  doRestoreToEmpty: function (event) {
+    var repo = event.contexts[0];
+    repo.set('baseUrl', '');
+  },
+
+  /**
+   * Click-handler for left-tabs with stack types
+   * Select first available stack with stackName equal to chosen
+   *
+   * @param {{context: StackType}} event
+   */
+  selectRepoInList: function (event) {
+    var id = this.get('availableStackTypes').findProperty('stackName', event.context.stackName).get('stacks.firstObject.id');
+    this.selectStackBy('id', id);
+  },
+
+  /**
+   * Click-handler for StackVersion-tabs
+   *
+   * @param {{context: App.Stack}} event
+   */
+  changeVersion: function (event) {
+    this.selectStackBy('id', event.context.get('id'));
+  },
+
+  /**
+   * Show popup with options to upload new version
+   *
+   * @returns {App.ModalPopup}
+   */
+  uploadVdf: function () {
+    return App.ModalPopup.show({
+
+      controller: this,
+
+      header: Em.I18n.t('installer.step1.changeVersion.title'),
+
+      primary: Em.I18n.t('installer.step1.useLocalRepo.readButton'),
+
+      disablePrimary: Em.computed.alias('controller.readInfoIsNotProvided'),
+
+      /**
+       * Try to read version info from the url or file (if provided)
+       */
+      onPrimary: function () {
+        var controller = this.get('controller');
+        controller.readVersionInfo().done(function (response) {
+          // load successfully, so make this local stack repo as selectedStack
+          var newStackId = response.stackNameVersion + '-' + response.actualVersion;
+          var oldStackNameVersion = controller.get('selectedStack.stackNameVersion');
+          controller.selectStackBy('id', newStackId);
+          if (oldStackNameVersion && oldStackNameVersion !== response.stackNameVersion) {
+            App.showAlertPopup(Em.I18n.t('common.warning'), Em.I18n.t('installer.step1.addVersion.stackChanged.popup.body').format(oldStackNameVersion, response.stackNameVersion));
+          }
+          Ember.run.next(function () {
+            $("[rel=skip-validation-tooltip]").tooltip({placement: 'right'});
+            $("[rel=use-redhat-tooltip]").tooltip({placement: 'right'});
+          });
+        });
+        this.restoreUploadOptions();
+        this._super();
+      },
+
+      /**
+       * Disable url/file fields on popup-close
+       */
+      onSecondary: function () {
+        this.restoreUploadOptions();
+        this._super();
+      },
+
+      /**
+       * Disable url/file fields on popup-close
+       */
+      onClose: function () {
+        this.restoreUploadOptions();
+        this._super();
+      },
+
+      /**
+       * Deselect file/url radio
+       */
+      restoreUploadOptions: function () {
+        this.set('controller.optionsToSelect.useLocalRepo.enterUrl.isSelected', false);
+        this.set('controller.optionsToSelect.useLocalRepo.enterUrl.url', '');
+        this.set('controller.optionsToSelect.useLocalRepo.uploadFile.isSelected', true);
+        this.set('controller.optionsToSelect.useLocalRepo.uploadFile.file', '');
+      },
+
+      bodyClass: Em.View.extend({
+
+        controller: this,
+
+        templateName: require('templates/wizard/step1/vdf_upload'),
+
+        /**
+         * Wrapper for 'upload-file' elements
+         *
+         * @type {Em.View}
+         */
+        uploadFileView: Em.View.extend({
+
+          classNames: ['clearfix'],
+
+          /**
+           * Checkbox for Use local Repo > Upload VDF file
+           *
+           * @type {Ember.Checkbox}
+           */
+          uploadFileRadioButton: Em.Checkbox.extend({
+            attributeBindings: ['type', 'checked'],
+            checked: Em.computed.alias('controller.optionsToSelect.useLocalRepo.uploadFile.isSelected'),
+            type: 'radio'
+          }),
+
+          /**
+           * Is File API available
+           *
+           * @type {bool}
+           */
+          isFileApi: window.File && window.FileReader && window.FileList,
+
+          /**
+           * Upload file is disabled when some stack is selected or url-field is selected
+           *
+           * @type {boolean}
+           */
+          fileBrowserDisabled: Em.computed.alias('controller.optionsToSelect.useLocalRepo.enterUrl.isSelected'),
+
+          /**
+           * Input to select vdf-file
+           *
+           * @type {Em.View}
+           */
+          fileInputView: Em.View.extend({
+            template: Em.Handlebars.compile('<input type="file" {{bindAttr class="controller.optionsToSelect.useLocalRepo.enterUrl.isSelected:disabled"}} />'),
+
+            classNames: ['vdf-input-indentation'],
+
+            change: function (e) {
+              var self = this;
+              if (e.target.files && e.target.files.length === 1) {
+                var file = e.target.files[0];
+                var reader = new FileReader();
+
+                reader.onload = (function () {
+                  return function (event) {
+                    self.set('controller.optionsToSelect.useLocalRepo.uploadFile.file', event.target.result);
+                  };
+                })(file);
+                reader.readAsText(file);
+              }
+            }
+
+          }),
+
+          click: function () {
+            if (!this.set('controller.optionsToSelect.useLocalRepo.uploadFile.isSelected')) {
+              this.set('controller.optionsToSelect.useLocalRepo.enterUrl.isSelected', false);
+              this.set('controller.optionsToSelect.useLocalRepo.uploadFile.isSelected', true);
+              this.set('controller.optionsToSelect.useLocalRepo.enterUrl.hasError', false);
+              this.set('controller.optionsToSelect.useLocalRepo.uploadFile.hasError', false);
+            }
+          }
+        }),
+
+        /**
+         * Wrapper for 'enter-url' elements
+         *
+         * @type {Em.View}
+         */
+        enterUrlView: Em.View.extend({
+
+          /**
+           * Url-field is disable when some stack is selected or upload file is selected
+           *
+           * @type {boolean}
+           */
+          enterUrlFieldDisabled: Em.computed.alias('controller.optionsToSelect.useLocalRepo.uploadFile.isSelected'),
+
+          /**
+           * Input for file upload
+           *
+           * @type {Em.TextField}
+           */
+          enterUrlField: Em.TextField.extend({
+            classNameBindings: [':input-block-level', 'controller.optionsToSelect.useLocalRepo.uploadFile.isSelected:disabled']
+          }),
+
+          /**
+           * Checkbox for Use local Repo > Enter Url of VDF file
+           *
+           * @type {Ember.Checkbox}
+           */
+          enterUrlRadioButton: Em.Checkbox.extend({
+            attributeBindings: [ 'type', 'checked' ],
+            checked: Em.computed.alias('controller.optionsToSelect.useLocalRepo.enterUrl.isSelected'),
+            type: 'radio',
+          }),
+
+          click: function () {
+            if (!this.set('controller.optionsToSelect.useLocalRepo.enterUrl.isSelected')) {
+              this.set('controller.optionsToSelect.useLocalRepo.enterUrl.isSelected', true);
+              this.set('controller.optionsToSelect.useLocalRepo.uploadFile.isSelected', false);
+              this.set('controller.optionsToSelect.useLocalRepo.enterUrl.hasError', false);
+              this.set('controller.optionsToSelect.useLocalRepo.uploadFile.hasError', false);
+            }
+          }
+        })
+
+      })
+    });
+  }
+
 });

+ 2 - 1
ambari-web/app/controllers/wizard/step3_controller.js

@@ -1668,7 +1668,8 @@ App.WizardStep3Controller = Em.Controller.extend(App.ReloadPopupMixin, {
    */
   submit: function () {
     var self = this;
-    if(App.router.nextBtnClickInProgress){
+
+    if(App.get('router.nextBtnClickInProgress')){
       return;
     }
     if (this.get('isHostHaveWarnings')) {

+ 1 - 1
ambari-web/app/controllers/wizard/step4_controller.js

@@ -137,7 +137,7 @@ App.WizardStep4Controller = Em.ArrayController.extend({
    * @method submit
    */
   submit: function () {
-    if(App.router.nextBtnClickInProgress){
+    if(App.get('router.nextBtnClickInProgress')){
       return;
     }
     if (!this.get('isSubmitDisabled')) {

+ 1 - 1
ambari-web/app/controllers/wizard/step7_controller.js

@@ -1475,7 +1475,7 @@ App.WizardStep7Controller = Em.Controller.extend(App.ServerValidatorMixin, App.E
    * @method submit
    */
   submit: function () {
-    if (this.get('isSubmitDisabled') || App.router.nextBtnClickInProgress) {
+    if (this.get('isSubmitDisabled') || App.get('router.nextBtnClickInProgress')) {
       return false;
     }
     App.set('router.nextBtnClickInProgress', true);

+ 5 - 5
ambari-web/app/mappers/stack_mapper.js

@@ -27,6 +27,7 @@ App.stackMapper = App.QuickDataMapper.create({
     id: 'id',
     stack_name: 'stack_name',
     stack_version: 'stack_version',
+    stack_default: 'stack_default',
     show_available: 'show_available',
     type: 'type',
     repository_version: 'repository_version',
@@ -96,12 +97,11 @@ App.stackMapper = App.QuickDataMapper.create({
 
     var item = json;
     var stack = item.VersionDefinition;
-    var operatingSystemsArray = [];
-    var servicesArray = [];
-
     if (!stack.id) {
-      stack.id = stack.stack_name + "-" + stack.stack_version + "-" + stack.repository_version; //HDP-2.5-2.5.0.0
+      stack.id = stack.stack_name + '-' + stack.stack_version + '-' + stack.repository_version; //HDP-2.5-2.5.0.0
     }
+    var operatingSystemsArray = [];
+    var servicesArray = [];
 
     item.operating_systems.forEach(function(ops) {
       var operatingSystems = ops.OperatingSystems;
@@ -110,7 +110,7 @@ App.stackMapper = App.QuickDataMapper.create({
       ops.repositories.forEach(function(repo) {
         repo.Repositories.id = [stack.id, repo.Repositories.os_type, repo.Repositories.repo_id].join('-');
         repo.Repositories.os_id = [stack.id, repo.Repositories.os_type].join('-');
-        if (!repo.Repositories.latest_base_url)  repo.Repositories.latest_base_url = repo.Repositories.base_url;
+        if (!repo.Repositories.latest_base_url) repo.Repositories.latest_base_url = repo.Repositories.base_url;
         resultRepo.push(this.parseIt(repo.Repositories, this.get('configRepository')));
         repositoriesArray.pushObject(repo.Repositories);
       }, this);

+ 10 - 2
ambari-web/app/messages.js

@@ -554,6 +554,8 @@ Em.I18n.translations = {
 
   'installer.step1.header':'Select Version',
   'installer.step1.body':'Select the software version and method of delivery for your cluster. Using a Public Repository requires Internet connectivity. Using a Local Repository requires you have configured the software in a repository available in your network.',
+  'installer.step1.changeVersion.title':'Change Version',
+  'installer.step1.changeVersion.defaultVersion':'Default Version Definition',
   'installer.step1.selectUseRepoOptions.public':'Use Public Repository',
   'installer.step1.selectUseRepoOptions.public.networkLost.button':'Not Available',
   'installer.step1.selectUseRepoOptions.public.networkLost':'Why is this disabled?',
@@ -568,9 +570,12 @@ Em.I18n.translations = {
   'installer.step1.usePublicRepo.viewRepos':'View Repositories',
   'installer.step1.useLocalRepo.uploadFile': 'Upload Version Definition File',
   'installer.step1.useLocalRepo.uploadFile.error.title': 'Upload Version Definition File Error',
-  'installer.step1.useLocalRepo.getSurpottedOs.error.title': 'Cannot get supportted OS types',
+  'installer.step1.useLocalRepo.getSurpottedOs.error.title': 'Cannot get supported OS types',
   'installer.step1.useLocalRepo.addRepo.button': 'Add Repository',
+  'installer.step1.addVersion': 'Add Version',
+  'installer.step1.addVersion.stackChanged.popup.body': 'Stack is changed from {0} to {1}.',
   'installer.step1.useLocalRepo.enterUrl': 'Version Definition File URL',
+  'installer.step1.useLocalRepo.enterUrl.placeholder': 'Enter URL to Version Definition File',
   'installer.step1.useLocalRepo.readButton': 'Read Version Info',
   'installer.step1.useLocalRepo.infoForm.details.title': 'Details',
   'installer.step1.useLocalRepo.infoForm.details.stackName': 'Stack Name',
@@ -591,14 +596,17 @@ Em.I18n.translations = {
   'installer.step1.advancedRepo.localRepo.label.os':'Operating System',
   'installer.step1.advancedRepo.localRepo.label.baseUrl':'Repository Base URL',
   'installer.step1.advancedRepo.localRepo.label.stack':'Stack',
+  'installer.step1.advancedRepo.localRepo.placeholder': 'Enter Base URL or remove this OS',
   'installer.step1.advancedRepo.skipValidation.tooltip':'<b>Warning:</b> This is for advanced users only. Use this option if you want to skip validation for Repository Base URLs.',
   'installer.step1.advancedRepo.useRedhatSatellite.tooltip':'Disable distributed repositories and use RedHat Satellite/Spacewalk channels instead',
+  'installer.step1.advancedRepo.useRedhatSatellite.disabled.tooltip':'Use of RedHat Satellite/Spacewalk is not available when is using Public Repositories',
   'installer.step1.advancedRepo.skipValidation.message':'Skip Repository Base URL validation (Advanced)',
   'installer.step1.advancedRepo.useRedhatSatellite.message': 'Use RedHat Satellite/Spacewalk',
   'installer.step1.advancedRepo.useRedhatSatellite.warning': 'By selecting to <b>“Use RedHat Satellite/Spacewalk”</b> for the software repositories, ' +
     'you are responsible for configuring the repository channel in Satellite/Spacewalk and confirming the repositories for the selected <b>stack version</b> are available on the hosts in the cluster. ' +
     'Refer to the Ambari documentation for more information.',
-  'installer.step1.attentionNeeded':'<b>Attention:</b> Repository URLs are REQUIRED before you can proceed.',
+  'installer.step1.addOs.disabled.tooltip':'All Operating Systems have been added',
+  'installer.step1.attentionNeeded':'<b>Attention:</b> Repository Base URLs are REQUIRED before you can proceed.',
   'installer.step1.invalidURLAttention': '<b>Attention:</b> Please make sure all repository URLs are valid before proceeding.',
   'installer.step1.checkAtLeastOneAttention': '<b>Attention:</b> Please check at least one repository.',
   'installer.step1.retryRepoUrls': 'Click <b>here</b> to retry.',

+ 16 - 2
ambari-web/app/models/repository.js

@@ -43,12 +43,26 @@ App.Repository = DS.Model.extend({
   }.property('baseUrl'),
 
   invalidError: function() {
-    return this.get('validation') == App.Repository.validation['INVALID'];
+    return this.get('validation') === App.Repository.validation.INVALID;
   }.property('validation'),
 
+  /**
+   * @type {boolean}
+   */
+  isUtils: function () {
+    return this.get('repoName').contains('UTILS');
+  }.property('repoName'),
+
   undo: Em.computed.notEqualProperties('baseUrl', 'latestBaseUrl'),
 
-  clearAll: Em.computed.alias('baseUrl')
+  notEmpty: Em.computed.notEqual('baseUrl', ''),
+
+  clearAll: Em.computed.alias('baseUrl'),
+
+  /**
+   * @type {string}
+   */
+  placeholder: Em.computed.ifThenElse('isUtils', '', Em.I18n.t('installer.step1.advancedRepo.localRepo.placeholder')),
 
 });
 

+ 33 - 14
ambari-web/app/models/stack.js

@@ -19,9 +19,10 @@
 var App = require('app');
 
 App.Stack = DS.Model.extend({
-  id: DS.attr('string'), //  ${stackName}-${stackVersion}-${repoVersion}.
+  id: DS.attr('string'), //  ${stackName}-${stackVersion}-${repositoryVersion}.
   stackName: DS.attr('string'),
   stackVersion: DS.attr('string'),
+  stackDefault: DS.attr('boolean'),
   repositoryVersion: DS.attr('string'),
   showAvailable: DS.attr('boolean'),  // All of the instances should have this value to true. We should map only those stacks that has this flag set to true
   type: DS.attr('string'), // ["PATCH", "STANDARD"]
@@ -30,18 +31,21 @@ App.Stack = DS.Model.extend({
   operatingSystems: DS.hasMany('App.OperatingSystem'),
   isSelected: DS.attr('boolean', {defaultValue: false}),
 
-  stackNameVersion: function () {
-    //${stackName}-${stackVersion}.
-    return this.get('stackName') + '-' + this.get('stackVersion');
-  }.property('stackName', 'stackVersion'),
+  stackNameVersion: Em.computed.concat('-', 'stackName', 'stackVersion'),
 
-  isPatch: function () {
-    return this.get('type') == "PATCH";
-  }.property('type'),
-  displayName: function () {
-    //${stackName}-${repositoryVersion}.
-    return this.get('stackName') + '-' + this.get('repositoryVersion');
-  }.property('stackName', 'repositoryVersion'),
+  isPatch: Em.computed.equal('type', 'PATCH'),
+
+  displayName: Em.computed.concat('-', 'stackName', 'repositoryVersion'),
+
+  /**
+   * @type {boolean}
+   */
+  usePublicRepo: true,
+
+  /**
+   * @type {boolean}
+   */
+  useLocalRepo: false,
 
   /**
    * @return: {Array} returns supported repositories for all OperatingSystem's supported by a stack instance
@@ -49,13 +53,28 @@ App.Stack = DS.Model.extend({
   repositories: function () {
     var operatingSystems = this.get('operatingSystems');
     var repositories = [];
-    operatingSystems.forEach(function (os) {
+    operatingSystems.filterProperty('isSelected', true).forEach(function (os) {
       os.get('repositories').forEach(function (repository) {
         repositories.pushObject(repository);
       }, this);
     }, this);
     return repositories;
-  }.property('id')
+  }.property('operatingSystems.@each.isSelected'),
+
+  cleanReposBaseUrls: function () {
+    this.get('operatingSystems').forEach(function (os) {
+      os.get('repositories').setEach('baseUrl', '');
+    });
+  },
+
+  restoreReposBaseUrls: function () {
+    this.get('operatingSystems').forEach(function (os) {
+      os.get('repositories').forEach(function (repo) {
+        repo.set('baseUrl', repo.get('latestBaseUrl'));
+      });
+    });
+  }
+
 });
 
 

+ 1 - 1
ambari-web/app/models/stack_version/service_simple.js

@@ -29,7 +29,7 @@ App.ServiceSimple = DS.Model.extend({
   }.property('name'),
 
   doNotShowAndInstall: function () {
-    var skipServices = [];
+    var skipServices = ['KERBEROS'];
     if(!App.supports.installGanglia) {
       skipServices.push('GANGLIA');
     }

+ 2 - 2
ambari-web/app/routes/installer.js

@@ -131,7 +131,7 @@ module.exports = Em.Route.extend(App.RouterRedirections, {
             index: 1,
             name: 'enterUrl',
             url: '',
-            placeholder: 'Enter URL to Version Definition File',
+            placeholder: Em.I18n.t('installer.step1.useLocalRepo.enterUrl.placeholder'),
             hasError: false,
             isSelected: false
           }
@@ -156,7 +156,7 @@ module.exports = Em.Route.extend(App.RouterRedirections, {
     back: Em.Router.transitionTo('step0'),
     next: function (router) {
       console.time('step1 next');
-      if(App.router.nextBtnClickInProgress || router.transitionInProgress){
+      if(router.get('nextBtnClickInProgress') || router.transitionInProgress){
         return;
       }
       var wizardStep1Controller = router.get('wizardStep1Controller');

+ 40 - 25
ambari-web/app/styles/application.less

@@ -6197,6 +6197,10 @@ input[type="radio"].align-checkbox, input[type="checkbox"].align-checkbox {
   }
 }
 
+.icon-undo {
+  color: rgb(243, 178, 11);
+}
+
 .view-permission-header th {
   padding-top: 40px;
 }
@@ -6248,30 +6252,21 @@ input[type="radio"].align-checkbox, input[type="checkbox"].align-checkbox {
     font-weight: normal;
     cursor: pointer;
   }
-  #upload-definition-file-panel {
-    .register-version-options {
-      padding: 5px 0px;
-      .local-option-label {
-        margin-left: 20px;
-        padding-top: 4px;
-      }
-    }
-    .vdf-url {
-      input {
-        width: 80%;
-      }
-    }
-    .read-info-button {
-      margin: 10px 0px;
-    }
-  }
   #upload-definition-file-panel.disabled {
      .local-option-label {
        color: #999999;
      }
   }
 }
-#select-stack #repoVersionInfoForm {
+.register-version-options {
+  input.disabled {
+    background-color: #eee;
+  }
+  input[type="file"] {
+    margin-bottom: 10px;
+  }
+}
+#select-stack {
   .accordion-heading {
     background-color: #f0f0f0;
     font-weight: bold;
@@ -6281,6 +6276,9 @@ input[type="radio"].align-checkbox, input[type="checkbox"].align-checkbox {
       padding: 8px 15px;
     }
   }
+  .accordion-inner {
+    border-top: none;
+  }
   .accordion-body {
     .version-info-section {
       padding: 10px;
@@ -6290,7 +6288,6 @@ input[type="radio"].align-checkbox, input[type="checkbox"].align-checkbox {
       line-height: 14px;
     }
     .version-info {
-      font-weight: bold;
       padding-top: 5px;
       line-height: 14px;
     }
@@ -6302,14 +6299,19 @@ input[type="radio"].align-checkbox, input[type="checkbox"].align-checkbox {
     border: 1px solid #ddd;
     max-height: 200px;
     overflow: auto;
-    padding: 8px 25px;
-    margin: 8px;
+    margin: 8px 0;
+    .table {
+      margin-bottom: 0;
+      tr:first-child td {
+        border-top: none;
+      }
+    }
   }
   .repos-panel {
     .remove-icon {
       color: red;
-      margin: 20px 0px;
-      padding: 0px;
+      margin: 20px 0 0 0;
+      padding: 0;
       text-align: center;
       cursor: pointer;
       &.disabled {
@@ -6321,9 +6323,11 @@ input[type="radio"].align-checkbox, input[type="checkbox"].align-checkbox {
     }
     .repo-table-title {
       padding-left: 5px;
+      padding-bottom: 10px;
       border-bottom: 1px solid #ebebeb;
       label {
         font-weight: bold;
+        margin-top: 5px;
       }
     }
     .os-type-label {
@@ -6338,26 +6342,37 @@ input[type="radio"].align-checkbox, input[type="checkbox"].align-checkbox {
       padding-top: 10px;
     }
     .repo-name-url {
-      padding: 8px 0px;
+      padding-top: 8px;
+      .repo-name-url-inner {
+        margin-bottom: 8px;
+      }
     }
     .repo-url input {
       width: 90%;
       height: 24px;
     }
     .add-os-button {
-      margin-top:10px;
       button.disabled {
         cursor: not-allowed;
       }
     }
     #skip-validation {
       margin-top: 13px;
+      span.disabled {
+        opacity: 0.7;
+      }
     }
     #use-redhat, #skip-validation {
       input{
         margin: 0px 10px;
       }
     }
+    #use-redhat span.disabled {
+      opacity: 0.7;
+    }
+  }
+  #repoVersionInfoForm {
+
   }
 }
 #combo_search_box {

+ 43 - 0
ambari-web/app/styles/common.less

@@ -414,4 +414,47 @@
   -webkit-border-radius: 4px;
   -moz-border-radius: 4px;
   border-radius: 4px;
+}
+
+.tabs-left, .tabs-right {
+  border-bottom: none;
+  padding-top: 2px;
+}
+.tabs-left {
+  border-right: 1px solid #ddd;
+}
+.tabs-right {
+  border-left: 1px solid #ddd;
+}
+.tabs-left>li, .tabs-right>li {
+  float: none;
+  margin-bottom: 2px;
+}
+.tabs-left>li {
+  margin-right: -1px;
+}
+.tabs-right>li {
+  margin-left: -1px;
+}
+.tabs-left>li.active>a,
+.tabs-left>li.active>a:hover,
+.tabs-left>li.active>a:focus {
+  border-bottom-color: #ddd;
+  border-right-color: transparent;
+}
+
+.tabs-right>li.active>a,
+.tabs-right>li.active>a:hover,
+.tabs-right>li.active>a:focus {
+  border-bottom: 1px solid #ddd;
+  border-left-color: transparent;
+}
+.tabs-left>li>a {
+  border-radius: 4px 0 0 4px;
+  margin-right: 0;
+  display:block;
+}
+.tabs-right>li>a {
+  border-radius: 0 4px 4px 0;
+  margin-right: 0;
 }

+ 169 - 157
ambari-web/app/templates/wizard/step1.hbs

@@ -19,188 +19,200 @@
   <h2>{{t installer.step1.header}}</h2>
   <p class="alert alert-info">{{t installer.step1.body}}</p>
 
-  <label class="radio big-radio">{{view view.usePublicRepoRadioButton}} {{t installer.step1.selectUseRepoOptions.public}}
-      {{#unless view.selectedPublicRepoVersion}}
-          <a id="public-disabled-link" {{action "openPublicOptionDisabledWindow" target="view"}}>{{t installer.step1.selectUseRepoOptions.public.networkLost}}</a>
-      {{/unless}}
-  </label>
-
-    <form {{bindAttr class="optionsToSelect.useLocalRepo.isSelected:disabled :stack-version-selection :row-fluid"}}>
-      <div class="span3 select-version-label">
-        {{t installer.step1.selectUseRepoOptions.public.select}}
-      </div>
-      <div class="span7 right-stack-info">
-        {{#if view.selectedPublicRepoVersion}}
-          <div class="repo-list-button btn-group">
-            <button type="button" {{bindAttr class="optionsToSelect.useLocalRepo.isSelected:disabled :btn :btn-primary :dropdown-toggle"}} data-toggle="dropdown">
-              {{view.selectedPublicRepoVersion.displayName}} &nbsp;<span class="caret"></span>
-            </button>
-            <ul class="dropdown-menu available-repos-dropdown">
-              {{#if view.availableStackRepoList}}
-                {{#each repo in view.availableStackRepoList}}
-                  {{#if repo.repositoryVersion}}
-                    <li><a {{action "selectRepoInList" repo target="view"}}>{{repo.displayName}}</a></li>
-                  {{/if}}
-                {{/each}}
-              {{else}}
-                <li><a class="disabled">{{t installer.step1.usePublicRepo.ReposList.empty}}</a></li>
-              {{/if}}
-            </ul>
-          </div>
-        {{else}}
-          <button type="button" class="disabled btn btn-primary dropdown-toggle"}} data-toggle="dropdown">
-            {{t installer.step1.selectUseRepoOptions.public.networkLost.button}}
-          </button>
-        {{/if}}
-      </div>
-    </form>
-
-    {{!--Local repo loaded info below--}}
-  <label class="radio big-radio">{{view view.useLocalRepoRadioButton}} {{t installer.step1.selectUseRepoOptions.local}}</label>
-
-    <div id="upload-definition-file-panel" {{bindAttr class="optionsToSelect.usePublicRepo.isSelected:disabled"}}>
-      <div class="clearfix register-version-options row-fluid">
-        <div class="span5 option-radio-button">
-          <label class="local-option-label radio">
-            {{view view.uploadFileRadioButton}} {{t installer.step1.useLocalRepo.uploadFile}}
-          </label>
-        </div>
-        <div class="span7">
-          {{#if view.isFileApi}}
-            {{view App.VersionDefinitionFileUploader disabledBinding="view.fileBrowserDisabled"}}
-          {{/if}}
-        </div>
-      </div>
-      <div class="clearfix register-version-options row-fluid">
-        <div class="span5 option-radio-button">
-          <label class="local-option-label radio">
-            {{view view.enterUrlRadioButton}} {{t installer.step1.useLocalRepo.enterUrl}}
-          </label>
-        </div>
-        <div class="span7 vdf-url">
-          <div {{bindAttr class="optionsToSelect.useLocalRepo.enterUrl.name optionsToSelect.useLocalRepo.enterUrl.url.hasError:has-error }"}}>
-            {{view Ember.TextField valueBinding="optionsToSelect.useLocalRepo.enterUrl.url" placeholderBinding="optionsToSelect.useLocalRepo.enterUrl.placeholder" disabledBinding="view.enterUrlFieldDisabled"}}
-          </div>
-        </div>
-        <div class="span12 read-info-button">
-          <button {{bindAttr class="view.readInfoButtonDisabled:disabled :btn :btn-primary :pull-right"}}
-            {{action "readVersionInfo" target="controller"}}> {{t installer.step1.useLocalRepo.readButton}}</button>
-        </div>
-      </div>
+  {{! left tabs }}
+  <div class="row-fluid">
+    <div class="span2">
+      <ul class="nav nav-tabs tabs-left">
+        {{#each stack in availableStackTypes}}
+          <li {{bindAttr class="stack.isSelected:active"}}><a {{action "selectRepoInList" stack target="controller"}} href="#">{{stack.stackName}}</a></li>
+        {{/each}}
+      </ul>
     </div>
 
-    <form id="repoVersionInfoForm" class="form-horizontal" role="form" name="localVersionInfoForm" novalidate>
-      <div class="accordion-group details-panel">
-        <div class="accordion-heading">
-          <p>{{t installer.step1.useLocalRepo.infoForm.details.title}}</p>
-        </div>
-        <div class="accordion-body">
-          <div class="accordion-inner">
-            <div class="row-fluid">
-              <div class="span5 version-info-section">
-                <div class="row-fluid">
-                  <label class="control-label span5">{{t installer.step1.useLocalRepo.infoForm.details.stackName}}</label>
-                  <div class="version-info span5">{{controller.selectedStack.stackNameVersion}}</div>
-                  {{#if controller.selectedStack.isPatch}}
-                      <div class="span2 patch-icon"><i class="icon-umbrella"></i>&nbsp;{{t common.patch}}</div>
-                  {{/if}}
-                </div>
-                <div class="row-fluid">
-                  <label class="control-label span5">{{t installer.step1.useLocalRepo.infoForm.details.displayName}}</label>
-                  <div class="version-info span7">{{controller.selectedStack.displayName}}</div>
+    <div class="span10">
+      <div class="tab-content">
+        <div class="accordion-group details-panel">
+          <div class="accordion-body">
+            <div class="accordion-inner">
+              <div class="row-fluid">
+                <div class="version-info">
+                  <div class="btn-group">
+                    <button type="button" data-toggle="dropdown" class="btn dropdown-toggle btn-info">{{controller.selectedStack.displayName}} <span class="caret"></span></button>
+                    <ul class="dropdown-menu">
+                      {{#each stack in selectedStackType.visibleStacks}}
+                        <li>
+                          {{!view view.stackRadioButton stackBinding="stack"}}
+                          <a href="#" {{action "changeVersion" stack target="controller"}}>{{stack.displayName}}
+                            {{#if stack.stackDefault}}
+                              ({{t installer.step1.changeVersion.defaultVersion}})
+                            {{/if}}
+                          </a>
+                        </li>
+                      {{/each}}
+                      <li><a href="#" {{action "uploadVdf" target="controller"}}>{{t installer.step1.addVersion}} ...</a></li>
+                    </ul>
+                  </div>
                 </div>
-                <div class="row-fluid">
-                  <label class="control-label span5">{{t installer.step1.useLocalRepo.infoForm.details.version}}</label>
-                  <div class="version-info span7">{{controller.selectedStack.repositoryVersion}}</div>
+                <div class="version-contents-section">
+                  {{#unless servicesForSelectedStack}}
+                    <div class="alert alert-info" role="alert">{{t installer.step1.useLocalRepo.infoForm.content.empty}}</div>
+                  {{/unless}}
+                  <table class="table table-striped table-condensed">
+                  {{#each service in servicesForSelectedStack}}
+                    <tr>
+                      <td class="span4">{{service.displayName}}</td>
+                      <td class="span8">{{service.latestVersion}}</td>
+                    </tr>
+                  {{/each}}
+                  </table>
                 </div>
               </div>
-              <div class="span6 version-contents-section">
-                {{#unless view.selectedServices}}
-                  <div class="alert alert-info" role="alert">{{t installer.step1.useLocalRepo.infoForm.content.empty}}</div>
-                {{/unless}}
-                {{#each service in view.selectedServices}}
-                  <div class="clearfix row-fluid">
-                      <div class="version-info span10">{{service.displayName}} ({{service.version}})</div>
-                  </div>
-                {{/each}}
-              </div>
             </div>
           </div>
         </div>
       </div>
-      <div class="accordion-group repos-panel">
-        <div class="accordion-heading">
-          <p>{{t common.repositories}}</p>
-        </div>
-          <div class="accordion-body version-contents-body">
-            <div class="accordion-inner">
-              <div class="alert alert-info" role="alert">{{t installer.step1.useLocalRepo.infoForm.alert.baseUrl}}</div>
-              {{#if view.hasValidationErrors}}
-                <div class="alert alert-warning" role="alert">{{t installer.step1.useLocalRepo.infoForm.alert.warning}}</div>
-              {{/if}}
+    </div>
+  </div>
+  {{! left tabs end }}
 
-              <div class="clearfix repo-table-title row-fluid">
-                <div class="span2"><label>{{t common.os}}</label></div>
-                <div class="span2"><label>{{t common.name}}</label></div>
-                <div class="span7"><label>{{t installer.step1.advancedRepo.localRepo.column.baseUrl}}</label></div>
-              </div>
+  {{! Public Repository radio }}
+  <label class="radio big-radio">
+    {{view view.usePublicRepoRadioButton}} {{t installer.step1.selectUseRepoOptions.public}}
+    {{#if networkIssuesExist}}
+      <a id="public-disabled-link" {{action "openPublicOptionDisabledWindow" target="view"}}>{{t installer.step1.selectUseRepoOptions.public.networkLost}}</a>
+    {{/if}}
+  </label>
+  {{! Public Repository radio END }}
 
-              {{#each operatingSystem in view.operatingSystems}}
-                {{#if operatingSystem.isSelected}}
-                  <div class="clearfix row-fluid border-bottom">
-                    <div class="span2 os-type-label">
-                      <label>{{operatingSystem.osType}}</label>
-                    </div>
-                    <div class="span9">
-                      {{#each repository in operatingSystem.repositories}}
-                        <div class="repo-name-url row-fluid" {{bindAttr class="repository.hasError:has-error"}} >
-                          <label class="repo-name-label control-label span3">{{repository.repoId}}</label>
-                          <div class="validation-td span1">
-                            {{#if repository.validation}}
-                                {{view view.popoverView repositoryBinding="repository"}}
-                            {{/if}}
-                          </div>
-                          <div {{bindAttr class=":span8 :repo-url repository.invalidFormatError:textfield-error repository.invalidError:textfield-error"}}>
-                            {{view Ember.TextField valueBinding="repository.baseUrl" disabledBinding="controller.selectedStack.useRedhatSatellite"}}
-                          </div>
-                        </div>
-                      {{/each}}
-                    </div>
-                    <div {{bindAttr class=":span1 :remove-icon controller.selectedStack.useRedhatSatellite:disabled"}} {{action "removeOS" operatingSystem target="controller"}}><i class="icon-minus"></i>{{t common.remove}}</div>
-                  </div>
-                {{/if}}
-              {{/each}}
+  {{!--Local repo loaded info below--}}
+  <label class="radio big-radio">{{view view.useLocalRepoRadioButton}} {{t installer.step1.selectUseRepoOptions.local}}</label>
 
+  {{#if App.router.nextBtnClickInProgress}}
+    {{view App.SpinnerView}}
+  {{else}}
+  <form id="repoVersionInfoForm" class="form-horizontal" role="form" name="localVersionInfoForm" novalidate>
+
+    <div class="accordion-group repos-panel">
+      <div class="accordion-heading">
+        <p>{{t common.repositories}}</p>
+      </div>
+      <div class="accordion-body version-contents-body">
+        <div class="accordion-inner">
+          <div class="alert alert-info" role="alert">{{t installer.step1.useLocalRepo.infoForm.alert.baseUrl}}</div>
+          {{#if view.hasValidationErrors}}
+            <div class="alert alert-warning" role="alert">{{t installer.step1.useLocalRepo.infoForm.alert.warning}}</div>
+          {{/if}}
+
+          {{! OSes and Repositories }}
+          <div class="clearfix repo-table-title row-fluid">
+            <div class="span2"><label>{{t common.os}}</label></div>
+            <div class="span9">
+              <div class="span3"><label>{{t common.name}}</label></div>
+              <div class="span1"></div>
+              <div class="span8"><label>{{t installer.step1.advancedRepo.localRepo.column.baseUrl}}</label></div>
+            </div>
+            <div class="span1">
+              {{! Add OS }}
               <div class="add-os-button btn-group pull-right">
-                <button {{bindAttr class=":btn :dropdown-toggle view.isAddOsButtonDisabled:disabled"}} data-toggle="dropdown">
+                <button
+                  type="button" {{bindAttr data-original-title="view.addOsButtonTooltip"  class=":btn :dropdown-toggle :add-os-button view.isAddOsButtonDisabled:disabled"}}
+                  data-toggle="dropdown">
                   <i class="icon-plus"></i> {{t common.add}} &nbsp;<span class="caret"></span>
                 </button>
                 <ul class="dropdown-menu">
-                  {{#each operatingSystem in view.operatingSystems}}
+                  {{#each operatingSystem in selectedStack.operatingSystems}}
                     {{#unless operatingSystem.isSelected}}
                       <li><a {{action "addOS" operatingSystem target="controller"}}>{{operatingSystem.osType}}</a></li>
                     {{/unless}}
                   {{/each}}
                 </ul>
               </div>
-              <div id="skip-validation" {{bindAttr class="controller.selectedStack.useRedhatSatellite:disabled"}}>
-                <label>{{view Ember.Checkbox checkedBinding="skipValidationChecked" disabledBinding="controller.selectedStack.useRedhatSatellite" class="checkbox"}}{{t installer.step1.advancedRepo.skipValidation.message}}
-                  <i class="icon-question-sign" rel="skip-validation-tooltip"
-                     data-toggle="tooltip" {{translateAttr title="installer.step1.advancedRepo.skipValidation.tooltip"}}></i></label>
-              </div>
-              <div id="use-redhat">
-                <label>{{view Ember.Checkbox classNames="align-checkbox" checkedBinding="controller.selectedStack.useRedhatSatellite"}}{{t installer.step1.advancedRepo.useRedhatSatellite.message}}
-                  <i class="icon-question-sign" rel="use-redhat-tooltip"
-                    data-toggle="tooltip" {{translateAttr title="installer.step1.advancedRepo.useRedhatSatellite.tooltip"}}>
-                  </i>
-                </label>
-              </div>
+              {{! Add OS END}}
             </div>
           </div>
-      </div>
-    </form>
 
+          {{#each operatingSystem in selectedStack.operatingSystems}}
+            {{#if operatingSystem.isSelected}}
+              <div class="clearfix row-fluid border-bottom">
+                <div class="span2 os-type-label">
+                  <label>{{operatingSystem.osType}}</label>
+                </div>
+                <div class="span9 repo-name-url">
+                  {{#each repository in operatingSystem.repositories}}
+                    <div class="clearfix repo-name-url-inner">
+                      <div class="span3">
+                        <label class="repo-name-label control-label">{{repository.repoId}}</label>
+                      </div>
+                      <div class="validation-td span1">
+                        {{#if repository.validation}}
+                          {{view view.popoverView repositoryBinding="repository"}}
+                        {{/if}}
+                      </div>
+                      <div {{bindAttr class=":span8 :repo-url repository.invalidFormatError:textfield-error repository.invalidError:textfield-error"}}>
+                        {{view Ember.TextField placeholderBinding="repository.placeholder" valueBinding="repository.baseUrl" disabledBinding="controller.selectedStack.useRedhatSatellite"}}
+                          {{#if controller.selectedStack.usePublicRepo}}
+                            {{#if repository.undo}}
+                              <i class="icon-undo" data-toggle="tooltip"
+                                {{action "doRestoreDefaultValue" repository target="controller"}}
+                                {{translateAttr title="common.undo"}}>
+                              </i>
+                            {{/if}}
+                          {{else}}
+                            {{#if repository.notEmpty}}
+                              <i class="icon-undo" data-toggle="tooltip"
+                                {{action "doRestoreToEmpty" repository target="controller"}}
+                                {{translateAttr title="common.undo"}}>
+                              </i>
+                            {{/if}}
+                          {{/if}}
+                      </div>
+                    </div>
+                  {{/each}}
+                </div>
+                <div {{bindAttr class=":span1 :remove-icon controller.selectedStack.useRedhatSatellite:disabled"}} {{action "removeOS" operatingSystem target="controller"}}>
+                  <i class="icon-minus"></i>{{t common.remove}}</div>
+              </div>
+            {{/if}}
+          {{/each}}
+          {{! OSes and Repositories END }}
+
+          {{! Skip Repository Base URL validation }}
+          <div id="skip-validation" {{bindAttr class="controller.selectedStack.useRedhatSatellite:disabled"}}>
+            <label>
+              {{view Ember.Checkbox checkedBinding="skipValidationChecked" disabledBinding="controller.selectedStack.useRedhatSatellite" class="checkbox"}}
+              <span {{bindAttr class="controller.selectedStack.useRedhatSatellite:disabled"}}>{{t installer.step1.advancedRepo.skipValidation.message}}</span>
+              <i class="icon-question-sign" rel="skip-validation-tooltip"
+                 data-toggle="tooltip" {{translateAttr title="installer.step1.advancedRepo.skipValidation.tooltip"}}>
+              </i>
+            </label>
+          </div>
+          {{! Skip Repository Base URL validation END }}
+
+          {{! Use RedHat Satellite/Spacewalk }}
+          <div id="use-redhat">
+            <label>
+              {{view view.redhatCheckBoxView}}
+              <span {{bindAttr class=":redhat-label controller.selectedStack.usePublicRepo:disabled" data-original-title="view.redhatDisabledTooltip"}}>{{t installer.step1.advancedRepo.useRedhatSatellite.message}}</span>
+              <i class="icon-question-sign" rel="use-redhat-tooltip"
+                 data-toggle="tooltip" {{translateAttr title="installer.step1.advancedRepo.useRedhatSatellite.tooltip"}}>
+              </i>
+            </label>
+          </div>
+          {{! Use RedHat Satellite/Spacewalk END }}
+        </div>
+      </div>
+    </div>
+  </form>
+  {{/if}}
+  {{#if view.invalidFormatUrlExist}}
+    <div class="alert">{{t installer.step1.attentionNeeded}}</div>
+  {{/if}}
+  {{#if view.invalidUrlExist}}
+    <div class="alert">
+      {{t installer.step1.invalidURLAttention}}
+      <a href="javascript:void(null)" {{action "retryRepoUrls" target="view"}}>{{t installer.step1.retryRepoUrls}}</a>
+    </div>
+  {{/if}}
   <a class="btn pull-left installer-back-btn" {{action back}}>&larr; {{t common.back}}</a>
   <button class="btn btn-success pull-right" {{bindAttr disabled="view.isSubmitDisabled"}} {{action next}}>{{t common.next}} &rarr;</button>
-
 </div>

+ 25 - 0
ambari-web/app/templates/wizard/step1/public_option_disabled_window_body.hbs

@@ -0,0 +1,25 @@
+{{!
+* 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.
+}}
+
+
+<div class="public-disabled-message">{{t installer.step1.selectUseRepoOptions.public.networkLost.popup.msg}}</div>
+<ul>
+  <li class="public-disabled-option">{{t installer.step1.selectUseRepoOptions.public.networkLost.popup.msg1}}</li>
+  <li class="public-disabled-option">{{t installer.step1.selectUseRepoOptions.public.networkLost.popup.msg2}}</li>
+  <li class="public-disabled-option">{{t installer.step1.selectUseRepoOptions.public.networkLost.popup.msg3}}</li>
+</ul>

+ 44 - 0
ambari-web/app/templates/wizard/step1/vdf_upload.hbs

@@ -0,0 +1,44 @@
+{{!
+* 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.
+}}
+
+<div class="clearfix register-version-options row-fluid">
+  {{#view "view.uploadFileView"}}
+    <div class="span5 option-radio-button">
+      <label class="local-option-label radio">
+        {{view view.uploadFileRadioButton}} {{t installer.step1.useLocalRepo.uploadFile}}
+      </label>
+    </div>
+    <div class="span12">
+      {{#if view.isFileApi}}
+        {{view view.fileInputView}}
+      {{/if}}
+    </div>
+  {{/view}}
+  {{#view "view.enterUrlView"}}
+    <div class="option-radio-button">
+      <label class="local-option-label radio">
+        {{view view.enterUrlRadioButton}} {{t installer.step1.useLocalRepo.enterUrl}}
+      </label>
+    </div>
+    <div class="span12 vdf-url">
+      <div {{bindAttr class="optionsToSelect.useLocalRepo.enterUrl.name optionsToSelect.useLocalRepo.enterUrl.url.hasError:has-error"}}>
+        {{view view.enterUrlField class="input-block-level" valueBinding="optionsToSelect.useLocalRepo.enterUrl.url" placeholderBinding="optionsToSelect.useLocalRepo.enterUrl.placeholder"}}
+      </div>
+    </div>
+  {{/view}}
+</div>

+ 32 - 0
ambari-web/app/utils/array_utils.js

@@ -16,6 +16,10 @@
  * limitations under the License.
  */
 
+function _parseId(id) {
+  return id.replace(/[^\d|\.]/g, '').split('.').map(function (i) {return parseInt(i, 10);});
+}
+
 module.exports = {
   /**
    *
@@ -52,5 +56,33 @@ module.exports = {
     });
 
     return intersection;
+  },
+
+  /**
+   * Callback for sorting models with `id`-property equal to something like version number: 'HDP-1.2.3', '4.2.52' etc
+   *
+   * @param {{id: string}} obj1
+   * @param {{id: string}} obj2
+   * @returns {number}
+   */
+  sortByIdAsVersion: function (obj1, obj2) {
+    var id1 = _parseId(Em.get(obj1, 'id'));
+    var id2 = _parseId(Em.get(obj2, 'id'));
+    var lId1 = id1.length;
+    var lId2 = id2.length;
+    var limit = lId1 > lId2 ? lId2 : lId1;
+    for (var i = 0; i < limit; i++) {
+      if (id1[i] > id2[i]) {
+        return 1;
+      }
+      if (id1[i] < id2[i]) {
+        return -1;
+      }
+    }
+    if (lId1 === lId2) {
+      return 0
+    }
+    return lId1 > lId2 ? 1 : -1;
   }
+
 };

+ 97 - 196
ambari-web/app/views/wizard/step1_view.js

@@ -26,85 +26,39 @@ App.WizardStep1View = Em.View.extend({
   didInsertElement: function () {
     $("[rel=skip-validation-tooltip]").tooltip({ placement: 'right'});
     $("[rel=use-redhat-tooltip]").tooltip({ placement: 'right'});
-    if (this.get('controller.selectedStack') && this.get('controller.selectedStack.showAvailable')) {
+    $('.add-os-button,.redhat-label').tooltip();
+    if (this.get('controller.selectedStack.showAvailable')) {
       // first time load
-      this.set('controller.latestSelectedPublicRepoId',this.get('controller.selectedStack.id'));
+      if (this.get('controller.selectedStack.useRedhatSatellite')) {
+        // restore `use local repo` on page refresh
+        this.get('controller').useLocalRepo();
+      }
     } else {
       var selected = this.get('controller.content.stacks') && this.get('controller.content.stacks').findProperty('showAvailable');
-      if (selected) {
-        // get back from other steps, set default public repo as selected public
-        this.set('controller.latestSelectedPublicRepoId', selected.get('id'));
-      } else {
+      if (!selected) {
         // network disconnection
-        this.set('controller.latestSelectedPublicRepoId', null);
-        this.set('controller.optionsToSelect.useLocalRepo.isSelected', true);
-        this.set('controller.optionsToSelect.usePublicRepo.isSelected', false);
+        Em.trySet(this, 'controller.selectedStack.useLocalRepo', true);
+        Em.trySet(this, 'controller.selectedStack.usePublicRepo', false);
       }
     }
   },
 
-  /**
-   * =========================== Option "Use Public Repository" starts from here ==================
-   */
-
-  /**
-   * The public reo version shown on the dropdown button
-   */
-  selectedPublicRepoVersion: function () {
-    var selectedId = this.get('controller.latestSelectedPublicRepoId');
-    return selectedId ? this.get('controller.content.stacks').findProperty('id', selectedId): null;
-  }.property('controller.latestSelectedPublicRepoId'),
+  willDestroyElement: function () {
+    $("[rel=skip-validation-tooltip]").tooltip('destroy');
+    $("[rel=use-redhat-tooltip]").tooltip('destroy');
+    $('.add-os-button,.redhat-label').tooltip('destroy');
+  },
 
   /**
-   * List of other available stack repos within the same stack name
-   * @type {Em.Object[]}
+   * Show possible reasons why Public Repo is disabled
+   *
+   * @returns {App.ModalPopup}
    */
-  availableStackRepoList: function () {
-    var selectedStack = this.get('controller.selectedStack');
-    var availableStackRepos = this.get('controller.content.stacks').filter(function (item) {
-      return item.get('showAvailable') && item.get('id') != selectedStack.get('id');
-    });
-    return availableStackRepos.toArray().map(function (stack) {
-      return Em.Object.create({
-        id: stack.get('id'),
-        repositoryVersion: stack.get('repositoryVersion'),
-        displayName: stack.get('stackName') + "-" + stack.get('repositoryVersion'),
-        isSelected: false
-      });
-    });
-  }.property('controller.selectedStack'),
-
-  selectRepoInList: function (event) {
-    if (this.get('controller.optionsToSelect.useLocalRepo.isSelected')) return;
-    this.get('controller.content.stacks').setEach('isSelected', false);
-    this.get('controller.content.stacks').findProperty('id', event.context.id).set('isSelected', true);
-    this.set('controller.latestSelectedPublicRepoId',event.context.id);
-  },
-
-  selectedServices: function () {
-    var selectedStack = this.get('controller.selectedStack');
-    return Em.isNone(selectedStack) ? [] : selectedStack.get('stackServices').toArray().filter(function (service) {
-      return !service.get('isHidden');
-    }).map(function (service) {
-      return Em.Object.create({
-        displayName: service.get('displayName'),
-        version: service.get('latestVersion')
-      });
-    });
-  }.property('controller.selectedStack'),
-
   openPublicOptionDisabledWindow: function () {
     return App.ModalPopup.show({
       header: Em.I18n.t('installer.step1.selectUseRepoOptions.public.networkLost.popup.title'),
-      message: Em.I18n.t('installer.step1.selectUseRepoOptions.public.networkLost.popup.msg'),
-      option1: Em.I18n.t('installer.step1.selectUseRepoOptions.public.networkLost.popup.msg1'),
-      option2: Em.I18n.t('installer.step1.selectUseRepoOptions.public.networkLost.popup.msg2'),
-      option3: Em.I18n.t('installer.step1.selectUseRepoOptions.public.networkLost.popup.msg3'),
       bodyClass: Ember.View.extend({
-        template: Em.Handlebars.compile('<div class="public-disabled-message">{{message}}</div>' +
-          '<li class="public-disabled-option">{{option1}}</li>' +
-          '<li class="public-disabled-option">{{option2}}</li>' +
-          '<li class="public-disabled-option">{{option3}}</li>')
+        templateName: require('templates/wizard/step1/public_option_disabled_window_body')
       }),
       secondary: false
     });
@@ -112,9 +66,10 @@ App.WizardStep1View = Em.View.extend({
 
   /**
    * Disable submit button flag
+   *
    * @type {bool}
    */
-  isSubmitDisabled: Em.computed.or('controller.content.isCheckInProgress'),
+  isSubmitDisabled: Em.computed.or('invalidFormatUrlExist', 'isNoOsChecked', 'controller.content.isCheckInProgress'),
 
   /**
    * Onclick handler for recheck repos urls. Used in Advanced Repository Options.
@@ -123,164 +78,107 @@ App.WizardStep1View = Em.View.extend({
     App.router.get('installerController').checkRepoURL(this.get('controller'));
   },
 
-
-  /**
-   * =========================== Option "Use Local Repository" starts from here ==================
-   */
-
   /**
    * Checkbox for use Public repo
+   *
    * @type {Ember.Checkbox}
    */
   usePublicRepoRadioButton: Em.Checkbox.extend({
     tagName: 'input',
     attributeBindings: [ 'type', 'checked' ],
     classNames: [''],
-    checked: Em.computed.alias('controller.optionsToSelect.usePublicRepo.isSelected'),
+    checked: Em.computed.alias('controller.selectedStack.usePublicRepo'),
     type: 'radio',
-    disabled: function() {
-      return !this.get('controller.latestSelectedPublicRepoId');
-    }.property('controller.latestSelectedPublicRepoId'),
+    disabled: Em.computed.alias('controller.networkIssuesExist'),
 
     click: function () {
-      this.set('controller.optionsToSelect.usePublicRepo.isSelected', true);
-      this.set('controller.optionsToSelect.useLocalRepo.isSelected', false);
-      var latestSelectedPublicRepoId = this.get('controller.latestSelectedPublicRepoId');
-      if (latestSelectedPublicRepoId) {
-        this.get('controller.content.stacks').setEach('isSelected', false);
-        this.get('controller.content.stacks').findProperty('id', latestSelectedPublicRepoId).set('isSelected', true);
-      } else {
-        // make the 1st public repo as selected
-        this.get('controller.content.stacks').findProperty('id').set('isSelected', true);
-      }
+      this.get('controller').usePublicRepo();
     }
   }),
 
   /**
    * Checkbox for use Public repo
+   *
    * @type {Ember.Checkbox}
    */
   useLocalRepoRadioButton: Em.Checkbox.extend({
     tagName: 'input',
     attributeBindings: [ 'type', 'checked' ],
     classNames: [''],
-    checked: Em.computed.alias('controller.optionsToSelect.useLocalRepo.isSelected'),
+    checked: Em.computed.alias('controller.selectedStack.useLocalRepo'),
     type: 'radio',
 
     click: function () {
-      this.set('controller.optionsToSelect.useLocalRepo.isSelected', true);
-      this.set('controller.optionsToSelect.usePublicRepo.isSelected', false);
-      var latestSelectedLocalRepoId = this.get('controller.latestSelectedLocalRepoId');
-      if (latestSelectedLocalRepoId) {
-        this.get('controller.content.stacks').setEach('isSelected', false);
-        this.get('controller.content.stacks').findProperty('id', latestSelectedLocalRepoId).set('isSelected', true);
-      }
+      this.get('controller').useLocalRepo();
     }
   }),
 
   /**
-   * Checkbox for Use local Repo > Upload VDF file
-   * @type {Ember.Checkbox}
+   * User already selected all OSes
+   *
+   * @type {boolean}
    */
-  uploadFileRadioButton: Em.Checkbox.extend({
-    tagName: 'input',
-    attributeBindings: [ 'type', 'checked' ],
-    classNames: [''],
-    checked: Em.computed.alias('controller.optionsToSelect.useLocalRepo.uploadFile.isSelected'),
-    type: 'radio',
-    disabled: function () {
-      return this.get("controller.optionsToSelect.usePublicRepo.isSelected");
-    }.property("controller.optionsToSelect.usePublicRepo.isSelected"),
-
-    click: function () {
-      this.set('controller.optionsToSelect.useLocalRepo.uploadFile.isSelected', true);
-      this.set('controller.optionsToSelect.useLocalRepo.enterUrl.isSelected', false);
-      this.set('controller.optionsToSelect.useLocalRepo.enterUrl.hasError', false);
-      this.set('controller.optionsToSelect.useLocalRepo.uploadFile.hasError', false);
-    }
-  }),
+  allOsesSelected: Em.computed.everyBy('controller.selectedStack.operatingSystems', 'isSelected', true),
 
   /**
-   * Checkbox for Use local Repo > Enter Url of VDF file
-   * @type {Ember.Checkbox}
+   * Disallow adding OS if all OSes are already added or user select <code>useRedhatSatellite</code>
+   *
+   * @type {boolean}
    */
-  enterUrlRadioButton: Em.Checkbox.extend({
-    tagName: 'input',
-    attributeBindings: [ 'type', 'checked' ],
-    classNames: [''],
-    checked: Em.computed.alias('controller.optionsToSelect.useLocalRepo.enterUrl.isSelected'),
-    type: 'radio',
-    disabled: function () {
-      return this.get("controller.optionsToSelect.usePublicRepo.isSelected");
-    }.property("controller.optionsToSelect.usePublicRepo.isSelected"),
+  isAddOsButtonDisabled: Em.computed.or('allOsesSelected', 'controller.selectedStack.useRedhatSatellite'),
 
-    click: function () {
-      this.set('controller.optionsToSelect.useLocalRepo.enterUrl.isSelected', true);
-      this.set('controller.optionsToSelect.useLocalRepo.uploadFile.isSelected', false);
-      this.set('controller.optionsToSelect.useLocalRepo.enterUrl.hasError', false);
-      this.set('controller.optionsToSelect.useLocalRepo.uploadFile.hasError', false);
-    }
-  }),
-
- /*
-  * Is File API available
-  * @type {bool}
-  */
-  isFileApi: function () {
-    return window.File && window.FileReader && window.FileList;
-  }.property(),
-
-  fileBrowserDisabled: function () {
-    return this.get("controller.optionsToSelect.usePublicRepo.isSelected") || this.get("controller.optionsToSelect.useLocalRepo.enterUrl.isSelected");
-  }.property("controller.optionsToSelect.usePublicRepo.isSelected", "controller.optionsToSelect.useLocalRepo.enterUrl.isSelected"),
-  enterUrlFieldDisabled: function () {
-    return this.get("controller.optionsToSelect.usePublicRepo.isSelected") || this.get("controller.optionsToSelect.useLocalRepo.uploadFile.isSelected");
-  }.property("controller.optionsToSelect.usePublicRepo.isSelected", "controller.optionsToSelect.useLocalRepo.uploadFile.isSelected"),
-  readInfoButtonDisabled: function () {
-    if (this.get('controller.optionsToSelect.useLocalRepo.isSelected')) {
-      if(this.get('controller.optionsToSelect.useLocalRepo.uploadFile.isSelected')) {
-        return !this.get('controller.optionsToSelect.useLocalRepo.uploadFile.file');
-      } else if (this.get('controller.optionsToSelect.useLocalRepo.enterUrl.isSelected')) {
-        return !this.get('controller.optionsToSelect.useLocalRepo.enterUrl.url');
-      }
-    } else {
-      return true;
-    }
-  }.property('controller.optionsToSelect.useLocalRepo.isSelected', 'controller.optionsToSelect.useLocalRepo.uploadFile.isSelected',
-    'controller.optionsToSelect.useLocalRepo.uploadFile.file', 'controller.optionsToSelect.useLocalRepo.enterUrl.url'),
-
-  operatingSystems: function () {
-    var selectedStack = this.get('controller.selectedStack');
-    return Em.isNone(selectedStack) ? [] : selectedStack.get('operatingSystems');
-  }.property('controller.selectedStack'),
-
-  isAddOsButtonDisabled: function () {
-    return this.get('operatingSystems').get('length') == this.get('operatingSystems').filterProperty('isSelected').get('length') || this.get('controller.selectedStack.useRedhatSatellite') === true;
-  }.property('operatingSystems', 'operatingSystems.@each.isSelected', 'controller.selectedStack.useRedhatSatellite'),
+  /**
+   * Tooltip for Add OS Button
+   * Empty if this button is enabled
+   *
+   * @type {string}
+   */
+  addOsButtonTooltip: Em.computed.ifThenElse('allOsesSelected', Em.I18n.t('installer.step1.addOs.disabled.tooltip'), ''),
 
+  /**
+   * Tooltip for useRedhatSatellite block
+   * Empty if usage Redhat is enabled
+   *
+   * @type {string}
+   */
+  redhatDisabledTooltip: Em.computed.ifThenElse('controller.selectedStack.usePublicRepo', Em.I18n.t('installer.step1.advancedRepo.useRedhatSatellite.disabled.tooltip'), ''),
   /**
    * List of all repositories under selected stack operatingSystems
+   *
+   * @type {App.Repository[]}
    */
   allRepositories: function () {
-    var selectedStack = this.get('controller.selectedStack');
-    return Em.isNone(selectedStack) ? [] : selectedStack.get('repositories');
-  }.property('controller.selectedStack'),
+    return this.getWithDefault('controller.selectedStack.repositories', []);
+  }.property('controller.selectedStack.repositories.[]'),
 
   /**
    * Verify if some repo has invalid base-url
+   * Ignore if <code>useRedhatSatellite</code> is true for selected stack
+   *
    * @type {bool}
    */
-  invalidFormatUrlExist: Em.computed.someBy('allRepositories', 'invalidFormatError', true),
+  invalidFormatUrlExist: function () {
+    if (this.get('controller.selectedStack.useRedhatSatellite')) {
+      return false;
+    }
+    var allRepositories = this.get('allRepositories');
+    if (!allRepositories) {
+      return false;
+    }
+    return allRepositories.someProperty('invalidFormatError', true);
+  }.property('controller.selectedStack.useRedhatSatellite', 'allRepositories.@each.invalidFormatError'),
+
   /**
    * Verify if some invalid repo-urls exist
    * @type {bool}
    */
-  invalidUrlExist: Em.computed.someBy('allRepositories', 'validation', App.Repository.validation['INVALID']),
+  invalidUrlExist: Em.computed.someBy('allRepositories', 'validation', App.Repository.validation.INVALID),
+
   /**
    * If all repo links are unchecked
    * @type {bool}
    */
-  isNoOsChecked: Em.computed.everyBy('operatingSystems', 'isSelected', false),
+  isNoOsChecked: Em.computed.everyBy('controller.selectedStack.operatingSystems', 'isSelected', false),
 
   popoverView: Em.View.extend({
     tagName: 'i',
@@ -291,44 +189,47 @@ App.WizardStep1View = Em.View.extend({
     }
   }),
 
+  /**
+   * @type {Em.Checkbox}
+   */
+  redhatCheckBoxView: Em.Checkbox.extend({
+    attributeBindings: [ 'type', 'checked' ],
+    checkedBinding: 'controller.selectedStack.useRedhatSatellite',
+    classNames: ['checkbox'],
+    disabledBinding: 'controller.selectedStack.usePublicRepo',
+    click: function () {
+      // click triggered before value is toggled, so if-statement is inverted
+      if (!this.get('controller.selectedStack.useRedhatSatellite')) {
+        App.ModalPopup.show({
+          header: Em.I18n.t('common.important'),
+          secondary: false,
+          bodyClass: Ember.View.extend({
+            template: Ember.Handlebars.compile(Em.I18n.t('installer.step1.advancedRepo.useRedhatSatellite.warning'))
+          })
+        });
+      }
+    }
+  }),
+
   /**
    * Handler when editing any repo BaseUrl
+   *
    * @method editLocalRepository
    */
   editLocalRepository: function () {
     //upload to content
     var repositories = this.get('allRepositories');
+    if (!repositories) {
+      return;
+    }
     repositories.forEach(function (repository) {
-      if (repository.get('lastBaseUrl') != repository.get('baseUrl')) {
+      if (repository.get('lastBaseUrl') !== repository.get('baseUrl')) {
         repository.setProperties({
           lastBaseUrl: repository.get('baseUrl'),
-          validation: App.Repository.validation['PENDING']
+          validation: App.Repository.validation.PENDING
         });
       }
     }, this);
   }.observes('allRepositories.@each.baseUrl')
 
-});
-
-
-App.VersionDefinitionFileUploader = Em.View.extend({
-  template: Em.Handlebars.compile('<input type="file" {{bindAttr disabled="view.disabled"}} />'),
-
-  classNames: ['vdf-input-indentation'],
-
-  change: function (e) {
-    var self = this;
-    if (e.target.files && e.target.files.length == 1) {
-      var file = e.target.files[0];
-      var reader = new FileReader();
-
-      reader.onload = (function () {
-        return function (e) {
-          self.get("controller").setVDFFile(e.target.result);
-        };
-      })(file);
-      reader.readAsText(file);
-    }
-  }
-
 });

+ 5 - 3
ambari-web/karma.conf.js

@@ -78,6 +78,8 @@ module.exports = function(config) {
       'vendor/scripts/jquery.flexibleArea.js',
       'vendor/scripts/FileSaver.js',
       'vendor/scripts/Blob.js',
+      'vendor/scripts/moment.js',
+      'vendor/scripts/moment-timezone-with-data-2010-2020.js',
       'vendor/**/*.js',
       'app/templates/**/*.hbs',
       'app!(assets)/**/!(karma_setup|tests).js',
@@ -108,7 +110,7 @@ module.exports = function(config) {
     },
 
     preprocessors: {
-//      '!(vendor|node_modules|test)/**/!(karma_setup|tests).js': 'coverage',
+      '!(vendor|node_modules|test)/**/!(karma_setup|tests).js': 'coverage',
       'app/templates/**/*.hbs': ['ember-precompiler-brunch', 'common-require'],
       'app!(assets)/**/!(karma_setup|tests).js': ['common-require'],
       'test/**/*.js': ['common-require']
@@ -123,7 +125,7 @@ module.exports = function(config) {
     // test results reporter to use
     // possible values: 'dots', 'progress', 'junit', 'growl', 'coverage'
 //    reporters: ['progress', 'coverage'],
-    reporters: ['progress'],
+    reporters: ['progress', 'coverage'],
 
 
     // web server port
@@ -160,6 +162,6 @@ module.exports = function(config) {
 
     // Continuous Integration mode
     // if true, it capture browsers, run tests and exit
-    singleRun: false
+    singleRun: true
   });
 };

+ 19 - 0
ambari-web/test/controllers/installer_test.js

@@ -1210,4 +1210,23 @@ describe('App.InstallerController', function () {
     });
   });
 
+  describe('#postVersionDefinitionFileErrorCallback', function () {
+
+    beforeEach(function () {
+      sinon.stub(App, 'showAlertPopup', Em.K);
+    });
+
+    afterEach(function () {
+      App.showAlertPopup.restore();
+    });
+
+    it('should delete VDF-data', function () {
+      App.db.setLocalRepoVDFData({});
+      expect(App.db.getLocalRepoVDFData()).to.not.be.an.object;
+      installerController.postVersionDefinitionFileErrorCallback({}, {}, {}, {}, {dfd: $.Deferred()});
+      expect(App.db.getLocalRepoVDFData()).to.be.undefined;
+    });
+
+  });
+
 });

+ 212 - 0
ambari-web/test/controllers/wizard/step1_test.js

@@ -0,0 +1,212 @@
+/**
+ * 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.
+ */
+
+var App = require('app');
+
+var wizardStep1Controller;
+
+var stacks = [
+  App.Stack.createRecord({
+    "id": "HDP-2.4",
+    "stackName": "HDP",
+    "stackVersion": "2.4"
+  }),
+  App.Stack.createRecord({
+    "id": "HDP-2.5-2.5.0.0",
+    "stackName": "HDP",
+    "stackVersion": "2.5"
+  }),
+  App.Stack.createRecord({
+    "id": "HDP-2.5",
+    "stackName": "HDP",
+    "stackVersion": "2.5"
+  }),
+  App.Stack.createRecord({
+    "id": "HDP-2.3.ECS",
+    "stackName": "HDP",
+    "stackVersion": "2.3.ECS"
+  }),
+  App.Stack.createRecord({
+    "id": "HDP-2.3",
+    "stackName": "HDP",
+    "stackVersion": "2.3"
+  }),
+  App.Stack.createRecord({
+    "id": "HDP-2.2",
+    "stackName": "HDP",
+    "stackVersion": "2.2"
+  }),
+  App.Stack.createRecord({
+    "id": "HDP-2.4-2.4.1.1-12345",
+    "stackName": "HDP",
+    "stackVersion": "2.4"
+  })
+];
+
+function getController() {
+  return App.WizardStep1Controller.create({content: Em.Object.create({stacks: stacks})});
+}
+
+describe('App.WizardStep1Controller', function () {
+
+  beforeEach(function() {
+    wizardStep1Controller = getController();
+  });
+
+  App.TestAliases.testAsComputedFindBy(getController(), 'selectedStack', 'content.stacks', 'isSelected', true);
+
+  App.TestAliases.testAsComputedFindBy(getController(), 'selectedStackType', 'availableStackTypes', 'isSelected', true);
+
+  App.TestAliases.testAsComputedFilterBy(getController(), 'servicesForSelectedStack', 'selectedStack.stackServices', 'isHidden', false);
+
+  App.TestAliases.testAsComputedEveryBy(getController(), 'networkIssuesExist', 'content.stacks', 'stackDefault', true);
+
+  describe('#usePublicRepo', function () {
+
+    beforeEach(function () {
+      wizardStep1Controller.get('content.stacks').findProperty('id', 'HDP-2.5-2.5.0.0').setProperties({
+        isSelected: true,
+        useRedhatSatellite: true,
+        usePublicRepo: false,
+        useLocalRepo: true,
+      });
+      wizardStep1Controller.usePublicRepo();
+    });
+
+    it('correct stack is selected', function () {
+      expect(wizardStep1Controller.get('selectedStack.id')).to.be.equal('HDP-2.5-2.5.0.0');
+    });
+
+    it('`useRedhatSatellite` is set `false`', function () {
+      expect(wizardStep1Controller.get('selectedStack.useRedhatSatellite')).to.be.false;
+    });
+
+    it('`usePublicRepo` is set `true`', function () {
+      expect(wizardStep1Controller.get('selectedStack.usePublicRepo')).to.be.true;
+    });
+
+    it('`useLocalRepo` is set `false`', function () {
+      expect(wizardStep1Controller.get('selectedStack.useLocalRepo')).to.be.false;
+    });
+
+  });
+
+  describe('#useLocalRepo', function () {
+
+    beforeEach(function () {
+      wizardStep1Controller.get('content.stacks').findProperty('id', 'HDP-2.5-2.5.0.0').setProperties({
+        isSelected: true,
+        usePublicRepo: true,
+        useLocalRepo: false,
+      });
+      wizardStep1Controller.useLocalRepo();
+    });
+
+    it('correct stack is selected', function () {
+      expect(wizardStep1Controller.get('selectedStack.id')).to.be.equal('HDP-2.5-2.5.0.0');
+    });
+
+    it('`usePublicRepo` is set `false`', function () {
+      expect(wizardStep1Controller.get('selectedStack.usePublicRepo')).to.be.false;
+    });
+
+    it('`useLocalRepo` is set `true`', function () {
+      expect(wizardStep1Controller.get('selectedStack.useLocalRepo')).to.be.true;
+    });
+
+  });
+
+  describe('#selectStackBy', function () {
+
+    it('select by `id`', function () {
+      wizardStep1Controller.selectStackBy('id', 'HDP-2.5-2.5.0.0');
+      expect(wizardStep1Controller.get('selectedStack.id')).to.be.equal('HDP-2.5-2.5.0.0');
+      expect(wizardStep1Controller.get('content.stacks').filterProperty('isSelected')).to.have.property('length').equal(1);
+    });
+
+    it('select by `stackNameVersion`', function () {
+      wizardStep1Controller.selectStackBy('stackNameVersion', 'HDP-2.5');
+      expect(wizardStep1Controller.get('selectedStack.id')).to.be.equal('HDP-2.5-2.5.0.0'); // `HDP-2.5-2.5.0.0`-id is before `HDP-2.5`-id
+      expect(wizardStep1Controller.get('content.stacks').filterProperty('isSelected')).to.have.property('length').equal(1);
+    });
+
+  });
+
+  describe('#availableStackTypes', function () {
+
+    it('stack types are sorted desc', function () {
+      expect(wizardStep1Controller.get('availableStackTypes').mapProperty('stackName')).to.be.eql(['HDP-2.5', 'HDP-2.4', 'HDP-2.3.ECS', 'HDP-2.3', 'HDP-2.2']);
+    });
+
+  });
+
+  describe('#readInfoIsNotProvided', function () {
+
+    Em.A([
+      {
+        options: {
+          uploadFile: {isSelected: false},
+          enterUrl: {isSelected: false}
+        },
+        m: 'url and file are not selected',
+        e: false
+      },
+      {
+        options: {
+          uploadFile: {isSelected: false},
+          enterUrl: {isSelected: true, url: ''}
+        },
+        m: 'url is selected but not provided',
+        e: true
+      },
+      {
+        options: {
+          uploadFile: {isSelected: false},
+          enterUrl: {isSelected: true, url: ' url'}
+        },
+        m: 'url is selected and provided',
+        e: false
+      },
+      {
+        options: {
+          uploadFile: {isSelected: true, file: ''},
+          enterUrl: {isSelected: false}
+        },
+        m: 'file is selected but not provided',
+        e: true
+      },
+      {
+        options: {
+          uploadFile: {isSelected: true, file: 'path'},
+          enterUrl: {isSelected: false}
+        },
+        m: 'file is selected and provided',
+        e: false
+      }
+    ]).forEach(function (test) {
+
+      it(test.m, function () {
+        wizardStep1Controller.set('optionsToSelect.useLocalRepo', test.options);
+        expect(wizardStep1Controller.get('readInfoIsNotProvided')).to.be.equal(test.e);
+      });
+
+    });
+
+  });
+
+});

+ 2 - 2
ambari-web/test/mixins/common/configs/config_recommendation_parser_test.js

@@ -508,13 +508,13 @@ describe('App.ConfigRecommendationParser', function() {
     it('add new file name', function() {
       instanceObject.set('modifiedFileNames', ['someFile']);
       instanceObject.addModifiedFileName('otherFile');
-      expect(instanceObject.get('modifiedFileNames').join(',')).to.eql('someFile,otherFile');
+      expect(instanceObject.get('modifiedFileNames').join(',')).to.be.equal('someFile,otherFile');
     });
 
     it('do not add file that already in list', function() {
       instanceObject.set('modifiedFileNames', ['someFile']);
       instanceObject.addModifiedFileName('someFile');
-      expect(instanceObject.get('modifiedFileNames').join(',')).to.eql('someFile');
+      expect(instanceObject.get('modifiedFileNames').join(',')).to.be.equal('someFile');
     });
   });
 });

+ 43 - 0
ambari-web/test/utils/array_utils_test.js

@@ -122,4 +122,47 @@ describe('array_utils', function () {
 
   });
 
+  describe('#sortByIdAsVersion', function () {
+
+    Em.A([
+      {
+        c: [{id: '1.2.4'}, {id: '1.2.5'}, {id: '1.2.3'}],
+        m: 'Items without letters and with same length',
+        e: ['1.2.3', '1.2.4', '1.2.5']
+      },
+      {
+        c: [{id: 'HDP-1.2.4'}, {id: 'HDP-1.2.5'}, {id: 'HDP-1.2.3'}],
+        m: 'Items with letters and with same length',
+        e: ['HDP-1.2.3', 'HDP-1.2.4', 'HDP-1.2.5']
+      },
+      {
+        c: [{id: 'HDP-1.2.4.2'}, {id: 'HDP-1.2.4.1'}, {id: 'HDP-1.2.3'}],
+        m: 'Items with letters and with custom length',
+        e: ['HDP-1.2.3', 'HDP-1.2.4.1', 'HDP-1.2.4.2']
+      },
+      {
+        c: [{id: 'HDP-1.2.4.2.3'}, {id: 'HDP-1.2.4.11.3'}, {id: 'HDP-1.2.3'}],
+        m: 'Items with letters and with double digits',
+        e: ['HDP-1.2.3', 'HDP-1.2.4.2.3', 'HDP-1.2.4.11.3']
+      },
+      {
+        c: [{id: 'HDP-1.2.3.2'}, {id: 'HDP-1.2.4'}, {id: 'HDP-1.2.3'}],
+        m: 'Items with letters and with custom length (2)',
+        e: ['HDP-1.2.3', 'HDP-1.2.3.2', 'HDP-1.2.4']
+      },
+      {
+        c: [{id: 'HDP-1.2.3'}, {id: 'HDP-1.2.4'}, {id: 'HDP-1.2.3'}],
+        m: 'Items with letters and equal ids',
+        e: ['HDP-1.2.3', 'HDP-1.2.3', 'HDP-1.2.4']
+      }
+    ]).forEach(function (test) {
+
+      it(test.m, function () {
+        expect(test.c.sort(arrayUtils.sortByIdAsVersion).mapProperty('id')).to.be.eql(test.e);
+      });
+
+    });
+
+  });
+
 });

+ 29 - 14
ambari-web/test/views/common/host_progress_popup_body_view_test.js

@@ -221,20 +221,35 @@ describe('App.HostProgressPopupBodyView', function () {
       ];
 
       cases.forEach(function(test) {
-        it(test.m, function() {
-          assert.equal(Em.get(view, 'hostInfoLoaded'), true, 'hostInfoLoaded should be true on init');
-          this.HostModelStub.returns(test.hosts);
-          this.isLogSearchInstalled.returns(test.isLogSearchInstalled);
-          this.logSearchSupported.returns(test.logSearchSupported);
-          if (test.requestFailed) {
-            this.updateCtrlStub.returns($.Deferred().reject().promise());
-          } else {
-            this.updateCtrlStub.returns($.Deferred().resolve().promise());
-          }
-          Em.set(view, 'hostInfoLoaded', false);
-          view.preloadHostModel(test.hostName);
-          assert.equal(App.router.get('updateController').updateLogging.called, test.e.updateLoggingCalled, 'updateLogging call validation');
-          assert.equal(Em.get(view, 'hostInfoLoaded'), true, 'in result hostInfoLoaded should be always true');
+        describe(test.m, function() {
+
+          beforeEach(function () {
+            this.HostModelStub.returns(test.hosts);
+            this.isLogSearchInstalled.returns(test.isLogSearchInstalled);
+            this.logSearchSupported.returns(test.logSearchSupported);
+            if (test.requestFailed) {
+              this.updateCtrlStub.returns($.Deferred().reject().promise());
+            } else {
+              this.updateCtrlStub.returns($.Deferred().resolve().promise());
+            }
+          });
+
+          it('hostInfoLoaded should be true on init', function () {
+            expect(Em.get(view, 'hostInfoLoaded')).to.be.true;
+          });
+
+          it('updateLogging call validation', function () {
+            Em.set(view, 'hostInfoLoaded', false);
+            view.preloadHostModel(test.hostName);
+            expect(App.router.get('updateController').updateLogging.called).to.be.equal(test.e.updateLoggingCalled);
+          });
+
+          it('in result hostInfoLoaded should be always true', function () {
+            Em.set(view, 'hostInfoLoaded', false);
+            view.preloadHostModel(test.hostName);
+            expect(Em.get(view, 'hostInfoLoaded')).to.be.true;
+          });
+
         });
       }, this);
     });

+ 4 - 197
ambari-web/test/views/wizard/step1_view_test.js

@@ -20,7 +20,6 @@ var App = require('app');
 require('views/wizard/step1_view');
 
 var view;
-var controller;
 
 function getView() {
   return App.WizardStep1View.create();
@@ -28,208 +27,16 @@ function getView() {
 
 describe('App.WizardStep1View', function () {
 
-  describe('#operatingSystems', function () {
-    beforeEach(function () {
-      sinon.stub(App.Stack, 'find', function () {
-        return [
-          Ember.Object.create({
-            id: 'HDP-1.3-1234',
-            stackName: 'HDP',
-            stackVersion: '1.3',
-            active: true,
-            operatingSystems: [
-              Ember.Object.create({
-                id: 'HDP-1.3-1234-redhat5',
-                osType: 'redhat5',
-                isSelected: false,
-                repositories: [
-                  Ember.Object.create({
-                    id: 'redhat5-HDP-1.3',
-                    isSelected: false
-                  }),
-                  Ember.Object.create({
-                    id: 'redhat5-HDP-UTILS-1.1.0.19',
-                    isSelected: false
-                  })
-                ]
-              }),
-              Ember.Object.create({
-                id: 'HDP-1.3-1234-redhat6',
-                osType: 'redhat6',
-                isSelected: false,
-                repositories: [
-                  Ember.Object.create({
-                    id: 'redhat6-HDP-1.3',
-                    isSelected: false
-                  }),
-                  Ember.Object.create({
-                    id: 'redhat6-HDP-UTILS-1.1.0.19',
-                    isSelected: false
-                  })
-                ]
-              })
-            ],
-            isSelected: false
-          }),
-          Ember.Object.create({
-            id: 'HDP-2.1',
-            stackName: 'HDP',
-            stackVersion: '2.1',
-            active: true,
-            operatingSystems: [
-              Ember.Object.create({
-                id: 'HDP-2.1-redhat5',
-                osType: 'redhat5',
-                isSelected: true,
-                repositories: [
-                  Ember.Object.create({
-                    id: 'redhat5-HDP-2.1',
-                    isSelected: true,
-                    baseUrl: "http://public-repo-1.hortonworks.com/HDP/centos5/2.x/updates/2.1.5.0",
-                    latestBaseUrl: "http://public-repo-1.hortonworks.com/HDP/centos5/2.x/updates/2.1.5.0"
-                  }),
-                  Ember.Object.create({
-                    id: 'redhat5-HDP-UTILS-1.1.0.19',
-                    isSelected: true,
-                    baseUrl: "http://s3.amazonaws.com/dev.hortonworks.com/HDP-UTILS-1.1.0.19/repos/centos5",
-                    latestBaseUrl: "http://s3.amazonaws.com/dev.hortonworks.com/HDP-UTILS-1.1.0.19/repos/centos5"
-                  })
-                ]
-              }),
-              Ember.Object.create({
-                id: 'HDP-2.1-redhat6',
-                osType: 'redhat6',
-                isSelected: true,
-                repositories: [
-                  Ember.Object.create({
-                    id: 'redhat6-HDP-2.1',
-                    isSelected: true,
-                    baseUrl: "http://public-repo-1.hortonworks.com/HDP/centos6/2.x/updates/2.1.5.0",
-                    latestBaseUrl: "http://public-repo-1.hortonworks.com/HDP/centos6/2.x/updates/2.1.5.0"
-                  }),
-                  Ember.Object.create({
-                    id: 'redhat6-HDP-UTILS-1.1.0.19',
-                    isSelected: true,
-                    baseUrl: "http://s3.amazonaws.com/dev.hortonworks.com/HDP-UTILS-1.1.0.19/repos/centos6",
-                    latestBaseUrl: "http://s3.amazonaws.com/dev.hortonworks.com/HDP-UTILS-1.1.0.19/repos/centos6"
-                  })
-                ]
-              })
-            ],
-            repositories: [
-              Ember.Object.create({
-                id: 'redhat5-HDP-2.1',
-                isSelected: true,
-                baseUrl: "http://public-repo-1.hortonworks.com/HDP/centos5/2.x/updates/2.1.5.0",
-                latestBaseUrl: "http://public-repo-1.hortonworks.com/HDP/centos5/2.x/updates/2.1.5.0"
-              }),
-              Ember.Object.create({
-                id: 'redhat5-HDP-UTILS-1.1.0.19',
-                isSelected: true,
-                baseUrl: "http://s3.amazonaws.com/dev.hortonworks.com/HDP-UTILS-1.1.0.19/repos/centos5",
-                latestBaseUrl: "http://s3.amazonaws.com/dev.hortonworks.com/HDP-UTILS-1.1.0.19/repos/centos5"
-              }),
-              Ember.Object.create({
-                id: 'redhat6-HDP-2.1',
-                isSelected: true,
-                baseUrl: "http://public-repo-1.hortonworks.com/HDP/centos6/2.x/updates/2.1.5.0",
-                latestBaseUrl: "http://public-repo-1.hortonworks.com/HDP/centos6/2.x/updates/2.1.5.0"
-              }),
-              Ember.Object.create({
-                id: 'redhat6-HDP-UTILS-1.1.0.19',
-                isSelected: true,
-                baseUrl: "http://s3.amazonaws.com/dev.hortonworks.com/HDP-UTILS-1.1.0.19/repos/centos6",
-                latestBaseUrl: "http://s3.amazonaws.com/dev.hortonworks.com/HDP-UTILS-1.1.0.19/repos/centos6"
-              })
-            ],
-            isSelected: true
-          })
-        ];
-      });
-    });
-
-    afterEach(function () {
-      App.Stack.find.restore();
-    });
-
-    describe('should create repo groups from repo list', function () {
-
-      var repositories;
-      beforeEach(function () {
-        controller = App.WizardStep1Controller.create({
-          content: {
-            stacks: App.Stack.find()
-          }
-        });
-
-        view = App.WizardStep1View.create({'controller': controller});
-        view.set('$', function () {
-          return Em.Object.create({hide: Em.K, toggle: Em.K});
-        });
-        repositories = view.get('allRepositories');
-      });
-
-      it('operatingSystems.length', function () {
-        expect(view.get('operatingSystems.length')).to.equal(2);
-      });
-
-      it('operatingSystems.0.osType', function () {
-        expect(view.get('operatingSystems')[0].get('osType')).to.equal('redhat5');
-      });
-
-      it('operatingSystems.1.osType', function () {
-        expect(view.get('operatingSystems')[1].get('osType')).to.equal('redhat6');
-      });
-
-      it('operatingSystems.0.isSelected', function () {
-        expect(view.get('operatingSystems')[0].get('isSelected')).to.be.true;
-      });
-
-      it('operatingSystems.1.isSelected', function () {
-        expect(view.get('operatingSystems')[1].get('isSelected')).to.be.true;
-      });
-
-      it('operatingSystems.0.repositories', function () {
-        expect(view.get('operatingSystems')[0].get('repositories')).to.eql([repositories[0], repositories[1]]);
-      });
-
-      it('operatingSystems.1.repositories', function () {
-        expect(view.get('operatingSystems')[1].get('repositories')).to.eql([repositories[2], repositories[3]]);
-      });
-
-    });
-
-  });
-
-  describe('#invalidFormatUrlExist', function () {
-
-    controller = App.WizardStep1Controller.create({
-      content: {
-        stacks: App.Stack.find()
-      }
-    });
-    view = App.WizardStep1View.create();
-    view.reopen({
-      controller: controller
-    });
-    view.set('$', function () {
-      return Em.Object.create({hide: Em.K, toggle: Em.K});
-    });
-
-    it(view.get('allRepositories').mapProperty('invalidFormatError').join(', '), function () {
-      expect(view.get('invalidFormatUrlExist')).to.equal(false);
-    });
+  beforeEach(function () {
+    view = getView();
   });
 
-  App.TestAliases.testAsComputedEveryBy(getView(), 'isNoOsChecked', 'operatingSystems', 'isSelected', false);
+  App.TestAliases.testAsComputedEveryBy(getView(), 'isNoOsChecked', 'controller.selectedStack.operatingSystems', 'isSelected', false);
 
-  App.TestAliases.testAsComputedOr(getView(), 'isSubmitDisabled', ['controller.content.isCheckInProgress']);
+  App.TestAliases.testAsComputedOr(getView(), 'isSubmitDisabled', ['invalidFormatUrlExist', 'isNoOsChecked', 'controller.content.isCheckInProgress']);
 
   App.TestAliases.testAsComputedSomeBy(getView(), 'invalidUrlExist', 'allRepositories', 'validation', App.Repository.validation.INVALID);
 
-  App.TestAliases.testAsComputedSomeBy(getView(), 'invalidFormatUrlExist', 'allRepositories', 'invalidFormatError', true);
-
-
   describe('#editLocalRepository', function () {
 
     it('should update repository', function () {