aurora-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From dmclaugh...@apache.org
Subject git commit: Add update information to the scheduler UI
Date Tue, 16 Sep 2014 20:24:34 GMT
Repository: incubator-aurora
Updated Branches:
  refs/heads/master 9e0b9ac43 -> c0a42f29f


Add update information to the scheduler UI

Adds update history to the job page. Adds an update details page.

Bugs closed: AURORA-614

Reviewed at https://reviews.apache.org/r/25259/


Project: http://git-wip-us.apache.org/repos/asf/incubator-aurora/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-aurora/commit/c0a42f29
Tree: http://git-wip-us.apache.org/repos/asf/incubator-aurora/tree/c0a42f29
Diff: http://git-wip-us.apache.org/repos/asf/incubator-aurora/diff/c0a42f29

Branch: refs/heads/master
Commit: c0a42f29fb73198c58266d2404ec2c32040692c8
Parents: 9e0b9ac
Author: David McLaughlin <david@dmclaughlin.com>
Authored: Tue Sep 16 13:15:40 2014 -0700
Committer: David McLaughlin <dmclaughlin@twitter.com>
Committed: Tue Sep 16 13:15:40 2014 -0700

----------------------------------------------------------------------
 .../scheduler/http/JettyServerModule.java       |   3 +
 .../aurora/scheduler/http/ui/breadcrumb.html    |   8 +-
 .../apache/aurora/scheduler/http/ui/css/app.css | 239 +++++++++++++++++++
 .../apache/aurora/scheduler/http/ui/job.html    |  61 ++++-
 .../apache/aurora/scheduler/http/ui/js/app.js   |   3 +
 .../aurora/scheduler/http/ui/js/controllers.js  |  92 ++++++-
 .../aurora/scheduler/http/ui/js/directives.js   |  50 ++++
 .../aurora/scheduler/http/ui/js/filters.js      |  53 +++-
 .../aurora/scheduler/http/ui/js/services.js     | 217 ++++++++++++++++-
 .../aurora/scheduler/http/ui/timeDisplay.html   |  19 ++
 .../apache/aurora/scheduler/http/ui/update.html | 130 ++++++++++
 .../scheduler/http/ui/updateSettings.html       |  89 +++++++
 .../thrift/org/apache/aurora/gen/api.thrift     |   4 +-
 13 files changed, 955 insertions(+), 13 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/c0a42f29/src/main/java/org/apache/aurora/scheduler/http/JettyServerModule.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/http/JettyServerModule.java b/src/main/java/org/apache/aurora/scheduler/http/JettyServerModule.java
index 0336a6e..392b4f7 100644
--- a/src/main/java/org/apache/aurora/scheduler/http/JettyServerModule.java
+++ b/src/main/java/org/apache/aurora/scheduler/http/JettyServerModule.java
@@ -231,6 +231,9 @@ public class JettyServerModule extends AbstractModule {
     registerAsset("ui/schedulingDetail.html", "/schedulingDetail.html");
     registerAsset("ui/groupSummary.html", "/groupSummary.html");
     registerAsset("ui/configSummary.html", "/configSummary.html");
+    registerAsset("ui/update.html", "/update.html");
+    registerAsset("ui/timeDisplay.html", "/timeDisplay.html");
+    registerAsset("ui/updateSettings.html", "/updateSettings.html");
 
     registerAsset("ui/css/app.css", "/css/app.css");
 

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/c0a42f29/src/main/resources/org/apache/aurora/scheduler/http/ui/breadcrumb.html
----------------------------------------------------------------------
diff --git a/src/main/resources/org/apache/aurora/scheduler/http/ui/breadcrumb.html b/src/main/resources/org/apache/aurora/scheduler/http/ui/breadcrumb.html
index c780b0f..5c4fe96 100644
--- a/src/main/resources/org/apache/aurora/scheduler/http/ui/breadcrumb.html
+++ b/src/main/resources/org/apache/aurora/scheduler/http/ui/breadcrumb.html
@@ -1,4 +1,5 @@
 <div class='row'>
+  <div class='col-md-12'>
   <!--
    Licensed under the Apache License, Version 2.0 (the "License");
    you may not use this file except in compliance with the License.
@@ -23,6 +24,11 @@
       <a href='/scheduler/{{role}}/{{environment}}'>Environment: {{environment}}</a>
     </li>
 
-    <li ng-if='job' class='active'>Job: {{job}}</li>
+    <li ng-if='job && !update' class='active'>Job: {{job}}</li>
+
+    <li ng-if='job && update'><a href='/scheduler/{{role}}/{{environment}}/{{job}}'>Job:
{{job}}</a></li>
+
+    <li ng-if='update' class='active'>Update: {{update.update.summary.updateId}}</li>
   </ul>
+  </div>
 </div>

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/c0a42f29/src/main/resources/org/apache/aurora/scheduler/http/ui/css/app.css
----------------------------------------------------------------------
diff --git a/src/main/resources/org/apache/aurora/scheduler/http/ui/css/app.css b/src/main/resources/org/apache/aurora/scheduler/http/ui/css/app.css
index 2a75231..befd590 100644
--- a/src/main/resources/org/apache/aurora/scheduler/http/ui/css/app.css
+++ b/src/main/resources/org/apache/aurora/scheduler/http/ui/css/app.css
@@ -156,4 +156,243 @@ div.pagination {
 
 .completed-tasks-tab {
   padding-top: 10px;
+}
+
+.content-box {
+  border: 1px solid #ddd;
+  padding: 15px;
+  margin: 15px 0;
+}
+
+.current-update {
+  border-left: 5px solid #5cb85c;
+}
+
+.text-center {
+  text-align: center;
+}
+
+.details {
+  text-transform: uppercase;
+  font-size: 11px;
+}
+
+.update-settings {
+  font-size: 11px;
+}
+
+.update-settings span {
+  text-transform: uppercase;
+}
+
+.finished-update {
+  font-size: 11px;
+  text-transform: uppercase;
+}
+
+.finished-update h3 {
+  text-transform: none;
+}
+
+.setting-label {
+  text-transform: uppercase;
+  width: 200px;
+  display: inline-block;
+}
+
+.axis {
+  font-size: 10px;
+}
+
+.x line, .y line {
+  stroke: #ccc;
+  stroke-dasharray: 5,5;
+}
+
+.x.axis path, .y.axis path {
+  stroke: #444;
+  fill: none;
+}
+
+.progress {
+  margin-bottom: 0;
+}
+
+.instance-grid {
+  list-style-type: none;
+  margin: 0;
+  padding: 0;
+  margin-bottom: 20px;
+}
+
+.instance-grid:after {
+  clear: both;
+  content: '#';
+  visibility: hidden;
+}
+
+.instance-grid li {
+  padding: 0;
+  float: left;
+}
+
+.instance-grid .instance-id {
+  visibility: hidden;
+}
+
+.instance-grid.big .instance-id {
+  visibility: visible;
+}
+
+.instance-grid.medium li {
+  width: 7px;
+  height: 7px;
+  margin-right: 2px;
+  margin-bottom: 2px;
+}
+
+.instance-grid.small li {
+  width: 3px;
+  height: 3px;
+  margin-right: 2px;
+  margin-bottom: 2px;
+}
+
+.instance-grid.big li {
+  width: 15px;
+  height: 15px;
+  margin-right: 5px;
+  margin-bottom: 5px;
+  color: white;
+  text-align: center;
+}
+
+.instance-updated {
+  background-color: darkseagreen;
+  fill: rgba(143, 188, 143, 0.5);
+  stroke: darkseagreen;
+  stroke-width: 2;
+}
+
+.instance-skipped {
+  background-color: #6FDE6F;
+  fill: rgba(111,222,111, 0.5);
+  stroke: #6FDE6F;
+  stroke-width: 2;
+}
+
+.instance-updating, .instance-added, .instance-rolling-back {
+  background-color: khaki;
+  fill: rgba(240, 230, 140, 0.5);
+  stroke: khaki;
+  stroke-width: 2;
+}
+
+.instance-update-failed,
+.instance-rollback-failed {
+  background-color: indianred;
+  fill: rgba(205, 92, 92, 0.5);
+  stroke: indianred;
+  stroke-width: 2;
+}
+
+.instance-removed {
+  background-color: darkseagreen;
+  fill: rgba(0, 0, 0, 0.7);
+  stroke: black;
+  stroke-width: 2;
+}
+
+li.instance-removed {
+  border: 1px solid #444;
+}
+
+.instance-rolled-back {
+  fill: rgba(60, 26, 26, 0.5);
+}
+
+li.instance-rolled-back {
+  background-color: darkseagreen;
+  border: 1px solid indianred;
+}
+
+.instance-grid li.pending {
+  background-color: white;
+  border: 1px solid darkseagreen;
+}
+
+.instance-grid li.ignore {
+  background-color: #eee;
+}
+
+.instance-summary-title .instance-title {
+  font-size: 12px;
+  text-transform: uppercase;
+  font-weight: bold;
+}
+
+.instance-summary-title .instance-progress {
+  font-size: 12px;
+  font-weight: bold;
+  position: absolute;
+  right: 25px;
+}
+
+.update-time {
+  margin: 20px 0;
+  text-align: center;
+  text-transform: uppercase;
+}
+
+.update-time h4 {
+  font-size: 33px;
+  margin: 0;
+}
+
+.update-time .time-ago {
+  color: #999;
+}
+
+.time-divider {
+  text-align: center;
+  font-size: 70px;
+  font-weight: bold;
+  color: #ccc;
+}
+
+.time-display-duration {
+  text-transform: uppercase;
+  text-align: center;
+  color: #999;
+  margin-top: -20px;
+  margin-bottom: 20px;
+}
+
+.update-user {
+  text-transform: uppercase;
+  margin-top: -10px;
+  display: block;
+  color: #999;
+}
+
+.progress-details {
+  margin-bottom: 20px;
+}
+
+.in-progress-alert a {
+  font-size: 25px;
+  font-weight: bold;
+}
+
+.in-progress-alert progressbar {
+  margin-top: 10px;
+}
+
+.in-progress-alert span {
+  font-size: 11px;
+  text-transform: uppercase;
+}
+
+.progress {
+  margin-top: 10px;
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/c0a42f29/src/main/resources/org/apache/aurora/scheduler/http/ui/job.html
----------------------------------------------------------------------
diff --git a/src/main/resources/org/apache/aurora/scheduler/http/ui/job.html b/src/main/resources/org/apache/aurora/scheduler/http/ui/job.html
index e21dee0..14dce65 100644
--- a/src/main/resources/org/apache/aurora/scheduler/http/ui/job.html
+++ b/src/main/resources/org/apache/aurora/scheduler/http/ui/job.html
@@ -22,7 +22,6 @@
         <breadcrumb/>
       </div>
 
-      <div class='container-fluid'>
         <div class='page-header'>
           <h2 class='text-center'>
             Job <em>{{job}}</em> in role <em>{{role}}</em> and environment
<em>{{environment}}</em>
@@ -34,7 +33,24 @@
             </span>
           </h2>
         </div>
+
+      <!-- begin update progress preview -->
+      <div ng-if="updateInProgress" class="content-box in-progress-alert">
+        <div class="row">
+          <div class="col-md-4">
+            <a href="/scheduler/{{role}}/{{environment}}/{{job}}/{{updateInProgress.update.summary.updateId}}">Update
In Progress</a>
+          </div>
+          <div class="col-md-4">
+            <progressbar class="progress" max="updateStats.totalInstancesToBeUpdated"
value="updateStats.instancesUpdatedSoFar" type="success"><i>{{updateStats.instancesUpdatedSoFar}}
of {{updateStats.totalInstancesToBeUpdated}}</i></progressbar>
+          </div>
+          <div class="col-md-4">
+            <span>started by <strong>{{updateInProgress.update.summary.user}}</strong></span><br/>
+            <span>{{updateInProgress.update.summary.state.createdTimestampMs | toElapsedTime}}
ago</span>
+          </div>
+        </div>
       </div>
+      <!-- end update progress preview -->
+
 
       <div ng-controller='CronJobSummaryController'>
         <div ng-show='error'>
@@ -42,7 +58,6 @@
         </div>
 
         <div ng-hide='error'>
-          <div class='container-fluid'>
             <div ng-if='cronJobSummary'>
               <h3>Cron Job Summary</h3>
 
@@ -54,10 +69,10 @@
                 </smart-table>
               </div>
             </div>
-          </div>
         </div>
       </div>
 
+
       <tabset justified='true'>
         <tab heading='Active tasks ({{activeTasks.length}})' title='All Active tasks for
this job.'>
           <div class='task-tab'>
@@ -112,8 +127,44 @@
         </tab>
       </tabset>
 
-      <div class='container-fluid'>
-        <a class='bottomRight' ng-click='toggleTaskInfoLinkVisibility()'>π</a>
+      <div ng-if="updates" class="content-box">
+        <div class='row'>
+          <div class='col-md-12'>
+            <h3>Update History</h3>
+            <table class="table table-bordered table-striped table-hover">
+              <tr>
+                <th>id</th>
+                <th>status</th>
+                <th>started</th>
+                <th>ended</th>
+                <th>user</th>
+              </tr>
+              <tr ng-repeat="update in updates">
+                <td>
+                  <a href="/scheduler/{{role}}/{{environment}}/{{job}}/{{update.updateId}}">
 {{update.updateId}}
+                  </a>
+                </td>
+                <td>
+                  {{update.state.status | toNiceStatus}}
+                </td>
+                <td>
+                  {{update.state.createdTimestampMs | toElapsedTime}} ago
+                </td>
+                <td>
+                  {{update.state.lastModifiedTimestampMs | toElapsedTime}} ago
+                </td>
+                <td>
+                  {{update.user}}
+                </td>
+              </tr>
+            </table>
+          </div>
+        </div>
+      </div>
+
+        <div class="container-fluid">
+          <a class='bottomRight' ng-click='toggleTaskInfoLinkVisibility()'>π</a>
+        </div>
       </div>
     </div>
   </div>

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/c0a42f29/src/main/resources/org/apache/aurora/scheduler/http/ui/js/app.js
----------------------------------------------------------------------
diff --git a/src/main/resources/org/apache/aurora/scheduler/http/ui/js/app.js b/src/main/resources/org/apache/aurora/scheduler/http/ui/js/app.js
index fb3b5b1..082d920 100644
--- a/src/main/resources/org/apache/aurora/scheduler/http/ui/js/app.js
+++ b/src/main/resources/org/apache/aurora/scheduler/http/ui/js/app.js
@@ -32,6 +32,9 @@ var auroraUI;
     $routeProvider.when('/scheduler/:role/:environment/:job',
       {templateUrl: '/job.html', controller: 'JobController'});
 
+    $routeProvider.when('/scheduler/:role/:environment/:job/:update',
+      {templateUrl: '/update.html', controller: 'UpdateController'});
+
     $routeProvider.otherwise({redirectTo: function (location, path) {
       window.location.href = path;
     }});

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/c0a42f29/src/main/resources/org/apache/aurora/scheduler/http/ui/js/controllers.js
----------------------------------------------------------------------
diff --git a/src/main/resources/org/apache/aurora/scheduler/http/ui/js/controllers.js b/src/main/resources/org/apache/aurora/scheduler/http/ui/js/controllers.js
index 3477b7e..0884cc8 100644
--- a/src/main/resources/org/apache/aurora/scheduler/http/ui/js/controllers.js
+++ b/src/main/resources/org/apache/aurora/scheduler/http/ui/js/controllers.js
@@ -12,9 +12,11 @@
  * limitations under the License.
  */
 (function () {
-  /* global ScheduleStatus:false */
+  /* global ScheduleStatus:false, JobUpdateQuery:false, JobKey:false */
   'use strict';
 
+  var AURORA_UPDATE_POLL_MS = 10000;
+
   /* Controllers */
 
   var auroraUIControllers = angular.module('auroraUI.controllers', []);
@@ -226,8 +228,64 @@
     }
   );
 
+  auroraUIControllers.controller('UpdateController',
+    function ($scope, $routeParams, $timeout, auroraClient, updateUtil) {
+      var updateId = $routeParams.update;
+
+      $scope.role = $routeParams.role;
+      $scope.environment = $routeParams.environment;
+      $scope.job = $routeParams.job;
+
+      var getUpdateProgress = function () {
+        $scope.update = auroraClient.getJobUpdateDetails(updateId).details;
+        $scope.inProgress = updateUtil.isInProgress($scope.update.update.summary.state.status);
+
+        var duration = $scope.update.update.summary.state.lastModifiedTimestampMs -
+          $scope.update.update.summary.state.createdTimestampMs;
+
+        $scope.duration = moment.duration(duration).humanize();
+
+        $scope.stats = updateUtil.getUpdateStats($scope.update);
+        $scope.configJson = JSON
+          .stringify($scope.update.update.configuration.newTaskConfig, undefined, 2);
+
+        // pagination for instance events
+        var instanceEvents = $scope.instanceEvents = $scope.update.instanceEvents;
+        $scope.eventsPerPage = 10;
+        $scope.changeInstancePage = function () {
+          var start = ($scope.currentPage - 1) * $scope.eventsPerPage;
+          var end   = start + $scope.eventsPerPage;
+          $scope.instanceEvents = instanceEvents.slice(start, end);
+        };
+        $scope.totalEvents = instanceEvents.length;
+        $scope.currentPage = 1;
+        $scope.changeInstancePage();
+
+        // Instance summary display.
+        $scope.instanceSummary = updateUtil.fillInstanceSummary($scope.update, $scope.stats);
+
+        if ($scope.instanceSummary.length <= 20) {
+          $scope.instanceGridSize = 'big';
+        } else if ($scope.instanceSummary.length <= 1000) {
+          $scope.instanceGridSize = 'medium';
+        } else {
+          $scope.instanceGridSize = 'small';
+        }
+
+        // Poll for updates while this update is in progress.
+        if ($scope.inProgress) {
+          $timeout(function () {
+            getUpdateProgress();
+          }, AURORA_UPDATE_POLL_MS);
+        }
+      };
+
+      getUpdateProgress();
+    }
+  );
+
   auroraUIControllers.controller('JobController',
-    function ($scope, $routeParams, auroraClient, taskUtil) {
+    function ($scope, $routeParams, $timeout, auroraClient, taskUtil, updateUtil) {
       $scope.error = '';
 
       $scope.role = $routeParams.role;
@@ -330,6 +388,35 @@
         });
       }
 
+      function getUpdatesForJob($scope) {
+        var query = new JobUpdateQuery();
+        var jobKey = new JobKey();
+        jobKey.role = $scope.role;
+        jobKey.environment = $scope.environment;
+        jobKey.name = $scope.job;
+        query.jobKey = jobKey;
+
+        $scope.updates = auroraClient.getJobUpdateSummaries(query).summaries;
+
+        function getUpdateInProgress() {
+          if ($scope.updates.length > 0 &&
+             updateUtil.isInProgress($scope.updates[0].status)) {
+
+            $scope.updateInProgress =
+              auroraClient.getJobUpdateDetails($scope.updates[0].updateId).details;
+
+            $scope.updateStats = updateUtil.getUpdateStats($scope.updateInProgress);
+
+            // Poll for updates as long as this update is in progress.
+            $timeout(function () {
+              getUpdateInProgress();
+            }, AURORA_UPDATE_POLL_MS);
+          }
+        }
+
+        getUpdateInProgress();
+      }
+
       function getTasksForJob(role, environment, job) {
         var response = auroraClient.getTasksWithoutConfigs(role, environment, job);
 
@@ -341,6 +428,7 @@
         $scope.jobDashboardUrl = getJobDashboardUrl(response.statsUrlPrefix);
 
         buildGroupSummary($scope);
+        getUpdatesForJob($scope);
 
         var tasks = _.map(response.tasks, function (task) {
           return summarizeTask(task);

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/c0a42f29/src/main/resources/org/apache/aurora/scheduler/http/ui/js/directives.js
----------------------------------------------------------------------
diff --git a/src/main/resources/org/apache/aurora/scheduler/http/ui/js/directives.js b/src/main/resources/org/apache/aurora/scheduler/http/ui/js/directives.js
index 7f05a55..8b7fc06 100644
--- a/src/main/resources/org/apache/aurora/scheduler/http/ui/js/directives.js
+++ b/src/main/resources/org/apache/aurora/scheduler/http/ui/js/directives.js
@@ -128,4 +128,54 @@
       replace: true
     };
   });
+
+  auroraUI.directive('timeDisplay', function () {
+    return {
+      restrict: 'E',
+      scope: {
+        'timestamp': '='
+      },
+      templateUrl: '/timeDisplay.html'
+    };
+  });
+
+  auroraUI.directive('updateSettings', function () {
+    return {
+      restrict: 'E',
+      scope: {
+        'update': '='
+      },
+      templateUrl: '/updateSettings.html'
+    };
+  });
+
+  auroraUI.directive('instanceSummary', function ($compile) {
+    return {
+      restrict: 'E',
+      scope: {
+        'instances': '=',
+        'size': '=',
+        'stats': '='
+      },
+      link: function (scope, element, attrs) {
+        var list = angular.element('<ul class="instance-grid ' + scope.size + '"></ul>');
+
+        scope.instances.forEach(function (i, n) {
+          list.append('<li class="' + i.className + '" tooltip="INSTANCE ' + n +
+            ': ' + i.className.toUpperCase() + '"><span class="instance-id">' +
n +
+            '</span></li>');
+        });
+
+        var title = angular.element('<div class="instance-summary-title"></div>');
+        title.append('<span class="instance-title">Instance Status</span>');
+        title.append('<span class="instance-progress">' + scope.stats.instancesUpdatedSoFar
+
+          ' / ' + scope.stats.totalInstancesToBeUpdated + ' (' + scope.stats.progress +
+          '%)<div>');
+
+        element.append(title);
+        element.append(list);
+        $compile(list)(scope);
+      }
+    };
+  });
 })();

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/c0a42f29/src/main/resources/org/apache/aurora/scheduler/http/ui/js/filters.js
----------------------------------------------------------------------
diff --git a/src/main/resources/org/apache/aurora/scheduler/http/ui/js/filters.js b/src/main/resources/org/apache/aurora/scheduler/http/ui/js/filters.js
index 76f48f8..7e8ca84 100644
--- a/src/main/resources/org/apache/aurora/scheduler/http/ui/js/filters.js
+++ b/src/main/resources/org/apache/aurora/scheduler/http/ui/js/filters.js
@@ -12,7 +12,7 @@
  * limitations under the License.
  */
 (function () {
-  /* global auroraUI:false */
+  /* global auroraUI:false, JobUpdateStatus: false, JobUpdateAction: false */
   'use strict';
 
   auroraUI.filter('scheduleStatusTooltip', function () {
@@ -44,6 +44,42 @@
     };
   });
 
+  auroraUI.filter('toNiceStatus', function () {
+    var updateStatusLookup = _.invert(JobUpdateStatus);
+
+    var STATUS_MAP = {
+      'ROLLED_FORWARD': 'SUCCESS',
+      'ROLLING_FORWARD': 'IN PROGRESS',
+      'ROLLING_BACK': 'ROLLING BACK',
+      'ROLL_BACK_PAUSED': 'ROLL BACK PAUSED',
+      'ROLL_FORWARD_PAUSED': 'PAUSED',
+      'ROLLED_BACK': 'ROLLED BACK'
+    };
+
+    return function (status) {
+      status = updateStatusLookup[status] || 'UNKNOWN';
+      return STATUS_MAP.hasOwnProperty(status) ? STATUS_MAP[status] : status;
+    };
+  });
+
+  auroraUI.filter('toNiceAction', function () {
+    var instanceActionLookup = _.invert(JobUpdateAction);
+
+    return function (action) {
+      return (instanceActionLookup[action] || 'UNKNOWN')
+        .replace(/INSTANCE_/, '')
+        .replace(/_/g, ' ');
+    };
+  });
+
+  auroraUI.filter('toNiceRanges', function () {
+    return function (ranges) {
+      return ranges.map(function (range) {
+        return range.first + '-' + range.last;
+      }).join(', ');
+    };
+  });
+
   auroraUI.filter('scaleMb', function () {
     var SCALE = ['MiB', 'GiB', 'TiB', 'PiB', 'EiB'];
 
@@ -72,7 +108,7 @@
 
   auroraUI.filter('toUtcTime', function () {
     return function (timestamp, timezone) {
-      return moment(timestamp).utc().format('MM/DD h:mm:ss') + ' UTC';
+      return moment(timestamp).utc().format('MM/DD HH:mm:ss') + ' UTC';
     };
   });
 
@@ -81,4 +117,17 @@
       return moment(timestamp).format('MM/DD HH:mm:ss') + ' LOCAL';
     };
   });
+
+  auroraUI.filter('toLocalDay', function () {
+    return function (timestamp) {
+      return moment(timestamp).format('ddd, MMM Do');
+    };
+  });
+
+  auroraUI.filter('toLocalTimeOnly', function () {
+    return function (timestamp) {
+      return moment(timestamp).format('HH:mm');
+    };
+  });
+
 })();

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/c0a42f29/src/main/resources/org/apache/aurora/scheduler/http/ui/js/services.js
----------------------------------------------------------------------
diff --git a/src/main/resources/org/apache/aurora/scheduler/http/ui/js/services.js b/src/main/resources/org/apache/aurora/scheduler/http/ui/js/services.js
index e898f12..c80146a 100644
--- a/src/main/resources/org/apache/aurora/scheduler/http/ui/js/services.js
+++ b/src/main/resources/org/apache/aurora/scheduler/http/ui/js/services.js
@@ -14,7 +14,8 @@
 (function () {
   /* global auroraUI:false, Identity:false, TaskQuery:false, ReadOnlySchedulerClient:false,
             ACTIVE_STATES:false, CronCollisionPolicy: false, JobKey: false,
-            ScheduleStatus:false */
+            ScheduleStatus: false, JobUpdateQuery:false, JobUpdateAction:false,
+            JobUpdateStatus: false */
   'use strict';
 
   function makeJobTaskQuery(role, environment, jobName) {
@@ -121,6 +122,23 @@
             return result;
           },
 
+          getJobUpdateSummaries: function (query) {
+            query = query || new JobUpdateQuery();
+            var response = auroraClient.getSchedulerClient().getJobUpdateSummaries(query);
+            var result = auroraClient.processResponse(response);
+            result.summaries = response.result !== null ?
+              response.result.getJobUpdateSummariesResult.updateSummaries : [];
+            return result;
+          },
+
+          getJobUpdateDetails: function (id) {
+            var response = auroraClient.getSchedulerClient().getJobUpdateDetails(id);
+            var result = auroraClient.processResponse(response);
+            result.details = response.result !== null ?
+              response.result.getJobUpdateDetailsResult.details : {};
+            return result;
+          },
+
           // Utility functions
           // TODO(Suman Karumuri): Make schedulerClient a service
           schedulerClient: null,
@@ -167,6 +185,203 @@
     ]);
 
   auroraUI.factory(
+    'updateUtil',
+    function () {
+      function toSet(values) {
+        var tmp = {};
+        values.forEach(function (key) {
+          tmp[key] = true;
+        });
+        return tmp;
+      }
+
+      var UPDATE_TERMINAL = toSet([
+        JobUpdateStatus.ROLLED_FORWARD,
+        JobUpdateStatus.ROLLED_BACK,
+        JobUpdateStatus.ABORTED,
+        JobUpdateStatus.ERROR
+      ]);
+
+      var INSTANCE_SUCCESSFUL = toSet([
+        JobUpdateAction.INSTANCE_UPDATED,
+        JobUpdateAction.INSTANCE_SKIPPED,
+        JobUpdateAction.INSTANCE_REMOVED
+      ]);
+
+      var INSTANCE_TERMINAL = toSet([
+        JobUpdateAction.INSTANCE_UPDATED,
+        JobUpdateAction.INSTANCE_SKIPPED,
+        JobUpdateAction.INSTANCE_REMOVED,
+        JobUpdateAction.INSTANCE_ROLLED_BACK,
+        JobUpdateAction.INSTANCE_UPDATE_FAILED,
+        JobUpdateAction.INSTANCE_ROLLBACK_FAILED
+      ]);
+
+      var instanceActionLookup = _.invert(JobUpdateAction);
+
+      var updateUtil = {
+        isTerminal: function (status) {
+          return UPDATE_TERMINAL.hasOwnProperty(status);
+        },
+        isInProgress: function (status) {
+          return ! updateUtil.isTerminal(status);
+        },
+        isInstanceSuccessful: function (action) {
+          return INSTANCE_SUCCESSFUL.hasOwnProperty(action);
+        },
+        isInstanceTerminal: function (action) {
+          return INSTANCE_TERMINAL.hasOwnProperty(action);
+        },
+        instanceCountFromRanges: function (ranges) {
+          // add the deltas of remaining ranges
+          // note - we don't check for overlapping ranges here
+          // because that would be a bug in the scheduler
+          var instanceCount = 0;
+
+          ranges.forEach(function (r) {
+            instanceCount += (r.last - r.first + 1);
+          });
+
+          return instanceCount;
+        },
+        instanceCountFromConfigs: function (instanceTaskConfigs) {
+          var flattenedRanges = [];
+
+          // get all ranges
+          instanceTaskConfigs.forEach(function (iTaskConfig) {
+            iTaskConfig.instances.forEach(function (range) {
+              flattenedRanges.push(range);
+            });
+          });
+
+          return updateUtil.instanceCountFromRanges(flattenedRanges);
+        },
+        progressFromEvents: function (instanceEvents) {
+          var successful = updateUtil.getLatestInstanceEvents(instanceEvents, function (e)
{
+            return updateUtil.isInstanceSuccessful(e.action);
+          });
+          return Object.keys(successful).length;
+        },
+        displayClassForInstanceStatus: function (action) {
+          return instanceActionLookup[action].toLowerCase().replace(/_/g, '-');
+        },
+        getLatestInstanceEvents: function (instanceEvents, condition) {
+          var events = _.sortBy(instanceEvents, 'timestampMs');
+          var instanceMap = {};
+          condition = condition || function () { return true; };
+
+          for (var i = events.length - 1; i >= 0; i--) {
+            if (!instanceMap.hasOwnProperty(events[i].instanceId) && condition(events[i]))
{
+              instanceMap[events[i].instanceId] = events[i];
+            }
+          }
+
+          return instanceMap;
+        },
+        fillInstanceSummary: function (details, stats) {
+          // get latest event for each instance
+          var instanceMap = updateUtil.getLatestInstanceEvents(details.instanceEvents);
+
+          // total number of instances to show is the max between
+          // new instance count and old instance count
+          var totalInstances = Math.max(
+              stats.oldInstanceCount,
+              details.update.configuration.instanceCount
+            );
+
+          var instances = [];
+          var instanceSubset = details.update.configuration.settings.updateOnlyTheseInstances;
+
+          function inRanges(ranges, x) {
+            if (ranges && x) {
+              for (var i = 0; i < ranges.length; i++) {
+                if (x >= ranges[i].first && x <= ranges[i].last) {
+                  return true;
+                }
+              }
+            }
+            return false;
+          }
+
+          for (var i = 0; i < totalInstances; i++) {
+            if (instanceMap.hasOwnProperty(i)) {
+              var event = instanceMap[i];
+              var className = updateUtil.displayClassForInstanceStatus(event.action);
+              instances.push({
+                instanceId: i,
+                className: className,
+                event: event
+              });
+            } else if (instanceSubset && !inRanges(instanceSubset, i)) {
+              // If they have declared a subset of instances to update
+              // AND this instance isn't part of that subset, it will be ignored.
+              instances.push({
+                instanceId: i,
+                className: 'ignore'
+              });
+            } else {
+              // Otherwise it is pending an update.
+              instances.push({
+                instanceId: i,
+                className: 'pending'
+              });
+            }
+          }
+          return instances;
+        },
+        getUpdateStats: function (details) {
+          if (!details || !details.update) {
+            return {};
+          }
+
+          // find number of instances to be updated
+          var newInstanceCount = details.update.configuration.instanceCount;
+          var updateSubset = false;
+
+          // find total number of existing instances
+          var oldInstanceCount = updateUtil.instanceCountFromConfigs(
+            details.update.configuration.oldTaskConfigs);
+
+          // max of those two numbers is the number of instances to be updated
+          var totalInstancesToBeUpdated = Math.max(oldInstanceCount, newInstanceCount);
+
+          if (details.update.configuration.settings.updateOnlyTheseInstances) {
+            newInstanceCount = updateUtil.instanceCountFromRanges(
+              details.update.configuration.settings.updateOnlyTheseInstances
+            );
+            updateSubset = true;
+            totalInstancesToBeUpdated = newInstanceCount;
+          }
+
+          // if necessary, differentiate between number of instances updated
+          // and number of instances that will be discarded
+          var instancesToBeUpdated = newInstanceCount;
+          var instancesToBeDiscarded = 0;
+          if (!updateSubset && (oldInstanceCount > newInstanceCount)) {
+            instancesToBeDiscarded = oldInstanceCount - newInstanceCount;
+          }
+
+          var instancesUpdated = updateUtil.progressFromEvents(details.instanceEvents);
+
+          // calculate the percentage of work done so far
+          var progress = Math.round((instancesUpdated / totalInstancesToBeUpdated) * 100);
+
+          return {
+            onlySubset: updateSubset,
+            oldInstanceCount: oldInstanceCount,
+            totalInstancesToBeUpdated: totalInstancesToBeUpdated,
+            instancesToBeUpdated: instancesToBeUpdated,
+            instancesToBeDiscarded: instancesToBeDiscarded,
+            instancesUpdatedSoFar: instancesUpdated,
+            progress: progress
+          };
+        }
+      };
+
+      return updateUtil;
+    });
+
+  auroraUI.factory(
     'taskUtil',
     function () {
       var taskUtil = {

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/c0a42f29/src/main/resources/org/apache/aurora/scheduler/http/ui/timeDisplay.html
----------------------------------------------------------------------
diff --git a/src/main/resources/org/apache/aurora/scheduler/http/ui/timeDisplay.html b/src/main/resources/org/apache/aurora/scheduler/http/ui/timeDisplay.html
new file mode 100644
index 0000000..08574cc
--- /dev/null
+++ b/src/main/resources/org/apache/aurora/scheduler/http/ui/timeDisplay.html
@@ -0,0 +1,19 @@
+<!--
+ Licensed 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="update-time">
+  <span>{{timestamp | toLocalDay}}</span>
+  <h4 tooltip="{{timestamp | toUtcTime}}">{{timestamp | toLocalTimeOnly}}</h4>
+
+  <span class="time-ago">{{timestamp | toElapsedTime}} ago</span>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/c0a42f29/src/main/resources/org/apache/aurora/scheduler/http/ui/update.html
----------------------------------------------------------------------
diff --git a/src/main/resources/org/apache/aurora/scheduler/http/ui/update.html b/src/main/resources/org/apache/aurora/scheduler/http/ui/update.html
new file mode 100644
index 0000000..b6cf4f0
--- /dev/null
+++ b/src/main/resources/org/apache/aurora/scheduler/http/ui/update.html
@@ -0,0 +1,130 @@
+<!--
+ Licensed 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="container-fluid">
+  <div ng-show='error'>
+    <error/>
+  </div>
+
+  <div ng-hide='error'>
+    <div>
+      <breadcrumb/>
+    </div>
+
+    <div class="content-box">
+      <div class="row">
+        <!-- begin finished update box -->
+        <div ng-if="!inProgress" class="col-md-6 finished-update">
+
+          <div class="text-center"><h3>Update Summary</h3></div>
+
+          <div class="row">
+            <div class="col-md-12 text-center">
+              <span>started by <strong>{{update.update.summary.user}}</strong></span><br/>
+              <span>final status <strong>{{update.update.summary.state.status
| toNiceStatus}}</strong></span>
+            </div>
+          </div>
+
+          <div class="row">
+            <div class="col-md-3 col-md-offset-2">
+              <time-display timestamp="update.update.summary.state.createdTimestampMs"/>
+            </div>
+            <div class="col-md-2 time-divider">
+              <span>~</span>
+            </div>
+            <div class="col-md-3">
+              <time-display timestamp="update.update.summary.state.lastModifiedTimestampMs"/>
+            </div>
+          </div>
+          <div class="row">
+            <div class="col-md-12">
+              <div class="time-display-duration">
+                 Duration: {{duration}}
+              </div>
+            </div>
+          </div>
+
+          <instance-summary instances="instanceSummary" stats="stats" size="instanceGridSize"></instance-summary>
+        </div>
+        <!-- end finished update box -->
+
+        <!-- begin update in progress box -->
+        <div ng-if="inProgress" class="col-md-6 update-settings">
+
+          <h3 ng-if="inProgress">Update Status</h3>
+
+          <div class="progress-details">
+          <span>started by <strong>{{update.update.summary.user}}</strong>,
<span tooltip="{{update.update.summary.state.createdTimestampMs | toLocalTime}}">{{update.update.summary.state.createdTimestampMs
| toElapsedTime}} ago</span></span><br/>
+          <span>current status <strong>{{update.update.summary.state.status |
toNiceStatus}}</strong></span>
+          </div>
+
+          <instance-summary instances="instanceSummary" stats="stats" size="instanceGridSize"></instance-summary>
+
+        </div>
+        <!-- end update in progress box -->
+
+        <div class="col-md-6 update-settings">
+          <h3>Update Settings</h3>
+
+          <update-settings update="update"></update-settings>
+        </div>
+      </div>
+
+      <hr/>
+
+      <div class="row">
+        <div class="col-md-6">
+          <h4>Update Events</h4>
+          <table class="table table-bordered table-striped">
+            <tr>
+              <th>event</th>
+              <th>time</th>
+            </tr>
+            <tr ng-repeat="e in update.updateEvents">
+              <td>{{e.status | toNiceStatus}}</td>
+              <td>{{e.timestampMs | toElapsedTime }} ago</td>
+            </tr>
+          </table>
+        </div>
+
+        <div class="col-md-6">
+          <h4>Instance Events</h4>
+          <table class="table table-bordered table-striped">
+          <tr>
+            <th>instance</th>
+            <th>event</th>
+            <th>time</th>
+          </tr>
+          <tr ng-repeat="e in instanceEvents">
+            <td>{{e.instanceId}}</td>
+            <td>{{e.action | toNiceAction}}</td>
+            <td>{{e.timestampMs | toElapsedTime}} ago</td>
+          </tr>
+          </table>
+
+            <pagination ng-change="changeInstancePage()" max-size="5" total-items="totalEvents"
ng-model="currentPage" items-per-page="eventsPerPage"></pagination>
+        </div>
+      </div>
+    </div>
+
+    <div class="content-box">
+      <div class="row">
+        <div class="col-md-12">
+          <h3>Job Configuration</h3>
+          <pre>{{configJson}}</pre>
+        </div>
+      </div>
+    </div>
+
+  </div>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/c0a42f29/src/main/resources/org/apache/aurora/scheduler/http/ui/updateSettings.html
----------------------------------------------------------------------
diff --git a/src/main/resources/org/apache/aurora/scheduler/http/ui/updateSettings.html b/src/main/resources/org/apache/aurora/scheduler/http/ui/updateSettings.html
new file mode 100644
index 0000000..613b532
--- /dev/null
+++ b/src/main/resources/org/apache/aurora/scheduler/http/ui/updateSettings.html
@@ -0,0 +1,89 @@
+<!--
+ Licensed 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.
+ -->
+<table class="table">
+  <tr>
+    <th>setting</th>
+    <th>value</th>
+  </tr>
+  <tr>
+    <td>
+      <span class="setting-label" tooltip="Max number of instances being updated at any
given moment.">
+        Update Group Size
+      </span>
+    </td>
+    <td>
+      <strong>{{update.update.configuration.settings.updateGroupSize}}</strong>
+    </td>
+  </tr>
+  <tr>
+    <td>
+      <span class="setting-label" tooltip="Max number of instance failures to tolerate
before marking instance as FAILED.">
+        Max Failures Per Instance
+      </span>
+    </td>
+    <td>
+      <strong>{{update.update.configuration.settings.maxPerInstanceFailures}}</strong>
+    </td>
+  </tr>
+  <tr>
+    <td>
+      <span class="setting-label" tooltip="Max number of FAILED instances to tolerate
before terminating the update.">
+        Max Failed Instances
+      </span>
+    </td>
+    <td>
+      <strong>{{update.update.configuration.settings.maxFailedInstances}}</strong>
+    </td>
+  </tr>
+  <tr>
+    <td>
+      <span class="setting-label" tooltip="Max time in ms to wait until an instance reaches
RUNNING state.">
+        Max Waiting Time For Running
+      </span>
+    </td>
+    <td>
+      <strong>{{update.update.configuration.settings.maxWaitToInstanceRunningMs}}</strong>
+    </td>
+  </tr>
+  <tr>
+    <td>
+      <span class="setting-label" tooltip="Min time to watch a RUNNING instance.">
+        Min Waiting Time In Running
+      </span>
+    </td>
+    <td>
+      <strong>{{update.update.configuration.settings.minWaitInInstanceRunningMs}}</strong>
+    </td>
+  </tr>
+  <tr>
+    <td>
+      <span class="setting-label" tooltip="If true, enables failed update rollback.">
+        Rollback on Failure
+      </span>
+    </td>
+    <td>
+      <strong>{{update.update.configuration.settings.rollbackOnFailure}}</strong>
+    </td>
+  </tr>
+  <tr ng-if="update.update.configuration.settings.updateOnlyTheseInstances">
+    <td>
+      <span class="setting-label" tooltip="A subset of instance IDs to act on. All instances
will be affected if this is not set.">
+        Instance IDs
+      </span>
+    </td>
+    <td>
+      <strong>{{update.update.configuration.settings.updateOnlyTheseInstances | toNiceRanges}}</strong>
+    </td>
+  </tr>
+</table>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-aurora/blob/c0a42f29/src/main/thrift/org/apache/aurora/gen/api.thrift
----------------------------------------------------------------------
diff --git a/src/main/thrift/org/apache/aurora/gen/api.thrift b/src/main/thrift/org/apache/aurora/gen/api.thrift
index a6dc548..e7770e6 100644
--- a/src/main/thrift/org/apache/aurora/gen/api.thrift
+++ b/src/main/thrift/org/apache/aurora/gen/api.thrift
@@ -574,13 +574,13 @@ struct JobUpdateSettings {
   /** Max number of instance failures to tolerate before marking instance as FAILED. */
   2: i32 maxPerInstanceFailures
 
-  /** Max number of FAILED instances to tolerate before terminating the forward roll. */
+  /** Max number of FAILED instances to tolerate before terminating the update. */
   3: i32 maxFailedInstances
 
   /** Max time to wait until an instance reaches RUNNING state. */
   4: i32 maxWaitToInstanceRunningMs
 
-  /** Min time to watch to watch a RUNNING instance. */
+  /** Min time to watch a RUNNING instance. */
   5: i32 minWaitInInstanceRunningMs
 
   /** If true, enables failed update rollback. */


Mime
View raw message