You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mesos.apache.org by ss...@apache.org on 2014/01/14 02:46:58 UTC

git commit: Implemented a table directive that allows for sorting and pagination.

Updated Branches:
  refs/heads/master 6d943bb45 -> 351e02bed


Implemented a table directive that allows for sorting and pagination.

From: Thomas Rampelberg <th...@saunter.org>
Review: https://reviews.apache.org/r/16787


Project: http://git-wip-us.apache.org/repos/asf/mesos/repo
Commit: http://git-wip-us.apache.org/repos/asf/mesos/commit/351e02be
Tree: http://git-wip-us.apache.org/repos/asf/mesos/tree/351e02be
Diff: http://git-wip-us.apache.org/repos/asf/mesos/diff/351e02be

Branch: refs/heads/master
Commit: 351e02bedb9056b3ab6720d3336028e6f40e4707
Parents: 6d943bb
Author: Ross Allen <ro...@mesosphere.io>
Authored: Mon Jan 13 16:30:14 2014 -0800
Committer: Ross Allen <ro...@mesosphere.io>
Committed: Mon Jan 13 16:30:14 2014 -0800

----------------------------------------------------------------------
 src/Makefile.am                                 |   4 +-
 .../master/static/directives/pagination.html    |   7 +
 src/webui/master/static/framework.html          |  79 +++-------
 src/webui/master/static/frameworks.html         |  81 +++-------
 src/webui/master/static/home.html               |  84 +++-------
 src/webui/master/static/js/app.js               | 101 +++++++++++-
 src/webui/master/static/js/controllers.js       | 153 +++----------------
 src/webui/master/static/offers.html             |  30 +---
 src/webui/master/static/slave.html              |  70 +++------
 src/webui/master/static/slave_executor.html     |  85 +++--------
 src/webui/master/static/slave_framework.html    |  67 +++-----
 src/webui/master/static/slaves.html             |  40 ++---
 12 files changed, 261 insertions(+), 540 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mesos/blob/351e02be/src/Makefile.am
----------------------------------------------------------------------
diff --git a/src/Makefile.am b/src/Makefile.am
index 21df397..22d23f4 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -448,7 +448,9 @@ nobase_dist_pkgdata_DATA +=						\
   webui/master/static/slave_executor.html				\
   webui/master/static/slave_framework.html				\
   webui/master/static/slaves.html					\
-  webui/master/static/directives/timestamp.html
+  webui/master/static/directives/timestamp.html				\
+  webui/master/static/directives/pagination.html
+
 
 # Need to distribute/install webui images.
 nobase_dist_pkgdata_DATA +=						\

http://git-wip-us.apache.org/repos/asf/mesos/blob/351e02be/src/webui/master/static/directives/pagination.html
----------------------------------------------------------------------
diff --git a/src/webui/master/static/directives/pagination.html b/src/webui/master/static/directives/pagination.html
new file mode 100644
index 0000000..dc4ad4d
--- /dev/null
+++ b/src/webui/master/static/directives/pagination.html
@@ -0,0 +1,7 @@
+<pagination
+  data-ng-if="originalData.length > pageLength"
+  max-size="5"
+  items-per-page="pageLength"
+  page="page"
+  on-select-page="changePage(page)"
+  total-items="originalData.length"></pagination>

http://git-wip-us.apache.org/repos/asf/mesos/blob/351e02be/src/webui/master/static/framework.html
----------------------------------------------------------------------
diff --git a/src/webui/master/static/framework.html b/src/webui/master/static/framework.html
index f0aff0d..08b8afa 100644
--- a/src/webui/master/static/framework.html
+++ b/src/webui/master/static/framework.html
@@ -42,34 +42,20 @@
 
   <div class="col-md-9">
     <h3 id="frameworks">Active Tasks</h3>
-    <table class="table table-striped table-bordered table-condensed">
+    <table m-table table-content="framework.tasks"
+      class="table table-striped table-bordered table-condensed">
       <thead>
         <tr>
-          <th ng-class="columnClass('active_tasks', 'id')"
-              ng-click="selectColumn('active_tasks', 'id')">
-            ID
-          </th>
-          <th ng-class="columnClass('active_tasks', 'name')"
-              ng-click="selectColumn('active_tasks', 'name')">
-            Name
-          </th>
-          <th ng-class="columnClass('active_tasks', 'state')"
-              ng-click="selectColumn('active_tasks', 'state')">
-            State
-          </th>
-          <th ng-class="columnClass('active_tasks', 'start_time')"
-              ng-click="selectColumn('active_tasks', 'start_time')">
-            Started
-          </th>
-          <th ng-class="columnClass('active_tasks', 'host')"
-              ng-click="selectColumn('active_tasks', 'host')">
-            Host
-          </th>
+          <th data-key="id">ID</th>
+          <th data-key="name">Name</th>
+          <th data-key="state">State</th>
+          <th data-key="start_time">Started</th>
+          <th data-key="host">Host</th>
           <th></th>
         </tr>
       </thead>
       <tbody>
-        <tr ng-repeat="task in framework.tasks | orderBy:tables['active_tasks'].selected_column:tables['active_tasks'].reverse | slice:((tables.active_tasks.page - 1) * itemsPerPage):(tables.active_tasks.page * itemsPerPage)">
+        <tr ng-repeat="task in $data">
           <td>
             <a href="#/slaves/{{task.slave_id}}/frameworks/{{task.framework_id}}/executors/{{task.executor_id}}">
               {{task.id}}
@@ -99,46 +85,23 @@
         </tr>
       </tbody>
     </table>
-    <pagination
-      data-ng-if="active_tasks.length > itemsPerPage"
-      max-size="5"
-      page="tables.active_tasks.page"
-      total-items="active_tasks.length"></pagination>
 
     <h3 id="frameworks">Completed Tasks</h3>
-    <table class="table table-striped table-bordered table-condensed">
+    <table m-table table-content="framework.completed_tasks"
+      class="table table-striped table-bordered table-condensed">
       <thead>
         <tr>
-          <th ng-class="columnClass('completed_tasks', 'id')"
-              ng-click="selectColumn('completed_tasks', 'id')">
-            ID
-          </th>
-          <th ng-class="columnClass('completed_tasks', 'name')"
-              ng-click="selectColumn('completed_tasks', 'name')">
-            Name
-          </th>
-          <th ng-class="columnClass('completed_tasks', 'state')"
-              ng-click="selectColumn('completed_tasks', 'state')">
-            State
-          </th>
-          <th ng-class="columnClass('completed_tasks', 'start_time')"
-              ng-click="selectColumn('completed_tasks', 'start_time')">
-            Started
-          </th>
-          <th ng-class="columnClass('completed_tasks', 'finish_time')"
-              ng-click="selectColumn('completed_tasks', 'finish_time')">
-            Stopped
-          </th>
-          <th ng-class="columnClass('completed_tasks', 'host')"
-              ng-click="selectColumn('completed_tasks', 'host')">
-            Host
-          </th>
-          <th>
-          </th>
+          <th data-key="id">ID</th>
+          <th data-key="name">Name</th>
+          <th data-key="state">State</th>
+          <th data-key="start_time">Started</th>
+          <th data-key="finish_time">Stopped</th>
+          <th data-key="host">Host</th>
+          <th></th>
         </tr>
       </thead>
       <tbody>
-        <tr ng-repeat="task in framework.completed_tasks | orderBy:tables.completed_tasks.selected_column:tables.completed_tasks.reverse | slice:((tables.completed_tasks.page - 1) * itemsPerPage):(tables.completed_tasks.page * itemsPerPage)">
+        <tr ng-repeat="task in $data">
           <td>{{task.id}}</td>
           <td>{{task.name}}</td>
           <td>{{task.state | truncateMesosState}}</td>
@@ -168,11 +131,5 @@
         </tr>
       </tbody>
     </table>
-    <pagination
-      data-ng-if="completed_tasks.length > itemsPerPage"
-      max-size="5"
-      page="tables.completed_tasks.page"
-      total-items="completed_tasks.length"></pagination>
-
   </div>
 </div>

http://git-wip-us.apache.org/repos/asf/mesos/blob/351e02be/src/webui/master/static/frameworks.html
----------------------------------------------------------------------
diff --git a/src/webui/master/static/frameworks.html b/src/webui/master/static/frameworks.html
index 1cdd39f..29683df 100644
--- a/src/webui/master/static/frameworks.html
+++ b/src/webui/master/static/frameworks.html
@@ -6,49 +6,23 @@
 </ol>
 
 <h3 id="frameworks">Active Frameworks</h3>
-<table class="table table-striped table-bordered table-condensed">
+<table m-table table-content="frameworks"
+  class="table table-striped table-bordered table-condensed">
   <thead>
     <tr>
-      <th ng-class="columnClass('frameworks', 'id')"
-          ng-click="selectColumn('frameworks', 'id')">
-        ID
-      </th>
-      <th ng-class="columnClass('frameworks', 'user')"
-          ng-click="selectColumn('frameworks', 'user')">
-        User
-      </th>
-      <th ng-class="columnClass('frameworks', 'name')"
-          ng-click="selectColumn('frameworks', 'name')">
-        Name
-      </th>
-      <th ng-class="columnClass('frameworks', 'tasks.length')"
-          ng-click="selectColumn('frameworks', 'tasks.length')">
-        Active Tasks
-      </th>
-      <th ng-class="columnClass('frameworks', 'resources.cpus')"
-          ng-click="selectColumn('frameworks', 'resources.cpus')">
-        CPUs
-      </th>
-      <th ng-class="columnClass('frameworks', 'resources.mem')"
-          ng-click="selectColumn('frameworks', 'resources.mem')">
-        Mem
-      </th>
-      <th ng-class="columnClass('frameworks', 'max_share')"
-          ng-click="selectColumn('frameworks', 'max_share')">
-        Max Share
-      </th>
-      <th ng-class="columnClass('frameworks', 'registered_time')"
-          ng-click="selectColumn('frameworks', 'registered_time')">
-        Registered
-      </th>
-      <th ng-class="columnClass('frameworks', 'reregistered_time')"
-          ng-click="selectColumn('frameworks', 'reregistered_time')">
-        Re-Registered
-      </th>
+      <th data-key="id">ID</th>
+      <th data-key="user">User</th>
+      <th data-key="name">Name</th>
+      <th data-key="tasks.length">Active Tasks</th>
+      <th data-key="resources.cpus">CPUs</th>
+      <th data-key="resources.mem">Mem</th>
+      <th data-key="max_share">Max Share</th>
+      <th data-key="registered_time">Registered</th>
+      <th data-key="reregistered_time">Re-Registered</th>
     </tr>
   </thead>
   <tbody>
-    <tr ng-repeat="framework in _.values(frameworks) | orderBy:tables['frameworks'].selected_column:tables['frameworks'].reverse">
+    <tr ng-repeat="framework in $data">
       <td>
         <a href="{{'#/frameworks/' + framework.id}}">
           {{(framework.id | truncateMesosID) || framework.name}}</a>
@@ -78,33 +52,20 @@
 </table>
 
 <h3>Terminated Frameworks</h3>
-<table class="table table-striped table-bordered table-condensed">
+
+<table m-table table-content="completed_frameworks"
+  class="table table-striped table-bordered table-condensed">
   <thead>
     <tr>
-      <th ng-class="columnClass('completed_frameworks', 'id')"
-          ng-click="selectColumn('completed_frameworks', 'id')">
-        ID
-      </th>
-      <th ng-class="columnClass('completed_frameworks', 'user')"
-          ng-click="selectColumn('completed_frameworks', 'user')">
-        User
-      </th>
-      <th ng-class="columnClass('completed_frameworks', 'name')"
-          ng-click="selectColumn('completed_frameworks', 'name')">
-        Name
-      </th>
-      <th ng-class="columnClass('completed_frameworks', 'registered_time')"
-          ng-click="selectColumn('completed_frameworks', 'registered_time')">
-        Registered
-      </th>
-      <th ng-class="columnClass('completed_frameworks', 'unregistered_time')"
-          ng-click="selectColumn('completed_frameworks', 'unregistered_time')">
-        Unregistered
-      </th>
+      <th data-key="id">ID</th>
+      <th data-key="user">User</th>
+      <th data-key="name">Name</th>
+      <th data-key="registered_time">Registered</th>
+      <th data-key="unregistered_time">Unregistered</th>
     </tr>
   </thead>
   <tbody>
-    <tr ng-repeat="framework in _.values(completed_frameworks) | orderBy:tables['completed_frameworks'].selected_column:tables['completed_frameworks'].reverse">
+    <tr ng-repeat="framework in $data">
       <td>
         <a href="{{'#/frameworks/' + framework.id}}" title="{{framework.id}}">
           {{framework.id | truncateMesosID}}</a>

http://git-wip-us.apache.org/repos/asf/mesos/blob/351e02be/src/webui/master/static/home.html
----------------------------------------------------------------------
diff --git a/src/webui/master/static/home.html b/src/webui/master/static/home.html
index 336d118..bffd7af 100644
--- a/src/webui/master/static/home.html
+++ b/src/webui/master/static/home.html
@@ -112,36 +112,20 @@
           </tr>
         </tbody>
       </table>
-
     </div>
   </div>
 
   <div class="col-md-9">
-
     <h3 id="frameworks">Active Tasks</h3>
-    <table class="table table-striped table-bordered table-condensed">
+    <table m-table table-content="active_tasks"
+      class="table table-striped table-bordered table-condensed">
       <thead>
         <tr>
-          <th ng-class="columnClass('active_tasks', 'id')"
-              ng-click="selectColumn('active_tasks', 'id')">
-            ID
-          </th>
-          <th ng-class="columnClass('active_tasks', 'name')"
-              ng-click="selectColumn('active_tasks', 'name')">
-            Name
-          </th>
-          <th ng-class="columnClass('active_tasks', 'state')"
-              ng-click="selectColumn('active_tasks', 'state')">
-            State
-          </th>
-          <th ng-class="columnClass('active_tasks', 'start_time')"
-              ng-click="selectColumn('active_tasks', 'start_time')">
-            Started
-          </th>
-          <th ng-class="columnClass('active_tasks', 'host')"
-              ng-click="selectColumn('active_tasks', 'host')">
-            Host
-          </th>
+          <th data-key="id">ID</th>
+          <th data-key="name">Name</th>
+          <th data-key="state">State</th>
+          <th data-key="start_time" data-sort>Started</th>
+          <th data-key="host">Host</th>
           <th></th>
         </tr>
       </thead>
@@ -149,7 +133,7 @@
         <tr data-ng-if="active_tasks.length === 0">
           <td colspan="6">No active tasks.</td>
         </tr>
-        <tr ng-repeat="task in active_tasks | orderBy:tables.active_tasks.selected_column:tables.active_tasks.reverse | slice:((tables.active_tasks.page - 1) * itemsPerPage):(tables.active_tasks.page * itemsPerPage)">
+        <tr ng-repeat="task in $data">
           <td>
             <a href="#/slaves/{{task.slave_id}}/frameworks/{{task.framework_id}}/executors/{{task.executor_id}}">
               {{task.id}}
@@ -179,49 +163,26 @@
         </tr>
       </tbody>
     </table>
-    <pagination
-      data-ng-if="active_tasks.length > itemsPerPage"
-      max-size="5"
-      page="tables.active_tasks.page"
-      total-items="active_tasks.length"></pagination>
 
     <h3 id="frameworks">Completed Tasks</h3>
-    <table class="table table-striped table-bordered table-condensed">
+    <table m-table table-content="completed_tasks"
+      class="table table-striped table-bordered table-condensed">
       <thead>
         <tr>
-          <th ng-class="columnClass('completed_tasks', 'id')"
-              ng-click="selectColumn('completed_tasks', 'id')">
-            ID
-          </th>
-          <th ng-class="columnClass('completed_tasks', 'name')"
-              ng-click="selectColumn('completed_tasks', 'name')">
-            Name
-          </th>
-          <th ng-class="columnClass('completed_tasks', 'state')"
-              ng-click="selectColumn('completed_tasks', 'state')">
-            State
-          </th>
-          <th ng-class="columnClass('completed_tasks', 'start_time')"
-              ng-click="selectColumn('completed_tasks', 'start_time')">
-            Started
-          </th>
-          <th ng-class="columnClass('completed_tasks', 'finish_time')"
-              ng-click="selectColumn('completed_tasks', 'finish_time')">
-            Stopped
-          </th>
-          <th ng-class="columnClass('completed_tasks', 'host')"
-              ng-click="selectColumn('completed_tasks', 'host')">
-            Host
-          </th>
-          <th>
-          </th>
+          <th data-key="id">ID</th>
+          <th data-key="name">Name</th>
+          <th data-key="state">State</th>
+          <th data-key="start_time" data-sort>Started</th>
+          <th data-key="finish_time">Stopped</th>
+          <th data-key="host">Host</th>
+          <th></th>
         </tr>
       </thead>
       <tbody>
         <tr data-ng-if="completed_tasks.length === 0">
           <td colspan="7">No completed tasks.</td>
         </tr>
-        <tr ng-repeat="task in completed_tasks | orderBy:tables.completed_tasks.selected_column:tables.completed_tasks.reverse | slice:((tables.completed_tasks.page - 1) * itemsPerPage):(tables.completed_tasks.page * itemsPerPage)">
+        <tr ng-repeat="task in $data">
           <td>{{task.id}}</td>
           <td>{{task.name}}</td>
           <td>{{task.state | truncateMesosState}}</td>
@@ -232,11 +193,11 @@
             <m-timestamp value="{{task.finish_time}}"></m-timestamp>
           </td>
           <td>
-            <a data-ng-show="slaves[task.slave_id]"
+            <a data-ng-show="_.has(slaves, task.slave_id)"
                 href="#/slaves/{{task.slave_id}}/frameworks/{{task.framework_id}}/executors/{{task.executor_id}}">
               {{slaves[task.slave_id].hostname}}
             </a>
-            <span class="text-muted" data-ng-show="!slaves[task.slave_id]">
+            <span class="text-muted" data-ng-show="!_.has(slaves, task.slave_id)">
               Slave offline
             </span>
           </td>
@@ -251,11 +212,6 @@
         </tr>
       </tbody>
     </table>
-    <pagination
-      data-ng-if="completed_tasks.length > itemsPerPage"
-      max-size="5"
-      page="tables.completed_tasks.page"
-      total-items="completed_tasks.length"></pagination>
 
   </div>
 </div>

http://git-wip-us.apache.org/repos/asf/mesos/blob/351e02be/src/webui/master/static/js/app.js
----------------------------------------------------------------------
diff --git a/src/webui/master/static/js/app.js b/src/webui/master/static/js/app.js
index 67e9a3a..69abe02 100644
--- a/src/webui/master/static/js/app.js
+++ b/src/webui/master/static/js/app.js
@@ -43,7 +43,6 @@
       //
       // [1] http://angular-ui.github.io/bootstrap/#/pagination
       paginationConfig.boundaryLinks = true;
-      paginationConfig.itemsPerPage = 50;
       paginationConfig.rotate = false;
 
       ZeroClipboard.setDefaults({
@@ -218,5 +217,103 @@
         },
         templateUrl: 'static/directives/timestamp.html'
       }
-    }]);
+    }])
+    .directive('mPagination', function() {
+      return {
+        templateUrl: 'static/directives/pagination.html',
+        link: function(scope, element, attrs) {
+          /* data-ng-if creates a brand new scope. That ends up hiding the
+           * value of page within this directive. The function reference isn't
+           * changing and therefore gets called successfully. The scope here
+           * (because ngIf hasn't run yet) is still correct and changing
+           * pgNum correctly propogates to mTable.
+           */
+          scope.changePage = function(pgNum) {
+            scope.pgNum = pgNum;
+          };
+        }
+      }
+    })
+    .directive('mTable', ['$compile', '$filter', function($compile, $filter) {
+      /* This directive does not have a template. The DOM doesn't like
+       * having partially defined tables and so they don't work well with
+       * directives and templates. Because of this, the sub-elements that this
+       * includes are their own directive/templates and it adds them via. DOM
+       * manipulation here.
+       */
+      return {
+        scope: true,
+        link: function(scope, element, attrs) {
+          var defaultOrder = true;
+
+          _.extend(scope, {
+            originalData: [],
+            columnKey: '',
+            sortOrder: defaultOrder,
+            pgNum: 1,
+            pageLength: 50
+          })
+          // ---
+
+          // --- Allow sorting by column based on the <th> data-key attr
+          var th = element.find('th');
+          th.attr('ng-click', 'sortColumn($event)');
+          $compile(th)(scope);
+
+          var setSorting = function(el) {
+            var key = el.attr('data-key');
+
+            if (scope.columnKey === key) {
+              scope.sortOrder = !scope.sortOrder;
+            }
+            else { scope.sortOrder = defaultOrder }
+
+            scope.columnKey = key;
+
+            th.removeClass('descending ascending');
+            el.addClass(scope.sortOrder ? 'descending' : 'ascending');
+          };
+
+          var defaultSortColumn = function() {
+            var el = element.find('[data-sort]');
+            if (el.length === 0) {
+              el = element.find('th:first');
+            }
+            return el;
+          };
+
+          scope.sortColumn = function(ev) {
+            setSorting(angular.element(ev.target));
+          };
+
+          setSorting(defaultSortColumn());
+          // ---
+
+          scope.$watch(attrs.tableContent, function(data) {
+            if (!data) { scope.originalData = []; return };
+            if (angular.isObject(data)) { data = _.values(data) }
+
+            scope.originalData = data;
+          });
+
+          var setTableData = function() {
+            scope.$data = $filter('orderBy')(
+              scope.originalData,
+              scope.columnKey,
+              scope.sortOrder).slice(
+                (scope.pgNum - 1) * scope.pageLength,
+                scope.pgNum * scope.pageLength);
+          };
+
+          _.each(['originalData', 'columnKey', 'sortOrder', 'pgNum'],
+            function(k) { scope.$watch(k, setTableData); });
+
+          // --- Pagination controls
+          var el = angular.element('<div m-pagination></div>');
+          $compile(el)(scope);
+          element.after(el);
+          // ---
+        }
+      };
+     }]);
 })();

http://git-wip-us.apache.org/repos/asf/mesos/blob/351e02be/src/webui/master/static/js/controllers.js
----------------------------------------------------------------------
diff --git a/src/webui/master/static/js/controllers.js b/src/webui/master/static/js/controllers.js
index 064f9ec..01fe64c 100644
--- a/src/webui/master/static/js/controllers.js
+++ b/src/webui/master/static/js/controllers.js
@@ -3,17 +3,6 @@
 
   var mesosApp = angular.module('mesos');
 
-  // Table Object.
-  //   page:            current page number if the table is paginated.
-  //   reverse:         boolean indicating sort order.
-  //   selected_column: column predicate for the selected column.
-  function Table(selected_column) {
-    this.page = 1;
-    this.reverse = true;
-    this.selected_column = selected_column;
-  }
-
-
   function hasSelectedText() {
     if (window.getSelection) {  // All browsers except IE before version 9.
       var range = window.getSelection();
@@ -22,43 +11,6 @@
     return false;
   }
 
-
-  // Returns a curried function for returning the HTML 'class=' tag
-  // attribute value for sorting table columns in the provided scope.
-  function columnClass($scope) {
-    // For the given table column, this behaves as follows:
-    // Column unselected            : 'unselected'
-    // Column selected / descending : 'descending'
-    // Column selected / ascending  : 'ascending'
-    return function(table, column) {
-      if ($scope.tables[table].selected_column === column) {
-        if ($scope.tables[table].reverse) {
-          return 'descending';
-        } else {
-          return 'ascending';
-        }
-      }
-      return 'unselected';
-    };
-  }
-
-
-  // Returns a curried function to be called when a table column is clicked
-  // in the provided scope.
-  function selectColumn($scope) {
-    // Assigns the given table column as the sort column, flipping the
-    // sort order if the sort column has not changed.
-    return function(table, column) {
-      if ($scope.tables[table].selected_column === column) {
-        $scope.tables[table].reverse = !$scope.tables[table].reverse;
-      } else {
-        $scope.tables[table].reverse = true;
-      }
-      $scope.tables[table].selected_column = column;
-    };
-  }
-
-
   // Invokes the pailer for the specified host and path using the
   // specified window_title.
   function pailer(host, path, window_title) {
@@ -99,7 +51,7 @@
       return true; // Continue polling.
     }
 
-    $scope.state = $.parseJSON(data);
+    $scope.state = JSON.parse(data);
 
     // Determine if there is a leader (and redirect if not the leader).
     if ($scope.state.leader) {
@@ -243,7 +195,7 @@
     $scope.idle_mem = $scope.total_mem - ($scope.offered_mem + $scope.used_mem);
 
     $scope.time_since_update = 0;
-    $.event.trigger('state_updated');
+    $scope.$broadcast('state_updated');
 
     return true; // Continue polling.
   }
@@ -256,8 +208,8 @@
   // active controller/view to easily access anything in scope (e.g.,
   // the state).
   mesosApp.controller('MainCntl', [
-      '$scope', '$http', '$location', '$timeout', '$modal', 'paginationConfig',
-      function($scope, $http, $location, $timeout, $modal, paginationConfig) {
+      '$scope', '$http', '$location', '$timeout', '$modal',
+      function($scope, $http, $location, $timeout, $modal) {
     $scope.doneLoading = true;
 
     // Adding bindings into scope so that they can be used from within
@@ -284,10 +236,6 @@
     $scope.retry = 0;
     $scope.time_since_update = 0;
 
-    // Make pagination config available in all scopes to properly calculate
-    // slices of collections for pagination in views.
-    $scope.itemsPerPage = paginationConfig.itemsPerPage;
-
     // Ordered Array of path => activeTab mappings. On successful route changes,
     // the `pathRegexp` values are matched against the current route. The first
     // match will be used to set the active navbar tab.
@@ -385,13 +333,6 @@
 
 
   mesosApp.controller('HomeCtrl', function($dialog, $scope) {
-    $scope.tables = {
-      active_tasks: new Table('start_time'),
-      completed_tasks: new Table('finish_time')
-    };
-    $scope.columnClass = columnClass($scope);
-    $scope.selectColumn = selectColumn($scope);
-
     $scope.log = function($event) {
       if (!$scope.state.log_dir) {
         $dialog.messageBox(
@@ -408,33 +349,11 @@
     };
   });
 
+  mesosApp.controller('FrameworksCtrl', function() {});
 
-  mesosApp.controller('FrameworksCtrl', function($scope) {
-    $scope.tables = {};
-    $scope.tables['frameworks'] = new Table('id');
-    $scope.tables['completed_frameworks'] = new Table('id');
-
-    $scope.columnClass = columnClass($scope);
-    $scope.selectColumn = selectColumn($scope);
-  });
-
-
-  mesosApp.controller('OffersCtrl', function($scope) {
-    $scope.tables = {};
-    $scope.tables['offers'] = new Table('id');
-
-    $scope.columnClass = columnClass($scope);
-    $scope.selectColumn = selectColumn($scope);
-  });
+  mesosApp.controller('OffersCtrl', function() {});
 
   mesosApp.controller('FrameworkCtrl', function($scope, $routeParams) {
-    $scope.tables = {};
-    $scope.tables['active_tasks'] = new Table('id');
-    $scope.tables['completed_tasks'] = new Table('id');
-
-    $scope.columnClass = columnClass($scope);
-    $scope.selectColumn = selectColumn($scope);
-
     var update = function() {
       if ($routeParams.id in $scope.completed_frameworks) {
         $scope.framework = $scope.completed_frameworks[$routeParams.id];
@@ -454,32 +373,17 @@
       update();
     }
 
-    $(document).on('state_updated', update);
-    $scope.$on('$routeChangeStart', function() {
-      $(document).off('state_updated', update);
-    });
+    var removeListener = $scope.$on('state_updated', update);
+    $scope.$on('$routeChangeStart', removeListener);
   });
 
 
-  mesosApp.controller('SlavesCtrl', function($scope) {
-    $scope.tables = {};
-    $scope.tables['slaves'] = new Table('id');
-
-    $scope.columnClass = columnClass($scope);
-    $scope.selectColumn = selectColumn($scope);
-  });
+  mesosApp.controller('SlavesCtrl', function() {});
 
 
   mesosApp.controller('SlaveCtrl', function($dialog, $scope, $routeParams, $http, $q) {
     $scope.slave_id = $routeParams.slave_id;
 
-    $scope.tables = {};
-    $scope.tables['frameworks'] = new Table('id');
-    $scope.tables['completed_frameworks'] = new Table('id');
-
-    $scope.columnClass = columnClass($scope);
-    $scope.selectColumn = selectColumn($scope);
-
     var update = function() {
       if (!($routeParams.slave_id in $scope.slaves)) {
         $scope.alert_message = 'No slave found with ID: ' + $routeParams.slave_id;
@@ -560,10 +464,8 @@
       update();
     }
 
-    $(document).on('state_updated', update);
-    $scope.$on('$routeChangeStart', function() {
-      $(document).off('state_updated', update);
-    });
+    var removeListener = $scope.$on('state_updated', update);
+    $scope.$on('$routeChangeStart', removeListener);
   });
 
 
@@ -571,13 +473,6 @@
     $scope.slave_id = $routeParams.slave_id;
     $scope.framework_id = $routeParams.framework_id;
 
-    $scope.tables = {};
-    $scope.tables['executors'] = new Table('id');
-    $scope.tables['completed_executors'] = new Table('id');
-
-    $scope.columnClass = columnClass($scope);
-    $scope.selectColumn = selectColumn($scope);
-
     var update = function() {
       if (!($routeParams.slave_id in $scope.slaves)) {
         $scope.alert_message = 'No slave found with ID: ' + $routeParams.slave_id;
@@ -662,10 +557,8 @@
       update();
     }
 
-    $(document).on('state_updated', update);
-    $scope.$on('$routeChangeStart', function() {
-      $(document).off('state_updated', update);
-    });
+    var removeListener = $scope.$on('state_updated', update);
+    $scope.$on('$routeChangeStart', removeListener);
   });
 
 
@@ -674,14 +567,6 @@
     $scope.framework_id = $routeParams.framework_id;
     $scope.executor_id = $routeParams.executor_id;
 
-    $scope.tables = {};
-    $scope.tables['tasks'] = new Table('id');
-    $scope.tables['queued_tasks'] = new Table('id');
-    $scope.tables['completed_tasks'] = new Table('id');
-
-    $scope.columnClass = columnClass($scope);
-    $scope.selectColumn = selectColumn($scope);
-
     var update = function() {
       if (!($routeParams.slave_id in $scope.slaves)) {
         $scope.alert_message = 'No slave found with ID: ' + $routeParams.slave_id;
@@ -758,10 +643,8 @@
       update();
     }
 
-    $(document).on('state_updated', update);
-    $scope.$on('$routeChangeStart', function() {
-      $(document).off('state_updated', update);
-    });
+    var removeListener = $scope.$on('state_updated', update);
+    $scope.$on('$routeChangeStart', removeListener);
   });
 
 
@@ -920,9 +803,7 @@
       update();
     }
 
-    $(document).on('state_updated', update);
-    $scope.$on('$routeChangeStart', function() {
-      $(document).off('state_updated', update);
-    });
+    var removeListener = $scope.$on('state_updated', update);
+    $scope.$on('$routeChangeStart', removeListener);
   });
 })();

http://git-wip-us.apache.org/repos/asf/mesos/blob/351e02be/src/webui/master/static/offers.html
----------------------------------------------------------------------
diff --git a/src/webui/master/static/offers.html b/src/webui/master/static/offers.html
index 121fd93..1cce030 100644
--- a/src/webui/master/static/offers.html
+++ b/src/webui/master/static/offers.html
@@ -8,33 +8,19 @@
 </ol>
 
 <h3>Offers</h3>
-<table class="table table-striped table-bordered table-condensed">
+<table m-table table-content="offers"
+  class="table table-striped table-bordered table-condensed">
   <thead>
     <tr>
-      <th ng-class="columnClass('offers', 'id')"
-          ng-click="selectColumn('offers', 'id')">
-        ID
-      </th>
-      <th ng-class="columnClass('offers', 'framework_name')"
-          ng-click="selectColumn('offers', 'framework_name')">
-        Framework
-      </th>
-      <th ng-class="columnClass('offers', 'hostname')"
-          ng-click="selectColumn('offers', 'hostname')">
-        Host
-      </th>
-      <th ng-class="columnClass('offers', 'resources.cpus')"
-          ng-click="selectColumn('offers', 'resources.cpus')">
-        CPUs
-      </th>
-      <th ng-class="columnClass('offers', 'resources.mem')"
-          ng-click="selectColumn('offers', 'resources.mem')">
-        Mem
-      </th>
+      <th data-key="id">ID</th>
+      <th data-key="framework_name">Framework</th>
+      <th data-key="hostname">Host</th>
+      <th data-key="resources.cpus">CPUs</th>
+      <th data-key="resources.mem">Mem</th>
     </tr>
   </thead>
   <tbody>
-    <tr ng-repeat="offer in _.values(offers) | orderBy:tables['offers'].selected_column:tables['offers'].reverse">
+    <tr ng-repeat="offer in $data">
       <td>
         <abbr title="{{offer.id}}">{{offer.id | truncateMesosID}}</abbr>
       </td>

http://git-wip-us.apache.org/repos/asf/mesos/blob/351e02be/src/webui/master/static/slave.html
----------------------------------------------------------------------
diff --git a/src/webui/master/static/slave.html b/src/webui/master/static/slave.html
index 60dc308..134aa0b 100644
--- a/src/webui/master/static/slave.html
+++ b/src/webui/master/static/slave.html
@@ -100,37 +100,20 @@
   </div>
   <div class="col-md-9">
     <h3>Frameworks</h3>
-    <table class="table table-striped table-bordered table-condensed">
+    <table m-table table-content="slave.frameworks"
+      class="table table-striped table-bordered table-condensed">
       <thead>
         <tr>
-          <th ng-class="columnClass('frameworks', 'id')"
-              ng-click="selectColumn('frameworks', 'id')">
-            ID
-          </th>
-          <th ng-class="columnClass('frameworks', 'user')"
-              ng-click="selectColumn('frameworks', 'user')">
-            User
-          </th>
-          <th ng-class="columnClass('frameworks', 'name')"
-              ng-click="selectColumn('frameworks', 'name')">
-            Name
-          </th>
-          <th ng-class="columnClass('frameworks', 'num_tasks')"
-              ng-click="selectColumn('frameworks', 'num_tasks')">
-            Active Tasks
-          </th>
-          <th ng-class="columnClass('frameworks', 'cpus')"
-              ng-click="selectColumn('frameworks', 'cpus')">
-            CPUs
-          </th>
-          <th ng-class="columnClass('frameworks', 'mem')"
-              ng-click="selectColumn('frameworks', 'mem')">
-            Mem
-          </th>
+          <th data-key="id">ID</th>
+          <th data-key="user">User</th>
+          <th data-key="name">Name</th>
+          <th data-key="num_tasks">Active Tasks</th>
+          <th data-key="cpus">CPUs</th>
+          <th data-key="Mem">Mem</th>
         </tr>
       </thead>
       <tbody>
-        <tr ng-repeat="framework in _.values(slave.frameworks) | orderBy:tables['frameworks'].selected_column:tables['frameworks'].reverse">
+        <tr ng-repeat="framework in $data">
           <td>
             <a href="{{'#/slaves/' + slave_id + '/frameworks/' + framework.id}}">
               {{(framework.id | truncateMesosID) || framework.name}}</a>
@@ -152,37 +135,20 @@
     </table>
 
     <h3>Completed Frameworks</h3>
-    <table class="table table-striped table-bordered table-condensed">
+    <table m-table table-content="slave.completed_frameworks"
+      class="table table-striped table-bordered table-condensed">
       <thead>
         <tr>
-          <th ng-class="columnClass('completed_frameworks', 'id')"
-              ng-click="selectColumn('completed_frameworks', 'id')">
-            ID
-          </th>
-          <th ng-class="columnClass('completed_frameworks', 'user')"
-              ng-click="selectColumn('completed_frameworks', 'user')">
-            User
-          </th>
-          <th ng-class="columnClass('completed_frameworks', 'name')"
-              ng-click="selectColumn('completed_frameworks', 'name')">
-            Name
-          </th>
-          <th ng-class="columnClass('completed_frameworks', 'num_tasks')"
-              ng-click="selectColumn('completed_frameworks', 'num_tasks')">
-            Active Tasks
-          </th>
-          <th ng-class="columnClass('completed_frameworks', 'cpus')"
-              ng-click="selectColumn('completed_frameworks', 'cpus')">
-            CPUs
-          </th>
-          <th ng-class="columnClass('completed_frameworks', 'mem')"
-              ng-click="selectColumn('completed_frameworks', 'mem')">
-            Mem
-          </th>
+          <th data-key="id">ID</th>
+          <th data-key="user">User</th>
+          <th data-key="name">Name</th>
+          <th data-key="tasks.length">Active Tasks</th>
+          <th data-key="resources.cpus">CPUs</th>
+          <th data-key="resources.mem">Mem</th>
         </tr>
       </thead>
       <tbody>
-        <tr ng-repeat="completed_framework in _.values(slave.completed_frameworks) | orderBy:tables['completed_frameworks'].selected_column:tables['completed_frameworks'].reverse">
+        <tr ng-repeat="completed_framework in $data">
           <td>
             <a href="{{'#/slaves/' + slave_id + '/frameworks/' + completed_framework.id}}">
               {{completed_framework.id | truncateMesosID}}</a>

http://git-wip-us.apache.org/repos/asf/mesos/blob/351e02be/src/webui/master/static/slave_executor.html
----------------------------------------------------------------------
diff --git a/src/webui/master/static/slave_executor.html b/src/webui/master/static/slave_executor.html
index e5501e3..81c10cb 100644
--- a/src/webui/master/static/slave_executor.html
+++ b/src/webui/master/static/slave_executor.html
@@ -91,29 +91,18 @@
 
   <div class="col-md-9">
     <h3>Queued Tasks</h3>
-    <table class="table table-striped table-bordered table-condensed">
+    <table m-table table-content="executor.queued_tasks"
+      class="table table-striped table-bordered table-condensed">
       <thead>
         <tr>
-          <th ng-class="columnClass('queued_tasks', 'id')"
-              ng-click="selectColumn('queued_tasks', 'id')">
-            ID
-          </th>
-          <th ng-class="columnClass('queued_tasks', 'name')"
-              ng-click="selectColumn('queued_tasks', 'name')">
-            Name
-          </th>
-          <th ng-class="columnClass('queued_tasks', 'resources.cpus')"
-              ng-click="selectColumn('queued_tasks', 'resources.cpus')">
-            CPUs
-          </th>
-          <th ng-class="columnClass('queued_tasks', 'resources.mem')"
-              ng-click="selectColumn('queued_tasks', 'resources.mem')">
-            Mem
-          </th>
+          <th data-key="id">ID</th>
+          <th data-key="name">Name</th>
+          <th data-key="resources.cpus">CPUs</th>
+          <th data-key="resources.mem">Mem</th>
         </tr>
       </thead>
       <tbody>
-        <tr ng-repeat="queued_task in _.values(executor.queued_tasks) | orderBy:tables['queued_tasks'].selected_column:tables['queued_tasks'].reverse">
+        <tr ng-repeat="queued_task in $data">
           <td>{{queued_task.id}}</td>
           <td>{{queued_task.name}}</td>
           <td>{{queued_task.resources.cpus | number}}</td>
@@ -123,34 +112,20 @@
     </table>
 
     <h3>Tasks</h3>
-    <table class="table table-striped table-bordered table-condensed">
+    <table m-table table-content="executor.tasks"
+      class="table table-striped table-bordered table-condensed">
       <thead>
         <tr>
-          <th ng-class="columnClass('tasks', 'id')"
-              ng-click="selectColumn('tasks', 'id')">
-            ID
-          </th>
-          <th ng-class="columnClass('tasks', 'name')"
-              ng-click="selectColumn('tasks', 'name')">
-            Name
-          </th>
-          <th ng-class="columnClass('tasks', 'state')"
-              ng-click="selectColumn('tasks', 'state')">
-            State
-          </th>
-          <th ng-class="columnClass('tasks', 'resources.cpus')"
-              ng-click="selectColumn('tasks', 'resources.cpus')">
-            CPUs (allocated)
-          </th>
-          <th ng-class="columnClass('tasks', 'resources.mem')"
-              ng-click="selectColumn('tasks', 'resources.mem')">
-            Mem (allocated)
-          </th>
+          <th data-key="id">ID</th>
+          <th data-key="name">Name</th>
+          <th data-key="state">State</th>
+          <th data-key="resources.cpus">CPUs (allocated)</th>
+          <th data-key="resources.mem">Mem (allocated)</th>
           <th></th>
         </tr>
       </thead>
       <tbody>
-        <tr ng-repeat="task in _.values(executor.tasks) | orderBy:tables['tasks'].selected_column:tables['tasks'].reverse">
+        <tr ng-repeat="task in $data">
           <td>{{task.id}}</td>
           <td>{{task.name}}</td>
           <td>{{task.state}}</td>
@@ -167,34 +142,20 @@
     </table>
 
     <h3>Completed Tasks</h3>
-    <table class="table table-striped table-bordered table-condensed">
+    <table m-table table-content="executor.completed_tasks"
+      class="table table-striped table-bordered table-condensed">
       <thead>
         <tr>
-          <th ng-class="columnClass('completed_tasks', 'id')"
-              ng-click="selectColumn('completed_tasks', 'id')">
-            ID
-          </th>
-          <th ng-class="columnClass('completed_tasks', 'name')"
-              ng-click="selectColumn('completed_tasks', 'name')">
-            Name
-          </th>
-          <th ng-class="columnClass('completed_tasks', 'state')"
-              ng-click="selectColumn('completed_tasks', 'state')">
-            State
-          </th>
-          <th ng-class="columnClass('completed_tasks', 'resources.cpus')"
-              ng-click="selectColumn('completed_tasks', 'resources.cpus')">
-            CPUs (allocated)
-          </th>
-          <th ng-class="columnClass('completed_tasks', 'resources.mem')"
-              ng-click="selectColumn('completed_tasks', 'resources.mem')">
-            Mem (allocated)
-          </th>
+          <th data-key="id">ID</th>
+          <th data-key="name">Name</th>
+          <th data-key="state">State</th>
+          <th data-key="resources.cpus">CPUs (allocated)</th>
+          <th data-key="resources.mem">Mem (allocated)</th>
           <th></th>
         </tr>
       </thead>
       <tbody>
-        <tr ng-repeat="completed_task in _.values(executor.completed_tasks) | orderBy:tables['completed_tasks'].selected_column:tables['completed_tasks'].reverse">
+        <tr ng-repeat="completed_task in $data">
           <td>{{completed_task.id}}</td>
           <td>{{completed_task.name}}</td>
           <td>{{completed_task.state}}</td>

http://git-wip-us.apache.org/repos/asf/mesos/blob/351e02be/src/webui/master/static/slave_framework.html
----------------------------------------------------------------------
diff --git a/src/webui/master/static/slave_framework.html b/src/webui/master/static/slave_framework.html
index b597ac5..04a041e 100644
--- a/src/webui/master/static/slave_framework.html
+++ b/src/webui/master/static/slave_framework.html
@@ -70,43 +70,22 @@
 
   <div class="col-md-9">
     <h3>Executors</h3>
-    <table class="table table-striped table-bordered table-condensed">
+    <table m-table table-content="framework.executors"
+      class="table table-striped table-bordered table-condensed">
       <thead>
         <tr>
-          <th ng-class="columnClass('executors', 'id')"
-              ng-click="selectColumn('executors', 'id')">
-            ID
-          </th>
-          <th ng-class="columnClass('executors', 'name')"
-              ng-click="selectColumn('executors', 'name')">
-            Name
-          </th>
-          <th ng-class="columnClass('executors', 'source')"
-              ng-click="selectColumn('executors', 'source')">
-            Source
-          </th>
-          <th ng-class="columnClass('executors', 'tasks.length')"
-              ng-click="selectColumn('executors', 'tasks.length')">
-            Active Tasks
-          </th>
-          <th ng-class="columnClass('executors', 'queued_tasks.length')"
-              ng-click="selectColumn('executors', 'queued_tasks.length')">
-            Queued Tasks
-          </th>
-          <th ng-class="columnClass('executors', 'resources.cpus')"
-              ng-click="selectColumn('executors', 'resources.cpus')">
-            CPUs (Used / Allocated)
-          </th>
-          <th ng-class="columnClass('executors', 'resources.mem')"
-              ng-click="selectColumn('executors', 'resources.mem')">
-            Mem (Used / Allocated)
-          </th>
+          <th data-key="id">ID</th>
+          <th data-key="name">Name</th>
+          <th data-key="source">Source</th>
+          <th data-key="tasks.length">Active Tasks</th>
+          <th data-key="queued_tasks.length">Queued Tasks</th>
+          <th data-key="resources.cpus">CPUs (Used / Allocated)</th>
+          <th data-key="resources.mem">Mem (Used / Allocated)</th>
           <th></th>
         </tr>
       </thead>
-
       <tbody>
-        <tr ng-repeat="executor in framework.executors | orderBy:tables['executors'].selected_column:tables['executors'].reverse">
+        <tr ng-repeat="executor in $data">
           <td>
             <a href="{{'#/slaves/' + slave_id + '/frameworks/' + framework.id + '/executors/' + executor.id}}">
               {{executor.id}}
@@ -131,30 +110,18 @@
     </table>
 
     <h3>Completed Executors</h3>
-    <table class="table table-striped table-bordered table-condensed">
+    <table m-table table-content="framework.completed_executors"
+      class="table table-striped table-bordered table-condensed">
       <thead>
         <tr>
-          <th ng-class="columnClass('completed_executors', 'id')"
-              ng-click="selectColumn('completed_executors', 'id')">
-            ID
-          </th>
-          <th ng-class="columnClass('completed_executors', 'name')"
-              ng-click="selectColumn('completed_executors', 'name')">
-            Name
-          </th>
-          <th ng-class="columnClass('completed_executors', 'source')"
-              ng-click="selectColumn('completed_executors', 'source')">
-            Source
-          </th>
-          <th ng-class="columnClass('completed_executors', 'sandbox')"
-              ng-click="selectColumn('completed_executors', 'sandbox')">
-            Sandbox
-          </th>
+          <th data-key="id">ID</th>
+          <th data-key="name">Name</th>
+          <th data-key="source">Source</th>
+          <th data-key="sandbox">Sandbox</th>
         </tr>
       </thead>
-
       <tbody>
-        <tr ng-repeat="completed_executor in framework.completed_executors | orderBy:tables['completed_executors'].selected_column:tables['completed_executors'].reverse">
+        <tr ng-repeat="completed_executor in $data">
           <td>
             <a href="{{'#/slaves/' + slave_id + '/frameworks/' + framework.id + '/executors/' + completed_executor.id}}">
               {{completed_executor.id}}

http://git-wip-us.apache.org/repos/asf/mesos/blob/351e02be/src/webui/master/static/slaves.html
----------------------------------------------------------------------
diff --git a/src/webui/master/static/slaves.html b/src/webui/master/static/slaves.html
index c729fad..ec31d4b 100644
--- a/src/webui/master/static/slaves.html
+++ b/src/webui/master/static/slaves.html
@@ -8,40 +8,20 @@
 </ol>
 
 <h3>Slaves</h3>
-<table class="table table-striped table-bordered table-condensed">
+<table m-table table-content="slaves"
+  class="table table-striped table-bordered table-condensed">
   <thead>
     <tr>
-      <th ng-class="columnClass('slaves', 'id')"
-          ng-click="selectColumn('slaves', 'id')">
-        ID
-      </th>
-      <th ng-class="columnClass('slaves', 'hostname')"
-          ng-click="selectColumn('slaves', 'hostname')">
-        Host
-      </th>
-      <th ng-class="columnClass('slaves', 'resources.cpus')"
-          ng-click="selectColumn('slaves', 'resources.cpus')">
-        CPUs
-      </th>
-      <th ng-class="columnClass('slaves', 'resources.mem')"
-          ng-click="selectColumn('slaves', 'resources.mem')">
-        Mem
-      </th>
-      <th ng-class="columnClass('slaves', 'resources.disk')"
-          ng-click="selectColumn('slaves', 'resources.disk')">
-        Disk
-      </th>
-      <th ng-class="columnClass('slaves', 'registered_time')"
-          ng-click="selectColumn('slaves', 'registered_time')">
-        Registered
-      </th>
-      <th ng-class="columnClass('slaves', 'reregistered_time')"
-          ng-click="selectColumn('slaves', 'reregistered_time')">
-        Re-Registered
-      </th>
+      <th data-key="id">ID</th>
+      <th data-key="hostname">Host</th>
+      <th data-key="resources.cpus">CPUs</th>
+      <th data-key="resources.mem">Mem</th>
+      <th data-key="resources.disk">Disk</th>
+      <th data-key="registered_time">Registered</th>
+      <th data-key="reregistered_time">Re-Registered</th>
     </tr>
   </thead>
-  <tr ng-repeat="slave in _.values(slaves) | orderBy:tables['slaves'].selected_column:tables['slaves'].reverse">
+  <tr ng-repeat="slave in $data">
     <td>
       <a href="#/slaves/{{slave.id}}">{{slave.id | truncateMesosID}}</a>
       <button class="btn btn-xs btn-default btn-toggle"