aurora-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From jco...@apache.org
Subject aurora git commit: Add a new UI page to show all tasks (active and completed) for a specific instance id.
Date Tue, 18 Aug 2015 21:24:05 GMT
Repository: aurora
Updated Branches:
  refs/heads/master 22f9cbb7e -> 9b8868fb7


Add a new UI page to show all tasks (active and completed) for a specific instance id.

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


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

Branch: refs/heads/master
Commit: 9b8868fb7a6e5bf131926e59b5023497e9b7db4f
Parents: 22f9cbb
Author: Joshua Cohen <jcohen@apache.org>
Authored: Tue Aug 18 16:23:42 2015 -0500
Committer: Joshua Cohen <jcohen@apache.org>
Committed: Tue Aug 18 16:23:42 2015 -0500

----------------------------------------------------------------------
 src/main/python/apache/aurora/client/base.py    |   2 +-
 .../resources/scheduler/assets/breadcrumb.html  |   5 +-
 src/main/resources/scheduler/assets/css/app.css |   4 +-
 .../resources/scheduler/assets/instance.html    | 106 ++++++
 src/main/resources/scheduler/assets/job.html    |   4 +-
 src/main/resources/scheduler/assets/js/app.js   |   5 +-
 .../scheduler/assets/js/controllers.js          | 346 +++++++------------
 .../resources/scheduler/assets/js/services.js   | 183 +++++++++-
 .../scheduler/assets/taskInstance.html          |  14 +
 .../apache/aurora/client/cli/test_supdate.py    |   6 +-
 .../python/apache/aurora/client/test_base.py    |  22 ++
 11 files changed, 472 insertions(+), 225 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/aurora/blob/9b8868fb/src/main/python/apache/aurora/client/base.py
----------------------------------------------------------------------
diff --git a/src/main/python/apache/aurora/client/base.py b/src/main/python/apache/aurora/client/base.py
index dee4c28..487f8e7 100644
--- a/src/main/python/apache/aurora/client/base.py
+++ b/src/main/python/apache/aurora/client/base.py
@@ -191,7 +191,7 @@ def synthesize_url(scheduler_url, role=None, env=None, job=None, update_id=None)
       if job:
         scheduler_url += '/' + job
         if update_id:
-          scheduler_url += '/' + update_id
+          scheduler_url += '/update/' + update_id
   return scheduler_url
 
 

http://git-wip-us.apache.org/repos/asf/aurora/blob/9b8868fb/src/main/resources/scheduler/assets/breadcrumb.html
----------------------------------------------------------------------
diff --git a/src/main/resources/scheduler/assets/breadcrumb.html b/src/main/resources/scheduler/assets/breadcrumb.html
index 1314793..9265277 100644
--- a/src/main/resources/scheduler/assets/breadcrumb.html
+++ b/src/main/resources/scheduler/assets/breadcrumb.html
@@ -24,10 +24,11 @@
       <a href='/scheduler/{{role}}/{{environment}}'>Environment: {{environment}}</a>
     </li>
 
-    <li ng-if='job && !update' class='active'>Job: {{job}}</li>
+    <li ng-if='job && (!update && !(instance >= 0))' class='active'>Job:
{{job}}</li>
 
-    <li ng-if='job && update'><a href='/scheduler/{{role}}/{{environment}}/{{job}}'>Job:
{{job}}</a></li>
+    <li ng-if='job && (update || instance >= 0)'><a href='/scheduler/{{role}}/{{environment}}/{{job}}'>Job:
{{job}}</a></li>
 
+    <li ng-if='instance >= 0' class='active'>Instance: {{instance}}</li>
     <li ng-if='update' class='active'>Update: {{update.update.summary.key.id}}</li>
   </ul>
   </div>

http://git-wip-us.apache.org/repos/asf/aurora/blob/9b8868fb/src/main/resources/scheduler/assets/css/app.css
----------------------------------------------------------------------
diff --git a/src/main/resources/scheduler/assets/css/app.css b/src/main/resources/scheduler/assets/css/app.css
index a4735ef..9437c53 100644
--- a/src/main/resources/scheduler/assets/css/app.css
+++ b/src/main/resources/scheduler/assets/css/app.css
@@ -441,6 +441,7 @@ li.instance-rolled-back {
     -webkit-transform: rotate(360deg);
   }
 }
+
 @keyframes spin {
   0% {
     transform: rotate(0deg);
@@ -454,7 +455,8 @@ li.instance-rolled-back {
   text-align: center;
 }
 
-.loading span {
+.loading span,
+span.loading {
   -webkit-animation: spin 1.1s infinite linear;
   animation: spin 1.1s infinite linear;
 }
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/aurora/blob/9b8868fb/src/main/resources/scheduler/assets/instance.html
----------------------------------------------------------------------
diff --git a/src/main/resources/scheduler/assets/instance.html b/src/main/resources/scheduler/assets/instance.html
new file mode 100644
index 0000000..317e2ce
--- /dev/null
+++ b/src/main/resources/scheduler/assets/instance.html
@@ -0,0 +1,106 @@
+<div class='container-fluid'>
+  <!--
+   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 ng-show='error'>
+    <error></error>
+  </div>
+
+  <div ng-hide='error'>
+    <breadcrumb></breadcrumb>
+
+    <div class='row'>
+      <div class='col-md-12'>
+        <div class='page-header'>
+          <h2 class='text-center'>
+            Instance <em>{{instance}}</em> of job <em>{{job}}</em>
in role <em>{{role}}</em> and
+            environment <em>{{environment}}</em>
+          </h2>
+        </div>
+      </div>
+    </div>
+
+    <div ng-if="!tasksReady">
+      <div class="row">
+        <div class="col-md-12">
+          Loading instance information.
+          <span class="glyphicon glyphicon-refresh loading" aria-hidden="true"></span>
+        </div>
+      </div>
+    </div>
+
+    <div ng-if="tasksReady">
+      <div ng-if="activeTasks.length === 0">
+        <h3>No Active Tasks</h3>
+      </div>
+      <div ng-if="activeTasks.length > 0">
+        <h3>Active Task</h3>
+        <div class="row">
+          <div class="col-md-6">
+            <h4>Task Details</h4>
+            <table class="table table-bordered table-striped">
+              <tbody>
+                <tr>
+                  <td><strong>Current Status</strong></td>
+                  <td>{{activeTasks[0].status}}</td>
+                </tr>
+                <tr>
+                  <td><strong>Task ID</strong></td>
+                  <td><a ng-href="/structdump/task/{{activeTasks[0].taskId}}">{{activeTasks[0].taskId}}</a></td>
+                </tr>
+                <tr>
+                  <td><strong>Host</strong></td>
+                  <td><a ng-href="http://{{activeTasks[0].host}}:1338/task/{{activeTasks[0].taskId}}">{{activeTasks[0].host}}</a></td>
+                </tr>
+              </tbody>
+            </table>
+          </div>
+
+          <div class="col-md-6">
+            <h4>Status History</h4>
+            <table class="table table-bordered table-striped">
+              <thead>
+                <tr>
+                  <th>Status</th>
+                  <th>Timestamp</th>
+                  <th>Message</th>
+                </tr>
+              </thead>
+              <tbody>
+                <tr ng-repeat="e in activeTasks[0].taskEvents">
+                  <td>{{e.status}}</td>
+                  <td>{{e.timestamp | toUtcTime}}</td>
+                  <td>{{e.message}}</td>
+                </tr>
+              </tbody>
+            </table>
+          </div>
+        </div>
+      </div>
+
+      <div ng-if="completedTasks.length === 0">
+        <h3>No Completed Tasks</h3>
+      </div>
+      <div ng-if="completedTasks.length > 0">
+        <h3>Completed Tasks</h3>
+        <div class='container-fluid'>
+          <smart-table config='completedTasksTableConfig'
+                       columns='completedTasksTableColumns'
+                       rows='completedTasks'
+                       class='table table-striped table-hover table-bordered table-condensed'>
+          </smart-table>
+        </div>
+      </div>
+    </div>
+  </div>
+</div>

http://git-wip-us.apache.org/repos/asf/aurora/blob/9b8868fb/src/main/resources/scheduler/assets/job.html
----------------------------------------------------------------------
diff --git a/src/main/resources/scheduler/assets/job.html b/src/main/resources/scheduler/assets/job.html
index 942635a..bfe51ab 100644
--- a/src/main/resources/scheduler/assets/job.html
+++ b/src/main/resources/scheduler/assets/job.html
@@ -39,7 +39,7 @@
     <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.key.id}}">Update
In Progress</a>
+          <a href="/scheduler/{{role}}/{{environment}}/{{job}}/update/{{updateInProgress.update.summary.key.id}}">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>
@@ -146,7 +146,7 @@
             </tr>
             <tr ng-repeat="update in updates">
               <td>
-                <a href="/scheduler/{{role}}/{{environment}}/{{job}}/{{update.key.id}}">
+                <a href="/scheduler/{{role}}/{{environment}}/{{job}}/update/{{update.key.id}}">
                   {{update.key.id}}
                 </a>
               </td>

http://git-wip-us.apache.org/repos/asf/aurora/blob/9b8868fb/src/main/resources/scheduler/assets/js/app.js
----------------------------------------------------------------------
diff --git a/src/main/resources/scheduler/assets/js/app.js b/src/main/resources/scheduler/assets/js/app.js
index b66409f..310aa35 100644
--- a/src/main/resources/scheduler/assets/js/app.js
+++ b/src/main/resources/scheduler/assets/js/app.js
@@ -32,7 +32,10 @@ var auroraUI;
     $routeProvider.when('/scheduler/:role/:environment/:job',
       {templateUrl: '/assets/job.html', controller: 'JobController'});
 
-    $routeProvider.when('/scheduler/:role/:environment/:job/:update',
+    $routeProvider.when('/scheduler/:role/:environment/:job/:instance',
+      {templateUrl: '/assets/instance.html', controller: 'JobInstanceController'});
+
+    $routeProvider.when('/scheduler/:role/:environment/:job/update/:update',
       {templateUrl: '/assets/update.html', controller: 'UpdateController'});
 
     $routeProvider.when('/updates',

http://git-wip-us.apache.org/repos/asf/aurora/blob/9b8868fb/src/main/resources/scheduler/assets/js/controllers.js
----------------------------------------------------------------------
diff --git a/src/main/resources/scheduler/assets/js/controllers.js b/src/main/resources/scheduler/assets/js/controllers.js
index 04ea1cb..9892019 100644
--- a/src/main/resources/scheduler/assets/js/controllers.js
+++ b/src/main/resources/scheduler/assets/js/controllers.js
@@ -12,7 +12,7 @@
  * limitations under the License.
  */
 (function () {
-  /* global ScheduleStatus:false, JobUpdateKey:false, JobUpdateQuery:false, JobKey:false
*/
+  /* global JobUpdateKey:false, JobUpdateQuery:false, JobKey:false */
   'use strict';
 
   /* Controllers */
@@ -330,239 +330,159 @@
     }
   );
 
-  auroraUIControllers.controller('JobController',
-    function ($scope, $routeParams, $timeout, $q, auroraClient, taskUtil, updateUtil) {
-      $scope.error = '';
-
-      $scope.role = $routeParams.role;
-      $scope.environment = $routeParams.environment;
-      $scope.job = $routeParams.job;
-
-      var taskTableConfig = {
-        isGlobalSearchActivated: false,
-        isPaginationEnabled: true,
-        itemsByPage: 50,
-        maxSize: 8,
-        selectionMode: 'single'
-      };
-
-      $scope.activeTasksTableConfig = taskTableConfig;
-      $scope.completedTasksTableConfig = taskTableConfig;
-
-      var taskColumns = [
-        {label: 'Instance', map: 'instanceId'},
-        {label: 'Status', map: 'status', cellTemplateUrl: '/assets/taskStatus.html'},
-        {label: 'Host', map: 'host', cellTemplateUrl: '/assets/taskSandbox.html'}
-      ];
-
-      var completedTaskColumns = addColumn(2,
-        taskColumns,
-        {label: 'Running duration',
-          map: 'duration',
-          formatFunction: function (duration) {
-            return moment.duration(duration).humanize();
-          }
-        });
-
-      var taskIdColumn = {
-        label: 'Task ID',
-        map: 'taskId',
-        cellTemplateUrl: '/assets/taskLink.html'
-      };
-
-      $scope.activeTasksTableColumns = taskColumns;
-
-      $scope.completedTasksTableColumns = completedTaskColumns;
-
-      function addColumn(idxPosition, currentColumns, newColumn) {
-        return _.union(
-          _.first(currentColumns, idxPosition),
-          [newColumn],
-          _.last(currentColumns, currentColumns.length - idxPosition));
+  function initializeJobController($scope, $routeParams) {
+    $scope.error = '';
+
+    $scope.role = $routeParams.role;
+    $scope.environment = $routeParams.environment;
+    $scope.job = $routeParams.job;
+    $scope.instance = $routeParams.instance;
+
+    // These two task arrays need to be initialized due to a quirk in SmartTable's behavior.
+    $scope.activeTasks = [];
+    $scope.completedTasks = [];
+    $scope.tasksReady = false;
+  }
+
+  function JobController(
+      $scope,
+      $routeParams,
+      $timeout,
+      $q,
+      auroraClient,
+      taskUtil,
+      updateUtil,
+      jobTasksService) {
+
+    initializeJobController($scope, $routeParams);
+
+    $scope.showTaskInfoLink = false;
+    $scope.jobDashboardUrl = '';
+
+    $scope.toggleTaskInfoLinkVisibility = function () {
+      $scope.showTaskInfoLink = !$scope.showTaskInfoLink;
+
+      $scope.activeTasksTableColumns = $scope.showTaskInfoLink ?
+        jobTasksService.addColumn(
+          'Status',
+          jobTasksService.taskColumns,
+          jobTasksService.taskIdColumn) :
+        jobTasksService.taskColumns;
+
+      $scope.completedTasksTableColumns = $scope.showTaskInfoLink ?
+        jobTasksService.addColumn(
+          'Running duration',
+          jobTasksService.completedTaskColumns,
+          jobTasksService.taskIdColumn) :
+        jobTasksService.completedTaskColumns;
+    };
+
+    jobTasksService.getTasksForJob($scope);
+
+    function buildGroupSummary(response) {
+      if (response.error) {
+        $scope.error = 'Error fetching configuration summary: ' + response.error;
+        return [];
       }
 
-      $scope.showTaskInfoLink = false;
-
-      $scope.toggleTaskInfoLinkVisibility = function () {
-        $scope.showTaskInfoLink = !$scope.showTaskInfoLink;
-
-        $scope.activeTasksTableColumns = $scope.showTaskInfoLink ?
-          addColumn(2, taskColumns, taskIdColumn) :
-          taskColumns;
-
-        $scope.completedTasksTableColumns = $scope.showTaskInfoLink ?
-          addColumn(3, completedTaskColumns, taskIdColumn) :
-          completedTaskColumns;
-      };
-
-      $scope.jobDashboardUrl = '';
-      // These two task arrays need to be initialized due to a quirk in SmartTable's behavior.
-      $scope.activeTasks = [];
-      $scope.completedTasks = [];
-      $scope.tasksReady = false;
-
-      function buildGroupSummary(response) {
-
-        if (response.error) {
-          $scope.error = 'Error fetching configuration summary: ' + response.error;
-          return [];
-        }
-
-        var colors = [
-          'steelblue',
-          'darkseagreen',
-          'sandybrown',
-          'plum',
-          'khaki'
-        ];
-
-        var total = _.reduce(response.groups, function (m, n) {
-          return m + n.instanceIds.length;
-        }, 0);
+      var colors = [
+        'steelblue',
+        'darkseagreen',
+        'sandybrown',
+        'plum',
+        'khaki'
+      ];
 
-        $scope.groupSummary = response.groups.map(function (group, i) {
-          var count = group.instanceIds.length;
-          var percentage = (count / total) * 100;
+      var total = _.reduce(response.groups, function (m, n) {
+        return m + n.instanceIds.length;
+      }, 0);
 
-          var ranges = taskUtil.toRanges(group.instanceIds).map(function (r) {
-            return (r.start === r.end) ? r.start : r.start + '-' + r.end;
-          });
+      $scope.groupSummary = response.groups.map(function (group, i) {
+        var count = group.instanceIds.length;
+        var percentage = (count / total) * 100;
 
-          return {
-            label: ranges.join(', '),
-            value: count,
-            percentage: percentage,
-            summary: { schedulingDetail: taskUtil.configToDetails(group.config)},
-            color: colors[i % colors.length]
-          };
+        var ranges = taskUtil.toRanges(group.instanceIds).map(function (r) {
+          return (r.start === r.end) ? r.start : r.start + '-' + r.end;
         });
-      }
-
-      auroraClient.getTasksWithoutConfigs($scope.role, $scope.environment, $scope.job)
-        .then(getTasksForJob);
 
-      auroraClient.getConfigSummary($scope.role, $scope.environment, $scope.job)
-        .then(buildGroupSummary);
-
-      var query = new JobUpdateQuery();
-      var jobKey = new JobKey();
-      jobKey.role = $scope.role;
-      jobKey.environment = $scope.environment;
-      jobKey.name = $scope.job;
-      query.jobKey = jobKey;
-
-      auroraClient.getJobUpdateSummaries(query).then(getUpdatesForJob);
-
-
-      function getUpdatesForJob(response) {
-        $scope.updates = response.summaries;
+        return {
+          label: ranges.join(', '),
+          value: count,
+          percentage: percentage,
+          summary: { schedulingDetail: taskUtil.configToDetails(group.config)},
+          color: colors[i % colors.length]
+        };
+      });
+    }
 
-        function getUpdateInProgress() {
-          auroraClient.getJobUpdateDetails($scope.updates[0].key).then(function (response)
{
-            $scope.updateInProgress = response.details;
+    auroraClient.getConfigSummary($scope.role, $scope.environment, $scope.job)
+      .then(buildGroupSummary);
 
-            $scope.updateStats = updateUtil.getUpdateStats($scope.updateInProgress);
+    var query = new JobUpdateQuery();
+    query.jobKey = new JobKey({
+        role: $scope.role,
+        environment: $scope.environment,
+        name: $scope.job
+      });
 
-            if (updateUtil.isInProgress($scope.updateInProgress.update.summary.state.status))
{
-              // Poll for updates as long as this update is in progress.
-              $timeout(function () {
-                getUpdateInProgress();
-              }, REFRESH_RATES.IN_PROGRESS_UPDATE_MS);
-            }
-          });
-        }
+    auroraClient.getJobUpdateSummaries(query).then(getUpdatesForJob);
 
-        if ($scope.updates.length > 0 && updateUtil.isInProgress($scope.updates[0].state.status))
{
-          getUpdateInProgress();
-        }
-      }
+    function getUpdatesForJob(response) {
+      $scope.updates = response.summaries;
 
-      function getTasksForJob(response) {
-        if (response.error) {
-          $scope.error = 'Error fetching tasks: ' + response.error;
-          return [];
-        }
+      function getUpdateInProgress() {
+        auroraClient.getJobUpdateDetails($scope.updates[0].key).then(function (response)
{
+          $scope.updateInProgress = response.details;
 
-        $scope.jobDashboardUrl = getJobDashboardUrl(response.statsUrlPrefix);
+          $scope.updateStats = updateUtil.getUpdateStats($scope.updateInProgress);
 
-        var tasks = _.map(response.tasks, function (task) {
-          return summarizeTask(task);
+          if (updateUtil.isInProgress($scope.updateInProgress.update.summary.state.status))
{
+            // Poll for updates as long as this update is in progress.
+            $timeout(function () {
+              getUpdateInProgress();
+            }, REFRESH_RATES.IN_PROGRESS_UPDATE_MS);
+          }
         });
-
-        var activeTaskPredicate = function (task) {
-          return task.isActive;
-        };
-
-        $scope.activeTasks = _.chain(tasks)
-          .filter(activeTaskPredicate)
-          .sortBy('instanceId')
-          .value();
-
-        $scope.completedTasks = _.chain(tasks)
-          .reject(activeTaskPredicate)
-          .sortBy(function (task) {
-            return -task.latestActivity; //sort in descending order
-          })
-          .value();
-
-        $scope.tasksReady = true;
       }
 
-      function summarizeTask(task) {
-        var isActive = taskUtil.isActiveTask(task);
-        var sortedTaskEvents = _.sortBy(task.taskEvents, function (taskEvent) {
-          return taskEvent.timestamp;
-        });
-
-        var latestTaskEvent = _.last(sortedTaskEvents);
-
-        return {
-          instanceId: task.assignedTask.instanceId,
-          status: _.invert(ScheduleStatus)[latestTaskEvent.status],
-          statusMessage: latestTaskEvent.message,
-          host: task.assignedTask.slaveHost || '',
-          latestActivity: _.isEmpty(sortedTaskEvents) ? 0 : latestTaskEvent.timestamp,
-          duration: getDuration(sortedTaskEvents),
-          isActive: isActive,
-          taskId: task.assignedTask.taskId,
-          taskEvents: summarizeTaskEvents(sortedTaskEvents),
-          showDetails: false,
-          // TODO(maxim): Revisit this approach when the UI fix in AURORA-715 is finalized.
-          sandboxExists: true
-        };
+      if ($scope.updates.length > 0 && updateUtil.isInProgress($scope.updates[0].state.status))
{
+        getUpdateInProgress();
       }
+    }
 
-      function getDuration(sortedTaskEvents) {
-        var runningTaskEvent = _.find(sortedTaskEvents, function (taskEvent) {
-          return taskEvent.status === ScheduleStatus.RUNNING;
-        });
-
-        if (runningTaskEvent) {
-          var nextEvent = sortedTaskEvents[_.indexOf(sortedTaskEvents, runningTaskEvent)
+ 1];
+  }
+
+  auroraUIControllers.controller('JobController', JobController);
+
+  var guidPattern = new RegExp(
+      /^[A-Za-z0-9]{8}-[A-Za-z0-9]{4}-[A-Za-z0-9]{4}-[A-Za-z0-9]{4}-[A-Za-z0-9]{12}$/);
+
+  function JobInstanceController($scope, $routeParams, $location, jobTasksService) {
+    if (guidPattern.test($routeParams.instance)) {
+      $location.path(
+        [
+          'scheduler',
+          $routeParams.role,
+          $routeParams.environment,
+          $routeParams.job,
+          'update',
+          $routeParams.instance
+        ].join('/'));
+      return;
+    }
 
-          return nextEvent ?
-            nextEvent.timestamp - runningTaskEvent.timestamp :
-            moment().valueOf() - runningTaskEvent.timestamp;
-        }
+    initializeJobController($scope, $routeParams);
+    jobTasksService.getTasksForJob($scope);
 
-        return 0;
-      }
+    $scope.completedTasksTableColumns = $scope.completedTasksTableColumns.filter(function
(column) {
+      return column.label !== 'Instance';
+    });
 
-      function summarizeTaskEvents(taskEvents) {
-        return _.map(taskEvents, function (taskEvent) {
-          return {
-            timestamp: taskEvent.timestamp,
-            status: _.invert(ScheduleStatus)[taskEvent.status],
-            message: taskEvent.message
-          };
-        });
-      }
+    $scope.completedTasksTableColumns = jobTasksService.addColumn(
+        'Status',
+        $scope.completedTasksTableColumns,
+        jobTasksService.taskIdColumn);
+  }
 
-      function getJobDashboardUrl(statsUrlPrefix) {
-        return _.isEmpty(statsUrlPrefix) ?
-          '' :
-          statsUrlPrefix + $scope.role + '.' + $scope.environment + '.' + $scope.job;
-      }
-    }
-  );
+  auroraUIControllers.controller('JobInstanceController', JobInstanceController);
 })();

http://git-wip-us.apache.org/repos/asf/aurora/blob/9b8868fb/src/main/resources/scheduler/assets/js/services.js
----------------------------------------------------------------------
diff --git a/src/main/resources/scheduler/assets/js/services.js b/src/main/resources/scheduler/assets/js/services.js
index a514fa7..085a3d9 100644
--- a/src/main/resources/scheduler/assets/js/services.js
+++ b/src/main/resources/scheduler/assets/js/services.js
@@ -28,13 +28,18 @@
   */
   'use strict';
 
-  function makeJobTaskQuery(role, environment, jobName) {
+  function makeJobTaskQuery(role, environment, jobName, instance) {
     var id = new Identity();
     id.role = role;
     var taskQuery = new TaskQuery();
     taskQuery.owner = id;
     taskQuery.environment = environment;
     taskQuery.jobName = jobName;
+
+    if (instance) {
+      taskQuery.instanceIds = [ instance ];
+    }
+
     return taskQuery;
   }
 
@@ -99,8 +104,8 @@
             });
           },
 
-          getTasksWithoutConfigs: function (role, environment, jobName) {
-            var query = makeJobTaskQuery(role, environment, jobName);
+          getTasksWithoutConfigs: function (role, environment, jobName, instance) {
+            var query = makeJobTaskQuery(role, environment, jobName, instance);
 
             return async(function (deferred) {
               var tasksPromise = async(function (d1) {
@@ -608,4 +613,176 @@
         };
         return cronJobSmrySvc;
       }]);
+
+  auroraUI.factory(
+    'jobTasksService',
+    ['auroraClient', 'taskUtil', function jobTasksServiceFactory(auroraClient, taskUtil)
{
+      var baseTableConfig = {
+        isGlobalSearchActivated: false,
+        isPaginationEnabled: true,
+        itemsByPage: 50,
+        maxSize: 8,
+        selectionMode: 'single'
+      };
+
+      function addColumn(afterLabel, currentColumns, newColumn) {
+        var idxPosition = -1;
+        currentColumns.some(function (column, index) {
+          if (column.label === afterLabel) {
+            idxPosition = index + 1;
+            return true;
+          }
+
+          return false;
+        });
+
+        if (idxPosition === -1) {
+          return;
+        }
+
+        return _.union(
+          _.first(currentColumns, idxPosition),
+          [newColumn],
+          _.last(currentColumns, currentColumns.length - idxPosition));
+      }
+
+      var baseColumns = [
+        {label: 'Instance', map: 'instanceId', cellTemplateUrl: '/assets/taskInstance.html'},
+        {label: 'Status', map: 'status', cellTemplateUrl: '/assets/taskStatus.html'},
+        {label: 'Host', map: 'host', cellTemplateUrl: '/assets/taskSandbox.html'}
+      ];
+
+      var completedTaskColumns = addColumn(
+        'Status',
+        baseColumns,
+        {
+          label: 'Running duration',
+          map: 'duration',
+          formatFunction: function (duration) {
+            return moment.duration(duration).humanize();
+          }
+        });
+
+      function summarizeTask(task) {
+        var isActive = taskUtil.isActiveTask(task);
+        var sortedTaskEvents = _.sortBy(task.taskEvents, function (taskEvent) {
+          return taskEvent.timestamp;
+        });
+
+        var latestTaskEvent = _.last(sortedTaskEvents);
+
+        return {
+          instanceId: task.assignedTask.instanceId,
+          jobKey: task.assignedTask.task.job,
+          status: _.invert(ScheduleStatus)[latestTaskEvent.status],
+          statusMessage: latestTaskEvent.message,
+          host: task.assignedTask.slaveHost || '',
+          latestActivity: _.isEmpty(sortedTaskEvents) ? 0 : latestTaskEvent.timestamp,
+          duration: getDuration(sortedTaskEvents),
+          isActive: isActive,
+          taskId: task.assignedTask.taskId,
+          taskEvents: summarizeTaskEvents(sortedTaskEvents),
+          showDetails: false,
+          // TODO(maxim): Revisit this approach when the UI fix in AURORA-715 is finalized.
+          sandboxExists: true
+        };
+      }
+
+      function getDuration(sortedTaskEvents) {
+        var runningTaskEvent = _.find(sortedTaskEvents, function (taskEvent) {
+          return taskEvent.status === ScheduleStatus.RUNNING;
+        });
+
+        if (runningTaskEvent) {
+          var nextEvent = sortedTaskEvents[_.indexOf(sortedTaskEvents, runningTaskEvent)
+ 1];
+
+          return nextEvent ?
+            nextEvent.timestamp - runningTaskEvent.timestamp :
+            moment().valueOf() - runningTaskEvent.timestamp;
+        }
+
+        return 0;
+      }
+
+      function summarizeTaskEvents(taskEvents) {
+        return _.map(taskEvents, function (taskEvent) {
+          return {
+            timestamp: taskEvent.timestamp,
+            status: _.invert(ScheduleStatus)[taskEvent.status],
+            message: taskEvent.message
+          };
+        });
+      }
+
+      function getJobDashboardUrl(statsUrlPrefix, role, environment, job) {
+        return _.isEmpty(statsUrlPrefix) ?
+          '' :
+          statsUrlPrefix + role + '.' + environment + '.' + job;
+      }
+
+      return {
+        taskIdColumn:  {
+          label: 'Task ID',
+          map: 'taskId',
+          cellTemplateUrl: '/assets/taskLink.html'
+        },
+
+        taskColumns: baseColumns,
+        completedTaskColumns: completedTaskColumns,
+        addColumn: addColumn,
+
+        getTasksForJob: function getTasksForJob($scope) {
+          $scope.activeTasksTableColumns = baseColumns;
+          $scope.completedTasksTableColumns = completedTaskColumns;
+
+          $scope.activeTasksTableConfig = baseTableConfig;
+          $scope.completedTasksTableConfig = baseTableConfig;
+
+          auroraClient.getTasksWithoutConfigs(
+              $scope.role,
+              $scope.environment,
+              $scope.job,
+              $scope.instance)
+            .then(function (response) {
+              if (response.error) {
+                $scope.error = 'Error fetching tasks: ' + response.error;
+                return [];
+              }
+
+              $scope.jobDashboardUrl = getJobDashboardUrl(
+                response.statsUrlPrefix,
+                $scope.role,
+                $scope.environment,
+                $scope.job);
+
+              var tasks = _.map(response.tasks, function (task) {
+                return summarizeTask(task);
+              });
+
+              var activeTaskPredicate = function (task) {
+                return task.isActive;
+              };
+
+              $scope.activeTasks = _.chain(tasks)
+                .filter(activeTaskPredicate)
+                .sortBy('instanceId')
+                .value();
+
+              $scope.completedTasks = _.chain(tasks)
+                .reject(activeTaskPredicate)
+                .sortBy(function (task) {
+                  return -task.latestActivity; //sort in descending order
+                })
+                .value();
+
+              $scope.activeTasksTableConfig.isPaginationEnabled =
+                  $scope.activeTasks.length > $scope.activeTasksTableConfig.itemsByPage;
+              $scope.completedTasksTableConfig.isPaginationEnabled =
+                  $scope.completedTasks.length > $scope.completedTasksTableConfig.itemsByPage;
+
+              $scope.tasksReady = true;
+            });
+        }
+      };
+    }]);
 })();

http://git-wip-us.apache.org/repos/asf/aurora/blob/9b8868fb/src/main/resources/scheduler/assets/taskInstance.html
----------------------------------------------------------------------
diff --git a/src/main/resources/scheduler/assets/taskInstance.html b/src/main/resources/scheduler/assets/taskInstance.html
new file mode 100644
index 0000000..0ab63ee
--- /dev/null
+++ b/src/main/resources/scheduler/assets/taskInstance.html
@@ -0,0 +1,14 @@
+<!--
+ 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.
+ -->
+<a ng-href="/scheduler/{{dataRow.jobKey.role}}/{{dataRow.jobKey.environment}}/{{dataRow.jobKey.name}}/{{dataRow.instanceId}}">{{dataRow.instanceId}}</a>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/aurora/blob/9b8868fb/src/test/python/apache/aurora/client/cli/test_supdate.py
----------------------------------------------------------------------
diff --git a/src/test/python/apache/aurora/client/cli/test_supdate.py b/src/test/python/apache/aurora/client/cli/test_supdate.py
index 21bea70..2135ca9 100644
--- a/src/test/python/apache/aurora/client/cli/test_supdate.py
+++ b/src/test/python/apache/aurora/client/cli/test_supdate.py
@@ -158,7 +158,8 @@ class TestStartUpdate(AuroraClientCommandTest):
         call(ANY, 'hello', None)
     ]
     assert self._fake_context.get_out() == [
-        StartUpdate.UPDATE_MSG_TEMPLATE % ('http://something_or_other/scheduler/role/env/name/id')
+        StartUpdate.UPDATE_MSG_TEMPLATE %
+        ('http://something_or_other/scheduler/role/env/name/update/id')
     ]
     assert self._fake_context.get_err() == []
 
@@ -183,7 +184,8 @@ class TestStartUpdate(AuroraClientCommandTest):
     ]
 
     assert self._fake_context.get_out() == [
-        StartUpdate.UPDATE_MSG_TEMPLATE % ('http://something_or_other/scheduler/role/env/name/id'),
+        StartUpdate.UPDATE_MSG_TEMPLATE %
+        ('http://something_or_other/scheduler/role/env/name/update/id'),
         'Current state ROLLED_FORWARD'
     ]
     assert self._fake_context.get_err() == []

http://git-wip-us.apache.org/repos/asf/aurora/blob/9b8868fb/src/test/python/apache/aurora/client/test_base.py
----------------------------------------------------------------------
diff --git a/src/test/python/apache/aurora/client/test_base.py b/src/test/python/apache/aurora/client/test_base.py
index 1a56008..fa5eb07 100644
--- a/src/test/python/apache/aurora/client/test_base.py
+++ b/src/test/python/apache/aurora/client/test_base.py
@@ -56,3 +56,25 @@ class TestBase(unittest.TestCase):
     resp = Response(responseCode=ResponseCode.OK, result=Result(populateJobResult=PopulateJobResult(
         taskConfig=config)))
     assert config == resp.result.populateJobResult.taskConfig
+
+  def test_synthesize_url(self):
+    base_url = 'http://example.com'
+    role = 'some-role'
+    environment = 'some-environment'
+    job = 'some-job'
+    update_id = 'some-update-id'
+
+    assert (('%s/scheduler/%s/%s/%s/update/%s' % (base_url, role, environment, job, update_id))
==
+        base.synthesize_url(base_url, role, environment, job, update_id=update_id))
+
+    assert (('%s/scheduler/%s/%s/%s' % (base_url, role, environment, job)) ==
+        base.synthesize_url(base_url, role, environment, job))
+
+    assert (('%s/scheduler/%s/%s' % (base_url, role, environment)) ==
+        base.synthesize_url(base_url, role, environment))
+
+    assert (('%s/scheduler/%s' % (base_url, role)) ==
+        base.synthesize_url(base_url, role))
+
+    assert (('%s/scheduler/%s' % (base_url, role)) ==
+        base.synthesize_url(base_url, role))


Mime
View raw message