ambari-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From jonathanhur...@apache.org
Subject ambari git commit: AMBARI-21933 - UI: Implement breadcrumbs in Background Operations modal (Jason Golieb via jonathanhurley)
Date Thu, 14 Sep 2017 14:32:16 GMT
Repository: ambari
Updated Branches:
  refs/heads/trunk 03edb8e78 -> 38604db2f


AMBARI-21933 - UI: Implement breadcrumbs in Background Operations modal (Jason Golieb via jonathanhurley)


Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/38604db2
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/38604db2
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/38604db2

Branch: refs/heads/trunk
Commit: 38604db2f17eb7eac3b4fe5bf308842d5018ee00
Parents: 03edb8e
Author: Jonathan Hurley <jhurley@hortonworks.com>
Authored: Thu Sep 14 10:23:32 2017 -0400
Committer: Jonathan Hurley <jhurley@hortonworks.com>
Committed: Thu Sep 14 10:23:32 2017 -0400

----------------------------------------------------------------------
 .../global/background_operations_controller.js  |   9 +-
 .../progress_popup_controller.js                |   8 +-
 ambari-web/app/messages.js                      |   2 +
 ambari-web/app/styles/modal_popups.less         |  11 +
 ambari-web/app/templates/common/breadcrumbs.hbs |   6 +-
 .../templates/common/host_progress_popup.hbs    |  27 +-
 ambari-web/app/utils/host_progress_popup.js     | 121 +++-
 ambari-web/app/views/common/breadcrumbs_view.js |  24 +-
 .../common/host_progress_popup_body_view.js     | 316 ++++++---
 .../global/background_operations_test.js        |   4 +-
 .../progress_popup_controller_test.js           |   4 +
 .../test/utils/host_progress_popup_test.js      |   7 +-
 .../test/views/common/breadcrumbs_view_test.js  |  30 +-
 .../host_progress_popup_body_view_test.js       | 641 ++++++++++++-------
 14 files changed, 805 insertions(+), 405 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/38604db2/ambari-web/app/controllers/global/background_operations_controller.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/controllers/global/background_operations_controller.js b/ambari-web/app/controllers/global/background_operations_controller.js
index de420b5..7cf4d72 100644
--- a/ambari-web/app/controllers/global/background_operations_controller.js
+++ b/ambari-web/app/controllers/global/background_operations_controller.js
@@ -40,13 +40,13 @@ App.BackgroundOperationsController = Em.Controller.extend({
   operationsCount: 10,
   /**
    * Possible levels:
-   * REQUESTS_LIST
+   * OPS_LIST
    * HOSTS_LIST
    * TASKS_LIST
    * TASK_DETAILS
    */
   levelInfo: Em.Object.create({
-    name: 'REQUESTS_LIST',
+    name: "OPS_LIST",
     requestId: null,
     taskId: null
   }),
@@ -258,7 +258,7 @@ App.BackgroundOperationsController = Em.Controller.extend({
     }
     this.removeOldRequests(currentRequestIds);
     this.set("allOperationsCount", runningServices);
-    this.set('isShowMoreAvailable', countGot >= countIssued);
+    this.set('isShowMoreAvailable', countGot > countIssued);
     this.set('serviceTimestamp', App.dateTimeWithTimeZone());
   },
 
@@ -368,6 +368,9 @@ App.BackgroundOperationsController = Em.Controller.extend({
     var self = this;
     App.router.get('userSettingsController').dataLoading('show_bg').done(function (initValue) {
       App.updater.immediateRun('requestMostRecent');
+
+      App.HostPopup.set("breadcrumbs", [ App.HostPopup.get("rootBreadcrumb") ]);
+
       if (self.get('popupView') && App.HostPopup.get('isBackgroundOperations')) {
         self.set('popupView.isNotShowBgChecked', !initValue);
         self.set('popupView.isOpen', true);

http://git-wip-us.apache.org/repos/asf/ambari/blob/38604db2/ambari-web/app/controllers/main/admin/highAvailability/progress_popup_controller.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/controllers/main/admin/highAvailability/progress_popup_controller.js b/ambari-web/app/controllers/main/admin/highAvailability/progress_popup_controller.js
index 4cbe59b..6e0ee5e 100644
--- a/ambari-web/app/controllers/main/admin/highAvailability/progress_popup_controller.js
+++ b/ambari-web/app/controllers/main/admin/highAvailability/progress_popup_controller.js
@@ -130,7 +130,7 @@ App.HighAvailabilityProgressPopupController = Ember.Controller.extend({
       })
     }, this);
   },
-  
+
   doPolling: function () {
     var self = this;
     this.set('progressController.logs', []);
@@ -158,7 +158,7 @@ App.HighAvailabilityProgressPopupController = Ember.Controller.extend({
     if (this.get('requestIds.length') === this.get('hostsData.length')) {
       var popupTitle = this.get('popupTitle');
       this.calculateHostsData(hostsData);
-      App.HostPopup.initPopup(popupTitle, this);
+      App.HostPopup.initPopup(popupTitle, this, false, this.get("requestIds")[0]);
       if (this.isRequestRunning(hostsData)) {
         if (this.get('progressController.name') === 'mainAdminStackAndUpgradeController') {
           this.doPolling();
@@ -180,6 +180,7 @@ App.HighAvailabilityProgressPopupController = Ember.Controller.extend({
     var hosts = [];
     var hostsMap = {};
     var popupTitle = this.get('popupTitle');
+    var id = data[0].Requests.id;
 
     data.forEach(function (request) {
       request.tasks.forEach(function (task) {
@@ -199,7 +200,7 @@ App.HighAvailabilityProgressPopupController = Ember.Controller.extend({
       hosts.push(hostsMap[host]);
     }
     this.set('services', [
-      {name: popupTitle, hosts: hosts}
+      {id: id, name: popupTitle, hosts: hosts}
     ]);
     this.set('serviceTimestamp', App.dateTime());
     if (!this.isRequestRunning(data)) {
@@ -293,4 +294,3 @@ App.HighAvailabilityProgressPopupController = Ember.Controller.extend({
   }
 
 });
-

http://git-wip-us.apache.org/repos/asf/ambari/blob/38604db2/ambari-web/app/messages.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/messages.js b/ambari-web/app/messages.js
index 869608a..568e537 100644
--- a/ambari-web/app/messages.js
+++ b/ambari-web/app/messages.js
@@ -127,6 +127,7 @@ Em.I18n.translations = {
   'common.generate.blueprint':'Generate Blueprint',
   'common.message':'Message',
   'common.tasks':'Tasks',
+  'common.taskLog':'Task Log',
   'common.open':'Open',
   'common.copy':'Copy',
   'common.complete':'Complete',
@@ -216,6 +217,7 @@ Em.I18n.translations = {
   'common.notAvailable': 'Not Available',
   'common.na': 'n/a',
   'common.operations': 'Operations',
+  'common.backgroundOperations': 'Background Operations',
   'common.startTime': 'Start Time',
   'common.duration': 'Duration',
   'common.reinstall': 'Re-Install',

http://git-wip-us.apache.org/repos/asf/ambari/blob/38604db2/ambari-web/app/styles/modal_popups.less
----------------------------------------------------------------------
diff --git a/ambari-web/app/styles/modal_popups.less b/ambari-web/app/styles/modal_popups.less
index f7fe88b..97083d2 100644
--- a/ambari-web/app/styles/modal_popups.less
+++ b/ambari-web/app/styles/modal_popups.less
@@ -21,6 +21,17 @@
   outline: none;
 }
 
+#modal-label {
+  margin-left: 20px;
+  margin-right: 20px;
+  text-indent: -20px;
+  line-height: 1.3em;
+
+  a:not(.disabled) {
+      cursor: pointer;
+  }
+}
+
 .host-component-popup-wrap {
   .task-top-wrap {
     .operation-name-top {

http://git-wip-us.apache.org/repos/asf/ambari/blob/38604db2/ambari-web/app/templates/common/breadcrumbs.hbs
----------------------------------------------------------------------
diff --git a/ambari-web/app/templates/common/breadcrumbs.hbs b/ambari-web/app/templates/common/breadcrumbs.hbs
index 89353d9..363b306 100644
--- a/ambari-web/app/templates/common/breadcrumbs.hbs
+++ b/ambari-web/app/templates/common/breadcrumbs.hbs
@@ -17,8 +17,12 @@
 }}
 
 {{#each item in view.items}}
+  {{#if item.itemView}}
+  {{view item.itemView}}
+  {{else}}
   <a {{bindAttr class="item.disabled:disabled"}} {{action moveTo item target="view"}}>
     {{{item.formattedLabel}}}
   </a>
+  {{/if}}
   {{#unless item.isLast}}&nbsp;/&nbsp;{{/unless}}
-{{/each}}
\ No newline at end of file
+{{/each}}

http://git-wip-us.apache.org/repos/asf/ambari/blob/38604db2/ambari-web/app/templates/common/host_progress_popup.hbs
----------------------------------------------------------------------
diff --git a/ambari-web/app/templates/common/host_progress_popup.hbs b/ambari-web/app/templates/common/host_progress_popup.hbs
index 03c014c..f5b1c73 100644
--- a/ambari-web/app/templates/common/host_progress_popup.hbs
+++ b/ambari-web/app/templates/common/host_progress_popup.hbs
@@ -19,11 +19,12 @@
 <div class="host-component-popup-wrap col-sm-12">
 
 {{#if view.parentView.isLoaded}}
-{{!-- SERVICES --}}
+{{!-- OPERATIONS --}}
 
   <div {{bindAttr class="view.parentView.isServiceListHidden:hidden :task-list-main-wrap :table-body-wrap"}}>
     <div class="row top-wrap">
-      <div class="table-controls row col-sm-12 pull-right">
+      <h2 class="table-title col-sm-6">{{view view.parentView.titleClass}}</h2>
+      <div class="table-controls row col-sm-6 pull-right">
         <div class="col-sm-12">
           <div class="btn-group pull-right">
             {{view Ember.Select
@@ -53,7 +54,7 @@
         <table class="table table-hover">
           <tbody>
             {{#each servicesInfo in view.services}}
-              <tr {{action gotoHosts servicesInfo}} {{bindAttr class="servicesInfo.isVisible::hidden :pointer"}}>
+              <tr {{action onOpClick servicesInfo}} {{bindAttr class="servicesInfo.isVisible::hidden :pointer"}}>
                 <td class="col-sm-3">
                   {{view statusIcon servicesInfoBinding="servicesInfo"}}
                   <a href="#">
@@ -116,11 +117,6 @@
               classNames="form-control"
             }}
           </div>
-          {{#if controller.isBackgroundOperations}}
-            <button type="button" class="btn btn-link pull-right" {{action backToServiceList}}>
-              <i class="glyphicon glyphicon-arrow-left"></i>&nbsp;{{t common.operations}}
-            </button>
-          {{/if}}
         </div>
       </div>
     </div>
@@ -148,7 +144,7 @@
           <table class="table table-hover">
             <tbody>
               {{#each hostInfo in view.pageContent}}
-                <tr {{action gotoTasks hostInfo}} {{bindAttr class="hostInfo.isVisible::hidden :pointer"}}>
+                <tr {{action onHostClick hostInfo}} {{bindAttr class="hostInfo.isVisible::hidden :pointer"}}>
                   <td class="col-sm-6 text-nowrap">
                     {{view statusIcon servicesInfoBinding="hostInfo"}}
                     <a href="#">
@@ -206,9 +202,6 @@
               classNames="form-control"
             }}
           </div>
-          <button type="button" class="btn btn-link pull-right" {{action backToHostList}}>
-            <i class="glyphicon glyphicon-arrow-left"></i>&nbsp;{{t common.hosts}}
-          </button>
         </div>
       </div>
     </div>
@@ -220,7 +213,7 @@
           <table class="table table-hover">
             <tbody>
               {{#each taskInfo in view.tasks}}
-                <tr {{action toggleTaskLog taskInfo}} {{bindAttr class="taskInfo.isVisible::hidden :pointer"}}>
+                <tr {{action onTaskClick taskInfo}} {{bindAttr class="taskInfo.isVisible::hidden :pointer"}}>
                   <td class="col-sm-3">
                     {{view statusIcon servicesInfoBinding="taskInfo"}}
                     <a href="#">
@@ -258,10 +251,7 @@
   <div {{bindAttr class="view.parentView.isLogWrapHidden:hidden :task-detail-info view.hostComponentLogsExists:task-detail-info-tabbed"}}>
     <div class="task-top-wrap top-wrap">
       <div {{bindAttr class="view.hostComponentLogsExists:task-detail-log-nav-actions :row"}}>
-        <h2 class="table-title col-sm-5">
-          <i {{bindAttr class="view.openedTask.status :task-detail-status-ico view.openedTask.icon"}}></i>
-          {{view.openedTask.commandDetail}}
-        </h2>
+        <h2 class="table-title col-sm-5">{{t common.taskLog}}</h2>
         <div class="table-controls row col-sm-7 pull-right">
           <div class="col-sm-12">
             {{#if App.supports.logSearch}}
@@ -279,9 +269,6 @@
             <button type="button" class="btn btn-link pull-right copy-clipboard" {{translateAttr title="common.fullLogPopup.clickToCopy"}} {{action "textTrigger" taskInfo target="view"}}>
               <i class="glyphicon glyphicon-copy"></i>&nbsp;{{t common.copy}}
             </button>
-            <button type="button" class="btn btn-link pull-right" {{action backToTaskList}}>
-              <i class="glyphicon glyphicon-arrow-left"></i>&nbsp;{{t common.tasks}}
-            </button>
           </div>
         </div>
       </div>

http://git-wip-us.apache.org/repos/asf/ambari/blob/38604db2/ambari-web/app/utils/host_progress_popup.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/utils/host_progress_popup.js b/ambari-web/app/utils/host_progress_popup.js
index 5bd02cd..edba87b 100644
--- a/ambari-web/app/utils/host_progress_popup.js
+++ b/ambari-web/app/utils/host_progress_popup.js
@@ -120,6 +120,14 @@ App.HostPopup = Em.Object.create({
    */
   popupHeaderName: '',
 
+  //This is what the breadcrumbs will be reset to every time the modal is opened.
+  rootBreadcrumb: null,
+
+  /**
+   * @type {object[]}
+   */
+  breadcrumbs: [],
+
   operationInfo: null,
 
   /**
@@ -293,17 +301,21 @@ App.HostPopup = Em.Object.create({
    */
   initPopup: function (serviceName, controller, isBackgroundOperations, requestId) {
     if (App.get('isClusterUser')) return;
+
     if (!isBackgroundOperations) {
       this.clearHostPopup();
-      this.set("popupHeaderName", serviceName);
+      this.set("rootBreadcrumb", { label: serviceName });
+    } else {
+      this.set("rootBreadcrumb", { label: Em.I18n.t("common.backgroundOperations") });
     }
 
     this.setProperties({
+      breadcrumbs: [ this.get("rootBreadcrumb") ],
       currentServiceId: requestId,
       serviceName: serviceName,
       dataSourceController: controller,
       isBackgroundOperations: isBackgroundOperations,
-      inputData: this.get("dataSourceController.services")
+      inputData: controller.get("services")
     });
 
     if (isBackgroundOperations) {
@@ -311,6 +323,7 @@ App.HostPopup = Em.Object.create({
     } else {
       this.onHostUpdate();
     }
+
     return this.createPopup();
   },
 
@@ -438,10 +451,12 @@ App.HostPopup = Em.Object.create({
    * @method onServiceUpdate
    */
   onServiceUpdate: function (isServiceListHidden) {
-    if (this.get('isBackgroundOperations') && this.get("inputData")) {
-      var servicesInfo = this.get("servicesInfo");
-      var currentServices = [];
-      this.get("inputData").forEach(function (service, index) {
+    var servicesInfo = this.get("servicesInfo");
+    var currentServices = [];
+
+    var inputData = this.get("inputData");
+    if (inputData) {
+      inputData.forEach(function (service, index) {
         var updatedService;
         var id = service.id;
         currentServices.push(id);
@@ -456,9 +471,10 @@ App.HostPopup = Em.Object.create({
         }
         updatedService.set('isAbortable', App.isAuthorized('SERVICE.START_STOP') && this.isAbortableByStatus(service.status));
       }, this);
-      this.removeOldServices(servicesInfo, currentServices);
-      this.setBackgroundOperationHeader(isServiceListHidden);
     }
+
+    this.removeOldServices(servicesInfo, currentServices);
+    this.setBackgroundOperationHeader(isServiceListHidden);
   },
 
   /**
@@ -513,8 +529,8 @@ App.HostPopup = Em.Object.create({
       barColor: status[2],
       isInProgress: status[3],
       barWidth: "width:" + newData.progress + "%;",
-      sourceRequestScheduleId: newData.get('sourceRequestScheduleId'),
-      contextCommand: newData.get('contextCommand')
+      sourceRequestScheduleId: newData.get && newData.get('sourceRequestScheduleId'),
+      contextCommand: newData.get && newData.get('contextCommand')
     });
   },
 
@@ -586,8 +602,7 @@ App.HostPopup = Em.Object.create({
 
       if (existedHosts && existedHosts.length && this.get('currentServiceId') === this.get('previousServiceId')) {
         this._processingExistingHostsWithSameService(hostsMap);
-      }
-      else {
+      } else {
         var hostsArr = this._hostMapProcessing(hostsMap);
         hostsArr = hostsArr.sortProperty('name');
         hostsArr.setEach("serviceName", this.get("serviceName"));
@@ -595,7 +610,7 @@ App.HostPopup = Em.Object.create({
         self.set('previousServiceId', this.get('currentServiceId'));
       }
     }
-    var operation = this.get('servicesInfo').findProperty('name', this.get('serviceName'));
+    var operation = this.get('servicesInfo').findProperty('id', this.get('currentServiceId'));
     this.set('operationInfo', !operation || operation && operation.get('progress') === 100 ? null : operation);
   },
 
@@ -610,11 +625,11 @@ App.HostPopup = Em.Object.create({
     var hostsData;
     var hostsMap = {};
     var inputData = this.get('inputData');
+
     if (this.get('isBackgroundOperations') && this.get("currentServiceId")) {
       //hosts popup for Background Operations
       hostsData = inputData.findProperty("id", this.get("currentServiceId"));
-    }
-    else {
+    } else {
       if (this.get("serviceName")) {
         //hosts popup for Wizards
         hostsData = inputData.findProperty("name", this.get("serviceName"));
@@ -625,14 +640,14 @@ App.HostPopup = Em.Object.create({
       if (hostsData.hostsMap) {
         //hosts data come from Background Operations as object map
         hostsMap = hostsData.hostsMap;
-      }
-      else {
+      } else {
         if (hostsData.hosts) {
           //hosts data come from Wizard as array
           hostsMap = hostsData.hosts.toMapByProperty('name');
         }
       }
     }
+
     return hostsMap;
   },
 
@@ -695,8 +710,10 @@ App.HostPopup = Em.Object.create({
     var existedHosts = self.get('hosts');
     var detailedProperties = this.get('detailedProperties');
     var detailedPropertiesKeys = Em.keys(detailedProperties);
+
     existedHosts.forEach(function (host) {
       var newHostInfo = hostsMap[host.get('name')];
+
       //update only hosts with changed tasks or currently opened tasks of host
       if (newHostInfo &&
           (!this.get('isBackgroundOperations') ||
@@ -704,6 +721,7 @@ App.HostPopup = Em.Object.create({
             this.get('currentHostName') === host.get('name'))) {
         var hostStatus = self.getStatus(newHostInfo.logTasks);
         var hostProgress = self.getProgress(newHostInfo.logTasks);
+
         host.setProperties({
           status: App.format.taskStatus(hostStatus[0]),
           icon: hostStatus[1],
@@ -713,27 +731,33 @@ App.HostPopup = Em.Object.create({
           barWidth: "width:" + hostProgress + "%;",
           logTasks: newHostInfo.logTasks
         });
+
         var existTasks = host.get('tasks');
+
         if (existTasks) {
           newHostInfo.logTasks.forEach(function (_task) {
             var existTask = existTasks.findProperty('id', _task.Tasks.id);
+
             if (existTask) {
               var status = _task.Tasks.status;
+
               detailedPropertiesKeys.forEach(function (key) {
                 var name = detailedProperties[key];
                 var value = _task.Tasks[name];
+
                 if (!Em.isNone(value)) {
                   existTask.set(key, value);
                 }
               }, this);
+
               existTask.setProperties({
                 status: App.format.taskStatus(status),
                 startTime: date.startTime(_task.Tasks.start_time),
                 duration: date.durationSummary(_task.Tasks.start_time, _task.Tasks.end_time)
               });
+
               existTask = self._handleRebalanceHDFS(_task, existTask);
-            }
-            else {
+            } else {
               existTasks.pushObject(this.createTask(_task));
             }
           }, this);
@@ -785,26 +809,63 @@ App.HostPopup = Em.Object.create({
     this.set('isPopup', App.ModalPopup.show({
 
       /**
+       * Controls visiblity of Task Details view.
        * @type {boolean}
        */
       isLogWrapHidden: true,
 
       /**
+       * Controls visiblity of Tasks view.
        * @type {boolean}
        */
       isTaskListHidden: true,
 
       /**
+       * Controls visiblity of Hosts view.
        * @type {boolean}
        */
       isHostListHidden: true,
 
       /**
+       * Controls visiblity of Background Operations view.
        * @type {boolean}
        */
       isServiceListHidden: false,
 
       /**
+       * Single function to handle changing the currently displayed view in the modal.
+       * Use this rather than setting the booleans above directly.
+       */
+      switchView: function(to) {
+        switch (to) {
+          case "OPS_LIST":
+            this.set("isLogWrapHidden", true);
+            this.set("isTaskListHidden", true);
+            this.set("isHostListHidden", true);
+            this.set("isServiceListHidden", false);
+            break;
+          case "HOSTS_LIST":
+            this.set("isLogWrapHidden", true);
+            this.set("isTaskListHidden", true);
+            this.set("isHostListHidden", false);
+            this.set("isServiceListHidden", true);
+            break;
+          case "TASKS_LIST":
+            this.set("isLogWrapHidden", true);
+            this.set("isTaskListHidden", false);
+            this.set("isHostListHidden", true);
+            this.set("isServiceListHidden", true);
+            break;
+          case "TASK_DETAILS":
+            this.set("isLogWrapHidden", false);
+            this.set("isTaskListHidden", true);
+            this.set("isHostListHidden", true);
+            this.set("isServiceListHidden", true);
+            break;
+        }
+      },
+
+      /**
        * @type {boolean}
        */
       isHideBodyScroll: true,
@@ -838,7 +899,25 @@ App.HostPopup = Em.Object.create({
       /**
        * @type {Em.View}
        */
-      headerClass: Em.View.extend({
+      headerClass: App.BreadcrumbsView.extend({
+        controller: this,
+        items: function () {
+          let items = this.get('controller.breadcrumbs');
+          items = items.map(item => App.BreadcrumbItem.extend(item).create());
+          if (items.length) {
+            items.get('lastObject').setProperties({
+              disabled: true,
+              isLast: true
+            });
+          }
+          return items;
+        }.property('controller.breadcrumbs')
+      }),
+
+      /**
+       * @type {Em.View}
+       */
+      titleClass: Em.View.extend({
         controller: this,
         template: Em.Handlebars.compile('{{popupHeaderName}} ' +
           '{{#unless view.parentView.isHostListHidden}}{{#if controller.operationInfo.isAbortable}}' +
@@ -888,7 +967,7 @@ App.HostPopup = Em.Object.create({
         this.set('isOpen', false);
         if (isBackgroundOperations) {
           $(this.get('element')).detach();
-          App.router.get('backgroundOperationsController').set('levelInfo.name', 'REQUESTS_LIST');
+          App.router.get('backgroundOperationsController').set('levelInfo.name', 'OPS_LIST');
         } else {
           this.hide();
           self.set('isPopup', null);

http://git-wip-us.apache.org/repos/asf/ambari/blob/38604db2/ambari-web/app/views/common/breadcrumbs_view.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/common/breadcrumbs_view.js b/ambari-web/app/views/common/breadcrumbs_view.js
index e35a196..7c5ec4e 100644
--- a/ambari-web/app/views/common/breadcrumbs_view.js
+++ b/ambari-web/app/views/common/breadcrumbs_view.js
@@ -60,6 +60,14 @@ App.BreadcrumbItem = Em.Object.extend({
   labelBindingPath: '',
 
   /**
+   * View shown as breadcrumb.
+   * If provied, <code>itemView</code> supersedes <code>label</code> and <code>labelBindingPath</code>.
+   *
+   * @type {object}
+   */
+  itemView: null,
+
+  /**
    * Determines if breadcrumb is disabled
    *
    * @type {boolean}
@@ -74,7 +82,16 @@ App.BreadcrumbItem = Em.Object.extend({
   isLast: false,
 
   /**
+   * Invoke this action when click on breadcrumb item
+   * If provided, <code>action</code> supersedes <code>route</code>.
+   *
+   * @type {Function}
+   */
+  action: null,
+
+  /**
    * Move user to this route when click on breadcrumb item (don't add prefix <code>main</code>)
+   * This is used if an action is not defined.
    *
    * @type {string}
    */
@@ -116,7 +133,12 @@ App.BreadcrumbItem = Em.Object.extend({
   },
 
   transition: function () {
-    return App.router.route('main/' + this.get('route'));
+    const action = this.get('action');
+    if (action) {
+      return action();
+    } else {
+      return App.router.route('main/' + this.get('route'));
+    }
   },
 
   /**

http://git-wip-us.apache.org/repos/asf/ambari/blob/38604db2/ambari-web/app/views/common/host_progress_popup_body_view.js
----------------------------------------------------------------------
diff --git a/ambari-web/app/views/common/host_progress_popup_body_view.js b/ambari-web/app/views/common/host_progress_popup_body_view.js
index 057f8bc..49d7dea 100644
--- a/ambari-web/app/views/common/host_progress_popup_body_view.js
+++ b/ambari-web/app/views/common/host_progress_popup_body_view.js
@@ -219,6 +219,13 @@ App.HostProgressPopupBodyView = App.TableView.extend({
   taskCategory: null,
 
   /**
+   * Indicates current level displayed.
+   *
+   * @type {string}
+   */
+  currentLevel: "",
+
+  /**
    * flag to indicate whether level data has already been loaded
    * applied only to HOSTS_LIST and TASK_DETAILS levels, whereas async query used to obtain data
    *
@@ -309,8 +316,11 @@ App.HostProgressPopupBodyView = App.TableView.extend({
    * @method resizeHandler
    */
   resizeHandler: function() {
-    if (this.get('state') === 'destroyed' || !this.get('parentView.isOpen')) return;
-    var modal = this.get('parentView').$().find('.modal'),
+    const parentView = this.get('parentView');
+
+    if (!parentView || !parentView.$ || !parentView.$() || this.get('state') === 'destroyed' || !parentView.get('isOpen')) return;
+
+    var modal = parentView.$().find('.modal'),
         headerHeight = $(modal).find('.modal-header').outerHeight(true),
         modalFooterHeight = $(modal).find('.modal-footer').outerHeight(true),
         taskTopWrapHeight = $(modal).find('.top-wrap:visible').outerHeight(true),
@@ -318,9 +328,9 @@ App.HostProgressPopupBodyView = App.TableView.extend({
         contentPaddingBottom = parseFloat($(modal).find('.modal-dialog').css('marginBottom')) || 0,
         hostsPageBarHeight = $(modal).find('#host-info tfoot').outerHeight(true),
         logComponentFileNameHeight = $(modal).find('#host-info tfoot').outerHeight(true),
-        levelName = this.get('currentLevelName'),
+        levelName = this.get('currentLevel'),
         boLevelHeightMap = {
-          'REQUESTS_LIST': {
+          'OPS_LIST': {
             height: window.innerHeight - 2*modalTopOffset - headerHeight - taskTopWrapHeight - modalFooterHeight - contentPaddingBottom,
             target: '#service-info'
           },
@@ -355,8 +365,6 @@ App.HostProgressPopupBodyView = App.TableView.extend({
     }
   },
 
-  currentLevelName: Em.computed.alias('controller.dataSourceController.levelInfo.name'),
-
   /**
    * Preset values on init
    *
@@ -364,13 +372,12 @@ App.HostProgressPopupBodyView = App.TableView.extend({
    */
   setOnStart: function () {
     this.set('serviceCategory', this.get('categories').findProperty('value', 'all'));
+
     if (this.get("controller.isBackgroundOperations")) {
       this.get('controller').setSelectCount(this.get("services"), this.get('categories'));
       this.updateHostInfo();
-    }
-    else {
-      this.set("parentView.isHostListHidden", false);
-      this.set("parentView.isServiceListHidden", true);
+    } else {
+      this.get('parentView').switchView("HOSTS_LIST");
     }
   },
 
@@ -381,12 +388,7 @@ App.HostProgressPopupBodyView = App.TableView.extend({
    */
   resetState: function () {
     if (this.get('parentView.isOpen')) {
-      this.get('parentView').setProperties({
-        isLogWrapHidden: true,
-        isTaskListHidden: true,
-        isHostListHidden: true,
-        isServiceListHidden: false
-      });
+      this.get('parentView').switchView("OPS_LIST");
       this.get("controller").setBackgroundOperationHeader(false);
       this.get('controller.hosts').clear();
       this.setOnStart();
@@ -526,69 +528,148 @@ App.HostProgressPopupBodyView = App.TableView.extend({
     this.set('isPaginate', !!isPaginate);
   }.observes('tasks.@each.status', 'hosts.@each.status', 'parentView.isTaskListHidden', 'parentView.isHostListHidden', 'services.@each.status'),
 
+  setBreadcrumbs: function (level) {
+    const breadcrumbs = [];
+    const self = this;
+    const opsCrumb = this.get("controller.rootBreadcrumb");
+
+    if (opsCrumb) {
+      opsCrumb.action = function () { self.switchLevel("OPS_LIST"); }
+
+      const opCrumb = {
+        label: this.get("controller.serviceName"),
+        action: function () { self.switchLevel("HOSTS_LIST", self.get('controller.servicesInfo').findProperty('id', self.get('controller.currentServiceId'))); }
+      }
+
+      const hostCrumb = {
+        label: this.get("controller.currentHostName"),
+        action: function () { self.switchLevel("TASKS_LIST", self.get('currentHost')); }
+      }
+
+      const taskCrumb = {
+        itemView: Em.View.extend({
+          tagName: "span",
+          controller: self,
+          template: Em.Handlebars.compile('<i style="margin-left: 20px;" {{bindAttr class="openedTask.status :task-detail-status-ico openedTask.icon"}}></i>{{openedTask.commandDetail}}')
+        })
+      }
+
+      switch (level) {
+        case "OPS_LIST":
+          breadcrumbs.push(opsCrumb);
+          break;
+        case "HOSTS_LIST":
+          breadcrumbs.push(opsCrumb);
+          if (opCrumb.label === breadcrumbs[0].label) {
+            breadcrumbs.pop();
+          }
+          breadcrumbs.push(opCrumb);
+          break;
+        case "TASKS_LIST":
+          breadcrumbs.push(opsCrumb);
+          if (opCrumb.label === breadcrumbs[0].label) {
+            breadcrumbs.pop();
+          }
+          breadcrumbs.push(opCrumb);
+          breadcrumbs.push(hostCrumb);
+          break;
+        case "TASK_DETAILS":
+          breadcrumbs.push(opsCrumb);
+          if (opCrumb.label === breadcrumbs[0].label) {
+            breadcrumbs.pop();
+          }
+          breadcrumbs.push(opCrumb);
+          breadcrumbs.push(hostCrumb);
+          breadcrumbs.push(taskCrumb);
+          break;
+      }
+
+      this.set('controller.breadcrumbs', breadcrumbs);
+    }
+  },
+
   /**
-   * control data uploading, depending on which display level is showed
+   * Sets up and tears down the different views in the modal when switching.
+   * Calls switchView() on the controller to perform the actual view switch.
    *
    * @param {string} levelName
    * @method switchLevel
    */
-  switchLevel: function (levelName, isBackToLevel = false) {
-    var dataSourceController = this.get('controller.dataSourceController');
-    var args = [].slice.call(arguments);
-    this.get('hostComponentLogs').clear();
+  switchLevel: function (levelName, context) {
+    const prevLevel = this.get('controller.dataSourceController.levelInfo.name');
+
+    //leaving level - do any cleanup here
+    switch (prevLevel) {
+      case "OPS_LIST":
+        break;
+      case "HOSTS_LIST":
+        break;
+      case "TASKS_LIST":
+        break;
+      case "TASK_DETAILS":
+        this.destroyClipBoard();
+        break;
+    }
+
+    //entering level - do any setup here
+    switch (levelName) {
+      case "OPS_LIST":
+        this.gotoOps();
+        break;
+      case "HOSTS_LIST":
+        this.gotoHosts(context);
+        break;
+      case "TASKS_LIST":
+        this.gotoTasks(context);
+        break;
+      case "TASK_DETAILS":
+        this.goToTaskDetails(context);
+        break;
+    }
+
+    if (!this.get("controller.isBackgroundOperations")) {
+      var customControllersSwitchLevelMap = this.get('customControllersSwitchLevelMap');
+      var args = [].slice.call(arguments);
+      Em.tryInvoke(this, customControllersSwitchLevelMap[this.get('controller.dataSourceController.name')], args);
+    }
+  },
+
+  changeLevel: function(levelName) {
     if (this.get("controller.isBackgroundOperations")) {
+      var dataSourceController = this.get('controller.dataSourceController');
       var levelInfo = dataSourceController.get('levelInfo');
+
       levelInfo.set('taskId', this.get('openedTaskId'));
       levelInfo.set('requestId', this.get('controller.currentServiceId'));
       levelInfo.set('name', levelName);
-      if (levelName === 'HOSTS_LIST') {
-        this.set('isLevelLoaded', dataSourceController.requestMostRecent());
-        if (!isBackToLevel) {
-          this.set('hostCategory', this.get('categories').findProperty('value', 'all'));
-        }
-      }
-      else {
-        if (levelName === 'TASK_DETAILS') {
-          dataSourceController.requestMostRecent();
-          this.set('isLevelLoaded', false);
-        }
-        else {
-          if (levelName === 'REQUESTS_LIST') {
-            if (!isBackToLevel) {
-              this.set('serviceCategory', this.get('categories').findProperty('value', 'all'));
-            }
-            this.get('controller.hosts').clear();
-            dataSourceController.requestMostRecent();
-          }
-          else {
-            if (!isBackToLevel) {
-              this.set('taskCategory', this.get('categories').findProperty('value', 'all'));
-            }
-          }
-        }
-      }
-    }
-    else {
-      var customControllersSwitchLevelMap = this.get('customControllersSwitchLevelMap');
-      Em.tryInvoke(this, customControllersSwitchLevelMap[dataSourceController.get('name')], args);
+      this.set('isLevelLoaded', dataSourceController.requestMostRecent());
     }
+
+    this.set('currentLevel', levelName); //NOTE: setting this triggers levelDidChange() which updates the breadcrumbs
   },
 
+  //This is triggered by changeLevel() -- (and probably some other things) --
+  //when "controller.dataSourceController.levelInfo.name" is set
   levelDidChange: function() {
-    var levelName = this.get('controller.dataSourceController.levelInfo.name'),
+    var levelName = this.get('currentLevel'),
         self = this;
 
+    if (this.get("parentView.isOpen")) {
+      self.setBreadcrumbs(levelName);
+    }
+
     if (levelName && this.get('isLevelLoaded')) {
       Em.run.next(this, function() {
         self.resizeHandler();
       });
     }
-  }.observes('controller.dataSourceController.levelInfo.name', 'isLevelLoaded'),
-
+  }.observes('currentLevel', 'isLevelLoaded'),
 
   popupIsOpenDidChange: function() {
-    if (!this.get('isOpen')) {
-      this.get('hostComponentLogs').clear();
+    const hostComponentLogs = this.get('hostComponentLogs');
+
+    if (!this.get('parentView.isOpen') && hostComponentLogs) {
+      hostComponentLogs.clear();
     }
   }.observes('parentView.isOpen'),
 
@@ -606,8 +687,7 @@ App.HostProgressPopupBodyView = App.TableView.extend({
       Em.keys(this.get('parentView.detailedProperties')).forEach(function (key) {
         dataSourceController.addObserver('taskInfo.' + this.get('parentView.detailedProperties')[key], this, 'updateTaskInfo');
       }, this);
-    }
-    else {
+    } else {
       dataSourceController.stopTaskPolling();
     }
   },
@@ -632,10 +712,6 @@ App.HostProgressPopupBodyView = App.TableView.extend({
    * @method backToTaskList
    */
   backToTaskList: function () {
-    this.destroyClipBoard();
-    this.set("openedTaskId", 0);
-    this.set("parentView.isLogWrapHidden", true);
-    this.set("parentView.isTaskListHidden", false);
     this.switchLevel("TASKS_LIST", true);
   },
 
@@ -645,27 +721,17 @@ App.HostProgressPopupBodyView = App.TableView.extend({
    * @method backToHostList
    */
   backToHostList: function () {
-    this.set("parentView.isHostListHidden", false);
-    this.set("parentView.isTaskListHidden", true);
-    this.get("controller").set("popupHeaderName", this.get("controller.serviceName"));
-    this.get("controller").set("operationInfo", this.get('controller.servicesInfo').findProperty('name', this.get('controller.serviceName')));
     this.switchLevel("HOSTS_LIST", true);
   },
 
   /**
-   * Onclick handler for button <-Services
+   * Onclick handler for button <-Operations
+   * TODO: This is still used somewhere outside the Background Operations heirarchy.
    *
    * @method backToServiceList
    */
   backToServiceList: function () {
-    this.get("controller").set("serviceName", "");
-    this.set("parentView.isHostListHidden", true);
-    this.set("parentView.isServiceListHidden", false);
-    this.set("parentView.isTaskListHidden", true);
-    this.set("parentView.isLogWrapHidden", true);
-    this.set("hosts", null);
-    this.get("controller").setBackgroundOperationHeader(false);
-    this.switchLevel("REQUESTS_LIST", true);
+    this.switchLevel("OPS_LIST", true);
   },
 
   /**
@@ -689,27 +755,40 @@ App.HostProgressPopupBodyView = App.TableView.extend({
     }
   }.observes('parentView.isOpen', 'App.router.backgroundOperationsController.isShowMoreAvailable'),
 
+  gotoOps: function () {
+    this.get('controller.hosts').clear();
+    var dataSourceController = this.get('controller.dataSourceController');
+    dataSourceController.requestMostRecent();
+    this.get("controller").setBackgroundOperationHeader(false);
+
+    this.changeLevel("OPS_LIST");
+    this.get("parentView").switchView("OPS_LIST");
+  },
+
   /**
-   * Onclick handler for selected Service
+   * Onclick handler for selected Service (Operation)
    *
    * @param {{context: wrappedService}} event
    * @method gotoHosts
    */
-  gotoHosts: function (event) {
-    this.get("controller").set("serviceName", event.context.get("name"));
-    this.get("controller").set("currentServiceId", event.context.get("id"));
+  onOpClick: function(event) {
+    this.switchLevel("HOSTS_LIST", event.context);
+  },
+
+  gotoHosts: function (service) {
+    this.get("controller").set("serviceName", service.get("name"));
+    this.get("controller").set("currentServiceId", service.get("id"));
     this.get("controller").set("currentHostName", null);
     this.get("controller").onHostUpdate();
-    this.switchLevel("HOSTS_LIST");
+    this.get('hostComponentLogs').clear();
+
+    this.changeLevel("HOSTS_LIST");
+
     var servicesInfo = this.get("controller.hosts");
-    this.set("controller.popupHeaderName", event.context.get("name"));
-    this.set("controller.operationInfo", event.context);
+    this.set("controller.operationInfo", service);
 
     //apply lazy loading on cluster with more than 100 nodes
     this.set('hosts', servicesInfo.length > 100 ? servicesInfo.slice(0, 50) : servicesInfo);
-    this.set("parentView.isServiceListHidden", true);
-    this.set("parentView.isHostListHidden", false);
-    this.set("parentView.isTaskListHidden", true);
     $(".modal").scrollTop(0);
     $(".modal-body").scrollTop(0);
     if (servicesInfo.length > 100) {
@@ -718,9 +797,13 @@ App.HostProgressPopupBodyView = App.TableView.extend({
       });
     }
     // Determine if source request schedule is present
-    this.set('sourceRequestScheduleId', event.context.get("sourceRequestScheduleId"));
-    this.set('sourceRequestScheduleCommand', event.context.get('contextCommand'));
+    this.set('sourceRequestScheduleId', service.get("sourceRequestScheduleId"));
+    this.set('sourceRequestScheduleCommand', service.get('contextCommand'));
     this.refreshRequestScheduleInfo();
+
+    this.set('hostCategory', this.get('categories').findProperty('value', 'all'));
+
+    this.get("parentView").switchView("HOSTS_LIST");
   },
 
   /**
@@ -851,22 +934,34 @@ App.HostProgressPopupBodyView = App.TableView.extend({
    * @param {{context: wrappedHost}} event
    * @method gotoTasks
    */
-  gotoTasks: function (event) {
+  onHostClick: function (event) {
+    this.switchLevel("TASKS_LIST", event.context);
+  },
+
+  gotoTasks: function (host) {
     var tasksInfo = [];
-    event.context.logTasks.forEach(function (_task) {
-      tasksInfo.pushObject(this.get("controller").createTask(_task));
-    }, this);
+
+    if (host.logTasks) {
+      host.logTasks.forEach(function (_task) {
+        tasksInfo.pushObject(this.get("controller").createTask(_task));
+      }, this);
+    }
+
     if (tasksInfo.length) {
-      this.get("controller").set("popupHeaderName", event.context.publicName);
-      this.get("controller").set("currentHostName", event.context.publicName);
+      this.get("controller").set("currentHostName", host.publicName);
+    }
+
+    const currentHost = this.get("currentHost");
+    if (currentHost) {
+      currentHost.set("tasks", tasksInfo);
     }
-    this.switchLevel("TASKS_LIST");
-    this.set('currentHost.tasks', tasksInfo);
-    this.set("parentView.isHostListHidden", true);
-    this.set("parentView.isTaskListHidden", false);
-    this.preloadHostModel(Em.getWithDefault(event.context || {}, 'name', false));
+    this.preloadHostModel(Em.getWithDefault(host || {}, 'name', false));
+    this.set('taskCategory', this.get('categories').findProperty('value', 'all'));
     $(".modal").scrollTop(0);
     $(".modal-body").scrollTop(0);
+
+    this.changeLevel("TASKS_LIST");
+    this.get("parentView").switchView("TASKS_LIST");
   },
 
   /**
@@ -939,31 +1034,42 @@ App.HostProgressPopupBodyView = App.TableView.extend({
     newDocument.close();
   },
 
+  onTaskClick: function (event) {
+    this.switchLevel("TASK_DETAILS", event.context);
+  },
+
   /**
    * Onclick event for show task detail info
    *
    * @param {{context: wrappedTask}} event
-   * @method toggleTaskLog
+   * @method goToTaskDetails
    */
-  toggleTaskLog: function (event) {
-    var taskInfo = event.context;
-    this.set("parentView.isLogWrapHidden", false);
+  goToTaskDetails: function (taskInfo) {
     const self = this;
     var taskLogsClipboard = new Clipboard('.btn.copy-clipboard', {
       text: function() {
         return self.get('textAreaValue');
       }
     });
+
     this.set('taskLogsClipboard', taskLogsClipboard);
     if (this.get('isClipBoardActive')) {
       this.destroyClipBoard();
     }
-    this.set("parentView.isHostListHidden", true);
-    this.set("parentView.isTaskListHidden", true);
+
     this.set('openedTaskId', taskInfo.id);
-    this.switchLevel("TASK_DETAILS");
+
+    if (this.get("controller.isBackgroundOperations")) {
+      var dataSourceController = this.get('controller.dataSourceController');
+      dataSourceController.requestMostRecent();
+      this.set('isLevelLoaded', false);
+    }
+
     $(".modal").scrollTop(0);
     $(".modal-body").scrollTop(0);
+
+    this.changeLevel("TASK_DETAILS");
+    this.get("parentView").switchView("TASK_DETAILS");
   },
 
   /**

http://git-wip-us.apache.org/repos/asf/ambari/blob/38604db2/ambari-web/test/controllers/global/background_operations_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/controllers/global/background_operations_test.js b/ambari-web/test/controllers/global/background_operations_test.js
index 4a8235c..cfadbf5 100644
--- a/ambari-web/test/controllers/global/background_operations_test.js
+++ b/ambari-web/test/controllers/global/background_operations_test.js
@@ -43,7 +43,7 @@ describe('App.BackgroundOperationsController', function () {
     var tests = Em.A([
       {
         levelInfo: Em.Object.create({
-          name: 'REQUESTS_LIST',
+          name: 'OPS_LIST',
           requestId: null,
           taskId: null,
           sync: false
@@ -539,7 +539,7 @@ describe('App.BackgroundOperationsController', function () {
 
     it("should return false when not on HOSTS_LIST level", function() {
       controller.set('levelInfo', Em.Object.create({
-        name: 'SERVICES_LIST'
+        name: 'OPS_LIST'
       }));
       expect(controller.isInitLoading()).to.be.false;
     });

http://git-wip-us.apache.org/repos/asf/ambari/blob/38604db2/ambari-web/test/controllers/main/admin/highAvailability/progress_popup_controller_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/controllers/main/admin/highAvailability/progress_popup_controller_test.js b/ambari-web/test/controllers/main/admin/highAvailability/progress_popup_controller_test.js
index 364d9b0..2742c44 100644
--- a/ambari-web/test/controllers/main/admin/highAvailability/progress_popup_controller_test.js
+++ b/ambari-web/test/controllers/main/admin/highAvailability/progress_popup_controller_test.js
@@ -343,6 +343,9 @@ describe('App.HighAvailabilityProgressPopupController', function () {
     it("calculate data", function() {
       var data = [
         {
+          Requests: {
+            id: 1
+          },
           tasks: [
             {
               Tasks: {
@@ -365,6 +368,7 @@ describe('App.HighAvailabilityProgressPopupController', function () {
       controller.calculateHostsData(data);
       expect(controller.get('services')).to.be.eql([
         {
+          "id": 1,
           "name": "popupTitle",
           "hosts": [
             {

http://git-wip-us.apache.org/repos/asf/ambari/blob/38604db2/ambari-web/test/utils/host_progress_popup_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/utils/host_progress_popup_test.js b/ambari-web/test/utils/host_progress_popup_test.js
index dea2666..23ccf37 100644
--- a/ambari-web/test/utils/host_progress_popup_test.js
+++ b/ambari-web/test/utils/host_progress_popup_test.js
@@ -516,6 +516,11 @@ describe('App.HostPopup', function () {
 
   });
 
+  describe("#initPopup", function() {
+    it("should reset the breadcrumbs", function() {
+      App.HostPopup.initPopup("rootBreadcrumb", Em.Object.create({ services: [] }));
 
-
+      expect(App.HostPopup.get("breadcrumbs")).to.deep.equal([{ label: "rootBreadcrumb" }]);
+    });
+  });
 });

http://git-wip-us.apache.org/repos/asf/ambari/blob/38604db2/ambari-web/test/views/common/breadcrumbs_view_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/views/common/breadcrumbs_view_test.js b/ambari-web/test/views/common/breadcrumbs_view_test.js
index eff19f6..3e2fc17 100644
--- a/ambari-web/test/views/common/breadcrumbs_view_test.js
+++ b/ambari-web/test/views/common/breadcrumbs_view_test.js
@@ -34,6 +34,34 @@ describe('App.BreadcrumbItem', function () {
 
   });
 
+  describe('#transition', function() {
+
+    beforeEach(function() {
+      sinon.stub(App.router, "route");
+
+      this.breadcrumb = App.BreadcrumbItem.create({
+        label: "label",
+        route: "route"
+      });
+    })
+
+    afterEach(function() {
+      App.router.route.restore();
+    })
+
+    it('App.router.route should be called', function() {
+      this.breadcrumb.transition();
+      expect(App.router.route.calledWith('main/' + this.breadcrumb.get("route"))).to.be.true;
+    })
+
+    it('action should be called when defined', function() {
+      this.breadcrumb.action = sinon.stub();
+      this.breadcrumb.transition();
+      expect(this.breadcrumb.action.called).to.be.true;
+      expect(App.router.route.called).to.be.false;
+    })
+  })
+
 });
 
 function getCurrentState(parentStateProps, currentStateProps) {
@@ -109,4 +137,4 @@ describe('App.BreadcrumbsView', function () {
 
   });
 
-});
\ No newline at end of file
+});

http://git-wip-us.apache.org/repos/asf/ambari/blob/38604db2/ambari-web/test/views/common/host_progress_popup_body_view_test.js
----------------------------------------------------------------------
diff --git a/ambari-web/test/views/common/host_progress_popup_body_view_test.js b/ambari-web/test/views/common/host_progress_popup_body_view_test.js
index 5ccc024..7f905ea 100644
--- a/ambari-web/test/views/common/host_progress_popup_body_view_test.js
+++ b/ambari-web/test/views/common/host_progress_popup_body_view_test.js
@@ -17,324 +17,473 @@
  */
 
 var App = require('app');
-var view;
+
+require("utils/host_progress_popup");
+require("views/common/modal_popup")
 
 describe('App.HostProgressPopupBodyView', function () {
+  var controller;
 
   beforeEach(function () {
-    view = App.HostProgressPopupBodyView.create({
-      controller: Em.Object.create({
-        setSelectCount: Em.K,
-        dataSourceController: Em.Object.create({}),
-        setBackgroundOperationHeader: Em.K,
-        hosts: []
+    controller = Em.Object.create({
+      setSelectCount: Em.K,
+      dataSourceController: Em.Object.create({
+        levelInfo: {},
+        requestMostRecent: Em.K
       }),
-      parentView: Em.Object.create()
+      refreshRequestScheduleInfo: Em.K,
+      setBackgroundOperationHeader: Em.K,
+      onHostUpdate: Em.K,
+      hosts: []
     });
   });
 
-  describe('#switchLevel', function () {
-
-    var map = App.HostProgressPopupBodyView.create().get('customControllersSwitchLevelMap');
-
-    Object.keys(map).forEach(function (controllerName) {
-      var methodName = map [controllerName];
-      var levelName = 'REQUESTS_LIST';
+  describe('when not isBackgroundOperations', function() {
+    var view;
 
-      beforeEach(function () {
-        sinon.stub(view, methodName, Em.K);
-      });
-
-      afterEach(function () {
-        view[methodName].restore();
-      });
-
-      it('should call ' + methodName, function () {
-        view.set('controller.dataSourceController.name', controllerName);
-        view.switchLevel(levelName);
-        expect(view[methodName].args[0]).to.eql([levelName]);
+    beforeEach(function () {
+      view = App.HostProgressPopupBodyView.create({
+        controller: controller,
+        parentView: App.HostPopup.initPopup("serviceName", controller, false, 1)
       });
-
     });
 
-  });
-
-  describe('_determineRoleRelation', function() {
-    var cases;
+    describe('#switchLevel when isBackgroundOperations is false', function () {
+      var map = App.HostProgressPopupBodyView.create().get('customControllersSwitchLevelMap');
 
-    beforeEach(function() {
-      sinon.stub(App.StackServiceComponent, 'find').returns([{componentName: 'DATANODE'}]);
-      sinon.stub(App.StackService, 'find').returns([{serviceName: 'HDFS'}])
-    });
+      Object.keys(map).forEach(function (controllerName) {
+        var methodName = map [controllerName];
+        var levelName = 'OPS_LIST';
 
-    afterEach(function() {
-      App.StackServiceComponent.find.restore();
-      App.StackService.find.restore();
-    });
+        beforeEach(function () {
+          sinon.stub(view, methodName, Em.K);
+        });
 
-    cases = [
-      {
-        task: { role: 'HDFS_SERVICE_CHECK'},
-        m: 'Role is HDFS_SERVICE_CHECK',
-        e: {
-          type: 'service',
-          value: 'HDFS'
-        }
-      },
-      {
-        task: { role: 'DATANODE'},
-        m: 'Role is DATANODE',
-        e: {
-          type: 'component',
-          value: 'DATANODE'
-        }
-      },
-      {
-        task: { role: 'UNDEFINED'},
-        m: 'Role is UNDEFINED',
-        e: false
-      }
-    ];
-
-    cases.forEach(function(test) {
-      it(test.m, function() {
-        view.reopen({
-          currentHost: Em.Object.create({
-            logTasks: [
-              { Tasks: { id: 1, role: test.task.role }}
-            ]
-          })
+        afterEach(function () {
+          view[methodName].restore();
         });
 
-        var ret = view._determineRoleRelation(Em.Object.create({ id: 1 }));
-        expect(ret).to.be.eql(test.e);
+        it('should call ' + methodName, function () {
+          view.set('controller.dataSourceController.name', controllerName);
+          view.switchLevel(levelName);
+          expect(view[methodName].args[0]).to.eql([levelName]);
+        });
       });
     });
-  });
 
-  describe('#didInsertElement', function () {
-
-    beforeEach(function () {
-      sinon.stub(view, 'updateHostInfo', Em.K);
-      view.didInsertElement();
-    });
-
-    afterEach(function () {
-      view.updateHostInfo.restore();
-    });
-
-    it('should display relevant info', function () {
-      expect(view.updateHostInfo.calledOnce).to.be.true;
-    });
-
-  });
-
-  describe('#preloadHostModel', function() {
-    describe('When Log Search installed', function() {
+    describe('_determineRoleRelation', function() {
+      var cases;
 
       beforeEach(function() {
-        this.HostModelStub = sinon.stub(App.Host, 'find');
-        this.isLogSearchInstalled = sinon.stub(view, 'get').withArgs('isLogSearchInstalled');
-        this.logSearchSupported = sinon.stub(App, 'get').withArgs('supports.logSearch');
-        this.updateCtrlStub = sinon.stub(App.router.get('updateController'), 'updateLogging');
+        sinon.stub(App.StackServiceComponent, 'find').returns([{componentName: 'DATANODE'}]);
+        sinon.stub(App.StackService, 'find').returns([{serviceName: 'HDFS'}])
       });
 
-      afterEach(function () {
-        App.Host.find.restore();
-        view.get.restore();
-        App.get.restore();
-        App.router.get('updateController').updateLogging.restore();
+      afterEach(function() {
+        App.StackServiceComponent.find.restore();
+        App.StackService.find.restore();
       });
 
-      [
-        {
-          hostName: 'host1',
-          logSearchSupported: true,
-          isLogSearchInstalled: true,
-          requestFailed: false,
-          hosts: [
-            {
-              hostName: 'host2'
-            }
-          ],
-          e: {
-            updateLoggingCalled: true
-          },
-          m: 'Host absent, log search installed and supported'
-        },
+      cases = [
         {
-          hostName: 'host1',
-          logSearchSupported: true,
-          isLogSearchInstalled: true,
-          requestFailed: false,
-          hosts: [
-            {
-              hostName: 'host1'
-            }
-          ],
+          task: { role: 'HDFS_SERVICE_CHECK'},
+          m: 'Role is HDFS_SERVICE_CHECK',
           e: {
-            updateLoggingCalled: false
-          },
-          m: 'Host present, log search installed and supported'
+            type: 'service',
+            value: 'HDFS'
+          }
         },
         {
-          hostName: 'host1',
-          logSearchSupported: false,
-          isLogSearchInstalled: true,
-          requestFailed: false,
-          hosts: [
-            {
-              hostName: 'host1'
-            }
-          ],
+          task: { role: 'DATANODE'},
+          m: 'Role is DATANODE',
           e: {
-            updateLoggingCalled: false
-          },
-          m: 'Host present, log search installed and support is off'
-        },
-        {
-          hostName: 'host1',
-          logSearchSupported: true,
-          isLogSearchInstalled: true,
-          requestFailed: true,
-          hosts: [
-            {
-              hostName: 'host2'
-            }
-          ],
-          e: {
-            updateLoggingCalled: true
-          },
-          m: 'Host is absent, log search installed and supported, update request was failed'
+            type: 'component',
+            value: 'DATANODE'
+          }
         },
         {
-          hostName: 'host1',
-          logSearchSupported: true,
-          isLogSearchInstalled: false,
-          requestFailed: true,
-          hosts: [
-            {
-              hostName: 'host2'
-            }
-          ],
-          e: {
-            updateLoggingCalled: false
-          },
-          m: 'Host is absent, log search not installed and supported'
+          task: { role: 'UNDEFINED'},
+          m: 'Role is UNDEFINED',
+          e: false
         }
-      ].forEach(function(test) {
+      ];
+
+      cases.forEach(function(test) {
+        it(test.m, function() {
+          view.reopen({
+            currentHost: Em.Object.create({
+              logTasks: [
+                { Tasks: { id: 1, role: test.task.role }}
+              ]
+            })
+          });
 
-        it('hostInfoLoaded should be true on init', function() {
-          expect(Em.get(view, 'hostInfoLoaded')).to.be.true;
+          var ret = view._determineRoleRelation(Em.Object.create({ id: 1 }));
+          expect(ret).to.be.eql(test.e);
         });
+      });
+    });
 
-        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());
-            }
-            Em.set(view, 'hostInfoLoaded', false);
-            view.preloadHostModel(test.hostName);
-          });
+    describe('#didInsertElement', function () {
 
-          it('updateLogging call validation', function() {
-            expect(App.router.get('updateController').updateLogging.called).to.be.equal(test.e.updateLoggingCalled);
-          });
+      beforeEach(function () {
+        sinon.stub(view, 'updateHostInfo', Em.K);
+        view.didInsertElement();
+      });
+
+      afterEach(function () {
+        view.updateHostInfo.restore();
+      });
+
+      it('should display relevant info', function () {
+        expect(view.updateHostInfo.calledOnce).to.be.true;
+      });
+
+    });
+
+    describe('#preloadHostModel', function() {
+      describe('When Log Search installed', function() {
 
-          it('in result hostInfoLoaded should be always true', function() {
+        beforeEach(function() {
+          this.HostModelStub = sinon.stub(App.Host, 'find');
+          this.isLogSearchInstalled = sinon.stub(view, 'get').withArgs('isLogSearchInstalled');
+          this.logSearchSupported = sinon.stub(App, 'get').withArgs('supports.logSearch');
+          this.updateCtrlStub = sinon.stub(App.router.get('updateController'), 'updateLogging');
+        });
+
+        afterEach(function () {
+          App.Host.find.restore();
+          view.get.restore();
+          App.get.restore();
+          App.router.get('updateController').updateLogging.restore();
+        });
+
+        [
+          {
+            hostName: 'host1',
+            logSearchSupported: true,
+            isLogSearchInstalled: true,
+            requestFailed: false,
+            hosts: [
+              {
+                hostName: 'host2'
+              }
+            ],
+            e: {
+              updateLoggingCalled: true
+            },
+            m: 'Host absent, log search installed and supported'
+          },
+          {
+            hostName: 'host1',
+            logSearchSupported: true,
+            isLogSearchInstalled: true,
+            requestFailed: false,
+            hosts: [
+              {
+                hostName: 'host1'
+              }
+            ],
+            e: {
+              updateLoggingCalled: false
+            },
+            m: 'Host present, log search installed and supported'
+          },
+          {
+            hostName: 'host1',
+            logSearchSupported: false,
+            isLogSearchInstalled: true,
+            requestFailed: false,
+            hosts: [
+              {
+                hostName: 'host1'
+              }
+            ],
+            e: {
+              updateLoggingCalled: false
+            },
+            m: 'Host present, log search installed and support is off'
+          },
+          {
+            hostName: 'host1',
+            logSearchSupported: true,
+            isLogSearchInstalled: true,
+            requestFailed: true,
+            hosts: [
+              {
+                hostName: 'host2'
+              }
+            ],
+            e: {
+              updateLoggingCalled: true
+            },
+            m: 'Host is absent, log search installed and supported, update request was failed'
+          },
+          {
+            hostName: 'host1',
+            logSearchSupported: true,
+            isLogSearchInstalled: false,
+            requestFailed: true,
+            hosts: [
+              {
+                hostName: 'host2'
+              }
+            ],
+            e: {
+              updateLoggingCalled: false
+            },
+            m: 'Host is absent, log search not installed and supported'
+          }
+        ].forEach(function(test) {
+
+          it('hostInfoLoaded should be true on init', function() {
             expect(Em.get(view, 'hostInfoLoaded')).to.be.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());
+              }
+              Em.set(view, 'hostInfoLoaded', false);
+              view.preloadHostModel(test.hostName);
+            });
+
+            it('updateLogging call validation', function() {
+              expect(App.router.get('updateController').updateLogging.called).to.be.equal(test.e.updateLoggingCalled);
+            });
+
+            it('in result hostInfoLoaded should be always true', function() {
+              expect(Em.get(view, 'hostInfoLoaded')).to.be.true;
+            });
+
+          });
         });
       });
     });
-  });
 
-  describe("#resetState()", function () {
+    describe("#resetState()", function () {
 
-    beforeEach(function() {
-      sinon.stub(view.get('controller'), 'setBackgroundOperationHeader');
-      sinon.stub(view, 'setOnStart');
-      sinon.stub(view, 'rerender');
-      sinon.stub(view, 'updateSelectView');
-    });
+      beforeEach(function() {
+        sinon.stub(view.get('controller'), 'setBackgroundOperationHeader');
+        sinon.stub(view, 'setOnStart');
+        sinon.stub(view, 'rerender');
+        sinon.stub(view, 'updateSelectView');
+      });
 
-    afterEach(function() {
-      view.get('controller').setBackgroundOperationHeader.restore();
-      view.setOnStart.restore();
-      view.rerender.restore();
-      view.updateSelectView.restore();
-    });
+      afterEach(function() {
+        view.get('controller').setBackgroundOperationHeader.restore();
+        view.setOnStart.restore();
+        view.rerender.restore();
+        view.updateSelectView.restore();
+      });
 
-    it("should set properties of parentView", function() {
-      view.set('parentView.isOpen', true);
-      expect(JSON.stringify(view.get('parentView'))).to.be.equal(JSON.stringify({
-        "isOpen": true,
-        "isLogWrapHidden": true,
-        "isTaskListHidden": true,
-        "isHostListHidden": true,
-        "isServiceListHidden": false
-      }));
-    });
+      it("should set properties of parentView", function() {
+        view.set('parentView.isOpen', true);
+        expect(view.get('parentView.isOpen')).to.be.true;
+        expect(view.get('parentView.isLogWrapHidden')).to.be.true;
+        expect(view.get('parentView.isTaskListHidden')).to.be.true;
+        expect(view.get('parentView.isHostListHidden')).to.be.true;
+        expect(view.get('parentView.isServiceListHidden')).to.be.false;
+      });
 
-    it("setBackgroundOperationHeader should be called", function() {
-      view.set('parentView.isOpen', true);
-      expect(view.get('controller').setBackgroundOperationHeader.calledWith(false)).to.be.true;
-    });
+      it("setBackgroundOperationHeader should be called", function() {
+        view.set('parentView.isOpen', true);
+        expect(view.get('controller').setBackgroundOperationHeader.calledWith(false)).to.be.true;
+      });
+
+      it("controller.hosts should be empty", function() {
+        view.set('controller.hosts', [Em.Object.create({})]);
+        view.set('parentView.isOpen', true);
+        expect(view.get('controller.hosts')).to.be.empty;
+      });
+
+      it("setOnStart should be called", function() {
+        view.set('parentView.isOpen', true);
+        //console.log("setOnStart.callCount:", view.setOnStart.callCount);
+        expect(view.setOnStart.calledOnce, "calledOnce").to.be.true;
+      });
 
-    it("controller.hosts should be empty", function() {
-      view.set('controller.hosts', [Em.Object.create({})]);
-      view.set('parentView.isOpen', true);
-      expect(view.get('controller.hosts')).to.be.empty;
+      it("rerender should be called", function() {
+        view.set('parentView.isOpen', true);
+        expect(view.rerender.calledOnce).to.be.true;
+      });
     });
 
-    it("setOnStart should be called", function() {
-      view.set('parentView.isOpen', true);
-      expect(view.setOnStart.calledOnce).to.be.true;
+    describe('#goToTaskDetails', function () {
+
+      var task = {};
+
+      beforeEach(function() {
+        view.goToTaskDetails({context: task});
+      });
+
+      it('clipboard created', function () {
+        expect(view.get('taskLogsClipboard')).to.be.instanceOf(Clipboard);
+      });
+
     });
 
-    it("rerender should be called", function() {
-      view.set('parentView.isOpen', true);
-      expect(view.rerender.calledOnce).to.be.true;
+    describe('#destroyClipBoard', function () {
+
+      beforeEach(function () {
+        view.goToTaskDetails({context: {}});
+        sinon.spy(view.get('taskLogsClipboard'), 'destroy');
+        view.destroyClipBoard();
+      });
+
+      afterEach(function () {
+        view.get('taskLogsClipboard').destroy.restore();
+      });
+
+      it('should destroy clipboard', function () {
+        expect(view.get('taskLogsClipboard').destroy.calledOnce).to.be.true;
+      });
+
     });
   });
 
-  describe('#toggleTaskLog', function () {
+  describe('when isBackgroundOperations', function() {
+    var view;
 
-    var task = {};
+    beforeEach(function () {
+      view = App.HostProgressPopupBodyView.create({
+        controller: controller,
+        parentView: App.HostPopup.initPopup("", controller, true)
+      });
 
-    beforeEach(function() {
-      view.toggleTaskLog({context: task});
+      sinon.stub(view, "changeLevel");
     });
 
-    it('clipboard created', function () {
-      expect(view.get('taskLogsClipboard')).to.be.instanceOf(Clipboard);
-    });
+    describe("#switchLevel", function () {
+      it("makes Operations list visible", function() {
+        view.switchLevel("OPS_LIST");
+
+        expect(view.changeLevel.calledWith("OPS_LIST")).to.be.true;
+
+        expect(view.get("parentView.isLogWrapHidden")).to.be.true;
+        expect(view.get("parentView.isTaskListHidden")).to.be.true;
+        expect(view.get("parentView.isHostListHidden")).to.be.true;
+        expect(view.get("parentView.isServiceListHidden")).to.be.false;
+      });
+
+      it("makes Hosts list visible", function() {
+        view.switchLevel("HOSTS_LIST", Em.Object.create());
+
+        expect(view.changeLevel.calledWith("HOSTS_LIST")).to.be.true;
+
+        expect(view.get("parentView.isLogWrapHidden")).to.be.true;
+        expect(view.get("parentView.isTaskListHidden")).to.be.true;
+        expect(view.get("parentView.isHostListHidden")).to.be.false;
+        expect(view.get("parentView.isServiceListHidden")).to.be.true;
+      });
 
+      it("makes Tasks list visible", function() {
+        view.switchLevel("TASKS_LIST", Em.Object.create());
+
+        expect(view.changeLevel.calledWith("TASKS_LIST")).to.be.true;
+
+        expect(view.get("parentView.isLogWrapHidden")).to.be.true;
+        expect(view.get("parentView.isTaskListHidden")).to.be.false;
+        expect(view.get("parentView.isHostListHidden")).to.be.true;
+        expect(view.get("parentView.isServiceListHidden")).to.be.true;
+      });
+
+      it("makes Task Details visible", function() {
+        view.switchLevel("TASK_DETAILS", Em.Object.create());
+
+        expect(view.changeLevel.calledWith("TASK_DETAILS")).to.be.true;
+
+        expect(view.get("parentView.isLogWrapHidden")).to.be.false;
+        expect(view.get("parentView.isTaskListHidden")).to.be.true;
+        expect(view.get("parentView.isHostListHidden")).to.be.true;
+        expect(view.get("parentView.isServiceListHidden")).to.be.true;
+      });
+    });
   });
 
-  describe('#destroyClipBoard', function () {
+  describe("#changeLevel", function() {
+    var view;
+    var controller;
 
     beforeEach(function () {
-      view.toggleTaskLog({context: {}});
-      sinon.spy(view.get('taskLogsClipboard'), 'destroy');
-      view.destroyClipBoard();
+      controller = Em.Object.create({
+        setSelectCount: Em.K,
+        dataSourceController: Em.Object.create({
+          levelInfo: {},
+          requestMostRecent: Em.K
+        }),
+        refreshRequestScheduleInfo: Em.K,
+        setBackgroundOperationHeader: Em.K,
+        onHostUpdate: Em.K,
+        hosts: [],
+        breadcrumbs: null,
+        rootBreadcrumb: { label: "rootBreadcrumb" },
+        serviceName: "serviceName",
+        currentHostName: "currentHostName"
+      });
+
+      view = App.HostProgressPopupBodyView.create({
+        controller: controller,
+        parentView: Em.Object.create({ isOpen: true })
+      });
     });
 
-    afterEach(function () {
-      view.get('taskLogsClipboard').destroy.restore();
+    it("sets correct breadcrumbs for Operations view", function() {
+      view.changeLevel("OPS_LIST");
+
+      var breadcrumbs = view.get("controller.breadcrumbs");
+
+      expect(breadcrumbs.length).to.equal(1);
+      expect(breadcrumbs[0].label).to.equal("rootBreadcrumb");
+      expect(breadcrumbs[0].action).to.be.a("function");
     });
 
-    it('should destroy clipboard', function () {
-      expect(view.get('taskLogsClipboard').destroy.calledOnce).to.be.true;
+    it("sets correct breadcrumbs for Hosts view", function() {
+      view.changeLevel("HOSTS_LIST");
+
+      var breadcrumbs = view.get("controller.breadcrumbs");
+
+      expect(breadcrumbs.length).to.equal(2);
+      expect(breadcrumbs[0].label).to.equal("rootBreadcrumb");
+      expect(breadcrumbs[0].action).to.be.a("function");
+      expect(breadcrumbs[1].label).to.equal("serviceName");
+      expect(breadcrumbs[1].action).to.be.a("function");
     });
 
-  });
+    it("sets correct breadcrumbs for Tasks view", function() {
+      view.changeLevel("TASKS_LIST");
+
+      var breadcrumbs = view.get("controller.breadcrumbs");
+
+      expect(breadcrumbs.length).to.equal(3);
+      expect(breadcrumbs[0].label).to.equal("rootBreadcrumb");
+      expect(breadcrumbs[0].action).to.be.a("function");
+      expect(breadcrumbs[1].label).to.equal("serviceName");
+      expect(breadcrumbs[1].action).to.be.a("function");
+      expect(breadcrumbs[2].label).to.equal("currentHostName");
+      expect(breadcrumbs[2].action).to.be.a("function");
+    });
 
+    it("sets correct breadcrumbs for Tasks view", function() {
+      view.changeLevel("TASK_DETAILS");
+
+      var breadcrumbs = view.get("controller.breadcrumbs");
+
+      expect(breadcrumbs.length).to.equal(4);
+      expect(breadcrumbs[0].label).to.equal("rootBreadcrumb");
+      expect(breadcrumbs[0].action).to.be.a("function");
+      expect(breadcrumbs[1].label).to.equal("serviceName");
+      expect(breadcrumbs[1].action).to.be.a("function");
+      expect(breadcrumbs[2].label).to.equal("currentHostName");
+      expect(breadcrumbs[2].action).to.be.a("function");
+      expect(breadcrumbs[3].itemView).to.be.a("function");
+    });
+  });
 });


Mime
View raw message