http://git-wip-us.apache.org/repos/asf/mesos/blob/c7685917/src/webui/app/controllers.js
----------------------------------------------------------------------
diff --git a/src/webui/app/controllers.js b/src/webui/app/controllers.js
new file mode 100644
index 0000000..7c228a1
--- /dev/null
+++ b/src/webui/app/controllers.js
@@ -0,0 +1,1179 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+(function() {
+  'use strict';
+
+  var mesosApp = angular.module('mesos');
+
+  function hasSelectedText() {
+    if (window.getSelection) {  // All browsers except IE before version 9.
+      var range = window.getSelection();
+      return range.toString().length > 0;
+    }
+    return false;
+  }
+
+  // Returns the URL prefix for an agent, there are two cases
+  // to consider:
+  //
+  //   (1) Some endpoints for the agent process itself require
+  //       the agent PID.name (processId) in the path, this is
+  //       to ensure that the webui works correctly when running
+  //       against mesos-local or other instances of multiple agents
+  //       running within the same "host:port":
+  //
+  //         //hostname:port/slave(1)
+  //         //hostname:port/slave(2)
+  //         ...
+  //
+  //   (2) Some endpoints for other components in the agent
+  //       do not require the agent PID.name in the path, since
+  //       a single endpoint serves multiple agents within the
+  //       same process. In this case we just return:
+  //
+  //         //hostname:port
+  //
+  // Note that there are some clashing issues in mesos-local
+  // (e.g., hosting '/slave/log' for each agent log, we don't
+  // namespace metrics within '/metrics/snapshot', etc).
+  function agentURLPrefix(agent, includeProcessId) {
+    var port = agent.pid.substring(agent.pid.lastIndexOf(':') + 1);
+    var processId = agent.pid.substring(0, agent.pid.indexOf('@'));
+
+    var url = '//' + agent.hostname + ':' + port;
+
+    if (includeProcessId) {
+      url += '/' + processId;
+    }
+
+    return url;
+  }
+
+  // Invokes the pailer, building the endpoint URL with the specified urlPrefix
+  // and path.
+  function pailer(urlPrefix, path, window_title) {
+    var url = urlPrefix + '/files/read?path=' + path;
+
+    // The randomized `storageKey` is removed from `localStorage` once the
+    // pailer window loads the URL into its `sessionStorage`, therefore
+    // the probability of collisions is low and we do not use a uuid.
+    var storageKey = Math.random().toString(36).substr(2, 8);
+
+    // Store the target URL in `localStorage` which is
+    // accessed by the pailer window when opened.
+    localStorage.setItem(storageKey, url);
+
+    var pailer =
+      window.open('app/shared/pailer.html', storageKey, 'width=580px, 
height=700px');
+
+    // Need to use window.onload instead of document.ready to make
+    // sure the title doesn't get overwritten.
+    pailer.onload = function() {
+      pailer.document.title = window_title;
+    };
+  }
+
+
+  function updateInterval(num_agents) {
+    // TODO(bmahler): Increasing the update interval for large clusters
+    // is done purely to mitigate webui performance issues. Ideally we can
+    // keep a consistently fast rate for updating statistical information.
+    // For the full system state updates, it may make sense to break
+    // it up using pagination and/or splitting the endpoint.
+    if (num_agents < 500) {
+      return 10000;
+    } else if (num_agents < 1000) {
+      return 20000;
+    } else if (num_agents < 5000) {
+      return 60000;
+    } else if (num_agents < 10000) {
+      return 120000;
+    } else if (num_agents < 15000) {
+      return 240000;
+    } else if (num_agents < 20000) {
+      return 480000;
+    } else {
+      return 960000;
+    }
+  }
+
+  // Set the task sandbox directory for use by the WebUI.
+  function setTaskSandbox(executor) {
+    _.each(
+        [executor.tasks, executor.queued_tasks, executor.completed_tasks],
+        function(tasks) {
+      _.each(tasks, function(task) {
+        if (executor.type === 'DEFAULT') {
+          task.directory = executor.directory + '/tasks/' + task.id;
+        } else {
+          task.directory = executor.directory;
+        }
+      });
+    });
+  }
+
+
+  // Update the outermost scope with the new state.
+  function updateState($scope, $timeout, state) {
+    // Don't do anything if the state hasn't changed.
+    if ($scope.state == state) {
+      return true; // Continue polling.
+    }
+
+    $scope.state = state;
+
+    // A cluster is named if the state returns a non-empty string name.
+    // Track whether this cluster is named in a Boolean for display purposes.
+    $scope.clusterNamed = !!$scope.state.cluster;
+
+    // Check for selected text, and allow up to 20 seconds to pass before
+    // potentially wiping the user highlighted text.
+    // TODO(bmahler): This is to avoid the annoying loss of highlighting when
+    // the tables update. Once we can have tighter granularity control on the
+    // angular.js dynamic table updates, we should remove this hack.
+    $scope.time_since_update += $scope.delay;
+
+    if (hasSelectedText() && $scope.time_since_update < 20000) {
+      return true;
+    }
+
+    // Pass this pollTime to all relativeDate calls to make them all relative 
to
+    // the same moment in time.
+    //
+    // If relativeDate is called without a reference time, it instantiates a 
new
+    // Date to be the reference. Since there can be hundreds of dates on a 
given
+    // page, they would all be relative to slightly different moments in time.
+    $scope.pollTime = new Date();
+
+    // Update the maps.
+    $scope.agents = {};
+    $scope.frameworks = {};
+    $scope.offers = {};
+    $scope.completed_frameworks = {};
+    $scope.active_tasks = [];
+    $scope.unreachable_tasks = [];
+    $scope.completed_tasks = [];
+
+    // Update the stats.
+    $scope.cluster = $scope.state.cluster;
+    $scope.total_cpus = 0;
+    $scope.total_gpus = 0;
+    $scope.total_mem = 0;
+    $scope.total_disk = 0;
+    $scope.used_cpus = 0;
+    $scope.used_gpus = 0;
+    $scope.used_mem = 0;
+    $scope.used_disk = 0;
+    $scope.offered_cpus = 0;
+    $scope.offered_gpus = 0;
+    $scope.offered_mem = 0;
+    $scope.offered_disk = 0;
+
+    $scope.activated_agents = $scope.state.activated_slaves;
+    $scope.deactivated_agents = $scope.state.deactivated_slaves;
+    $scope.unreachable_agents = $scope.state.unreachable_slaves;
+
+    _.each($scope.state.slaves, function(agent) {
+      $scope.agents[agent.id] = agent;
+      $scope.total_cpus += agent.resources.cpus;
+      $scope.total_gpus += agent.resources.gpus;
+      $scope.total_mem += agent.resources.mem;
+      $scope.total_disk += agent.resources.disk;
+    });
+
+    var setTaskMetadata = function(task) {
+      if (!task.executor_id) {
+        task.executor_id = task.id;
+      }
+      if (task.statuses.length > 0) {
+          var firstStatus = task.statuses[0];
+          if (!isStateTerminal(firstStatus.state)) {
+              task.start_time = firstStatus.timestamp * 1000;
+          }
+          var lastStatus = task.statuses[task.statuses.length - 1];
+          if (isStateTerminal(task.state)) {
+              task.finish_time = lastStatus.timestamp * 1000;
+          }
+          task.healthy = lastStatus.healthy;
+      }
+    };
+
+    var isStateTerminal = function(taskState) {
+      var terminalStates = [
+          'TASK_ERROR',
+          'TASK_FAILED',
+          'TASK_FINISHED',
+          'TASK_KILLED',
+          'TASK_LOST',
+          'TASK_DROPPED',
+          'TASK_GONE',
+          'TASK_GONE_BY_OPERATOR'
+      ];
+      return terminalStates.indexOf(taskState) > -1;
+    };
+
+    _.each($scope.state.frameworks, function(framework) {
+      $scope.frameworks[framework.id] = framework;
+
+      // Fill in the `roles` field for non-MULTI_ROLE schedulers.
+      if (framework.role) {
+        framework.roles = [framework.role];
+      }
+
+      _.each(framework.offers, function(offer) {
+        $scope.offers[offer.id] = offer;
+        $scope.offered_cpus += offer.resources.cpus;
+        $scope.offered_gpus += offer.resources.gpus;
+        $scope.offered_mem += offer.resources.mem;
+        $scope.offered_disk += offer.resources.disk;
+        offer.framework_name = $scope.frameworks[offer.framework_id].name;
+        offer.hostname = $scope.agents[offer.slave_id].hostname;
+      });
+
+      $scope.used_cpus += framework.resources.cpus;
+      $scope.used_gpus += framework.resources.gpus;
+      $scope.used_mem += framework.resources.mem;
+      $scope.used_disk += framework.resources.disk;
+
+      framework.cpus_share = 0;
+      if ($scope.total_cpus > 0) {
+        framework.cpus_share = framework.used_resources.cpus / 
$scope.total_cpus;
+      }
+
+      framework.gpus_share = 0;
+      if ($scope.total_gpus > 0) {
+        framework.gpus_share = framework.used_resources.gpus / 
$scope.total_gpus;
+      }
+
+      framework.mem_share = 0;
+      if ($scope.total_mem > 0) {
+        framework.mem_share = framework.used_resources.mem / $scope.total_mem;
+      }
+
+      framework.disk_share = 0;
+      if ($scope.total_disk > 0) {
+        framework.disk_share = framework.used_resources.disk / 
$scope.total_disk;
+      }
+
+      framework.max_share = Math.max(
+          framework.cpus_share,
+          framework.gpus_share,
+          framework.mem_share,
+          framework.disk_share);
+
+      // If the executor ID is empty, this is a command executor with an
+      // internal executor ID generated from the task ID.
+      // TODO(brenden): Remove this once
+      // https://issues.apache.org/jira/browse/MESOS-527 is fixed.
+      _.each(framework.tasks, setTaskMetadata);
+      _.each(framework.unreachable_tasks, setTaskMetadata);
+      _.each(framework.completed_tasks, setTaskMetadata);
+
+      // TODO(bmahler): Add per-framework metrics to the master so that
+      // the webui does not need to loop over all tasks!
+      framework.running_tasks = 0;
+      framework.staging_tasks = 0;
+      framework.starting_tasks = 0;
+      framework.killing_tasks = 0;
+
+      _.each(framework.tasks, function(task) {
+        switch (task.state) {
+            case "TASK_STAGING": framework.staging_tasks += 1; break;
+            case "TASK_STARTING": framework.starting_tasks += 1; break;
+            case "TASK_RUNNING": framework.running_tasks += 1; break;
+            case "TASK_KILLING": framework.killing_tasks += 1; break;
+            default: break;
+        }
+      })
+
+      framework.finished_tasks = 0;
+      framework.killed_tasks = 0;
+      framework.failed_tasks = 0;
+      framework.lost_tasks = 0;
+
+      _.each(framework.completed_tasks, function(task) {
+        switch (task.state) {
+            case "TASK_FINISHED": framework.finished_tasks += 1; break;
+            case "TASK_KILLED": framework.killed_tasks += 1; break;
+            case "TASK_FAILED": framework.failed_tasks += 1; break;
+            case "TASK_LOST": framework.lost_tasks += 1; break;
+            default: break;
+        }
+      })
+
+      $scope.active_tasks = $scope.active_tasks.concat(framework.tasks);
+      $scope.unreachable_tasks = 
$scope.unreachable_tasks.concat(framework.unreachable_tasks);
+      $scope.completed_tasks =
+        $scope.completed_tasks.concat(framework.completed_tasks);
+    });
+
+    _.each($scope.state.completed_frameworks, function(framework) {
+      $scope.completed_frameworks[framework.id] = framework;
+
+      // Fill in the `roles` field for non-MULTI_ROLE schedulers.
+      if (framework.role) {
+        framework.roles = [framework.role];
+      }
+
+      _.each(framework.completed_tasks, setTaskMetadata);
+    });
+
+    $scope.used_cpus -= $scope.offered_cpus;
+    $scope.used_gpus -= $scope.offered_gpus;
+    $scope.used_mem -= $scope.offered_mem;
+    $scope.used_disk -= $scope.offered_disk;
+
+    $scope.idle_cpus = $scope.total_cpus - ($scope.offered_cpus + 
$scope.used_cpus);
+    $scope.idle_gpus = $scope.total_gpus - ($scope.offered_gpus + 
$scope.used_gpus);
+    $scope.idle_mem = $scope.total_mem - ($scope.offered_mem + 
$scope.used_mem);
+    $scope.idle_disk = $scope.total_disk - ($scope.offered_disk + 
$scope.used_disk);
+
+    $scope.time_since_update = 0;
+    $scope.$broadcast('state_updated');
+
+    return true; // Continue polling.
+  }
+
+  // Update the outermost scope with the metrics/snapshot endpoint.
+  function updateMetrics($scope, $timeout, metrics) {
+    $scope.staging_tasks = metrics['master/tasks_staging'];
+    $scope.starting_tasks = metrics['master/tasks_starting'];
+    $scope.running_tasks = metrics['master/tasks_running'];
+    $scope.killing_tasks = metrics['master/tasks_killing'];
+    $scope.finished_tasks = metrics['master/tasks_finished'];
+    $scope.killed_tasks = metrics['master/tasks_killed'];
+    $scope.failed_tasks = metrics['master/tasks_failed'];
+    $scope.lost_tasks = metrics['master/tasks_lost'];
+
+    return true; // Continue polling.
+  }
+
+
+  // Main controller that can be used to handle "global" events. E.g.,:
+  //     $scope.$on('$afterRouteChange', function() { ...; });
+  //
+  // In addition, the MainCtrl encapsulates the "view", allowing the
+  // active controller/view to easily access anything in scope (e.g.,
+  // the state).
+  mesosApp.controller('MainCtrl', [
+      '$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
+    // AngularJS expressions.
+    $scope._ = _;
+    $scope.stringify = JSON.stringify;
+    $scope.encodeURIComponent = encodeURIComponent;
+    $scope.basename = function(path) {
+      // This is only a basic version of basename that handles the cases we 
care
+      // about, rather than duplicating unix basename functionality perfectly.
+      if (path === '/') {
+        return path;  // Handle '/'.
+      }
+
+      // Strip a trailing '/' if present.
+      if (path.length > 0 && path.lastIndexOf('/') === (path.length - 1)) {
+        path = path.substr(0, path.length - 1);
+      }
+      return path.substr(path.lastIndexOf('/') + 1);
+    };
+
+    $scope.$location = $location;
+    $scope.delay = 2000;
+    $scope.retry = 0;
+    $scope.time_since_update = 0;
+    $scope.isErrorModalOpen = false;
+
+    // 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.
+    var NAVBAR_PATHS = [
+      {
+        pathRegexp: /^\/agents/,
+        tab: 'agents'
+      },
+      {
+        pathRegexp: /^\/frameworks/,
+        tab: 'frameworks'
+      },
+      {
+        pathRegexp: /^\/roles/,
+        tab: 'roles'
+      },
+      {
+        pathRegexp: /^\/offers/,
+        tab: 'offers'
+      },
+      {
+        pathRegexp: /^\/maintenance/,
+        tab: 'maintenance'
+      }
+    ];
+
+    // Set the active tab on route changes according to NAVBAR_PATHS.
+    $scope.$on('$routeChangeSuccess', function(event, current) {
+      var path = current.$$route.originalPath;
+
+      // Use _.some so the loop can exit on the first `pathRegexp` match.
+      var matched = _.some(NAVBAR_PATHS, function(nav) {
+        if (path.match(nav.pathRegexp)) {
+          $scope.navbarActiveTab = nav.tab;
+          return true;
+        }
+      });
+
+      if (!matched) $scope.navbarActiveTab = null;
+    });
+
+    var leadingMasterURL = function(path) {
+      // Use current location as address in case we could not find the
+      // leading master.
+      var address = location.hostname + ':' + location.port;
+      if ($scope.state && $scope.state.leader_info) {
+          address = $scope.state.leader_info.hostname + ':' +
+                    $scope.state.leader_info.port;
+      }
+
+      return '//' + address + path;
+    }
+
+    var popupErrorModal = function() {
+      if ($scope.delay >= 128000) {
+        $scope.delay = 2000;
+      } else {
+        $scope.delay = $scope.delay * 2;
+      }
+
+      $scope.isErrorModalOpen = true;
+
+      var errorModal = $modal.open({
+        controller: function($scope, $modalInstance, scope) {
+          // Give the modal reference to the root scope so it can access the
+          // `retry` variable. It needs to be passed by reference, not by
+          // value, since its value is changed outside the scope of the
+          // modal.
+          $scope.rootScope = scope;
+        },
+        resolve: {
+          scope: function() { return $scope; }
+        },
+        templateUrl: "template/dialog/master-gone.html"
+      });
+
+      // Make it such that everytime we hide the error-modal, we stop the
+      // countdown and restart the polling.
+      errorModal.result.then(function() {
+        $scope.isErrorModalOpen = false;
+
+        if ($scope.countdown != null) {
+          if ($timeout.cancel($scope.countdown)) {
+            // Restart since they cancelled the countdown.
+            $scope.delay = 2000;
+          }
+        }
+
+        // Start polling again, but do it asynchronously (and wait at
+        // least a second because otherwise the error-modal won't get
+        // properly shown).
+        $timeout(pollState, 1000);
+        $timeout(pollMetrics, 1000);
+      });
+
+      $scope.retry = $scope.delay;
+      var countdown = function() {
+        if ($scope.retry === 0) {
+          errorModal.close();
+        } else {
+          $scope.retry = $scope.retry - 1000;
+          $scope.countdown = $timeout(countdown, 1000);
+        }
+      };
+      countdown();
+    };
+
+    var pollState = function() {
+      // When the current master is not the leader, the request is redirected 
to
+      // the leading master automatically. This would cause a CORS error if we
+      // use XMLHttpRequest here. To avoid the CORS error, we use JSONP as a
+      // workaround. Please refer to MESOS-5911 for further details.
+      $http.jsonp(leadingMasterURL('/master/state?jsonp=JSON_CALLBACK'))
+        .success(function(response) {
+          if (updateState($scope, $timeout, response)) {
+            $scope.delay = updateInterval(_.size($scope.agents));
+            $timeout(pollState, $scope.delay);
+          }
+        })
+        .error(function() {
+          if ($scope.isErrorModalOpen === false) {
+            popupErrorModal();
+          }
+        });
+    };
+
+    var pollMetrics = function() {
+      $http.jsonp(leadingMasterURL('/metrics/snapshot?jsonp=JSON_CALLBACK'))
+        .success(function(response) {
+          if (updateMetrics($scope, $timeout, response)) {
+            $scope.delay = updateInterval(_.size($scope.agents));
+            $timeout(pollMetrics, $scope.delay);
+          }
+        })
+        .error(function(message, code) {
+          if ($scope.isErrorModalOpen === false) {
+            // If return code is 401 or 403 the user is unauthorized to reach
+            // the endpoint, which is not a connection error.
+            if ([401, 403].indexOf(code) < 0) {
+              popupErrorModal();
+            }
+          }
+        });
+    };
+
+    pollState();
+    pollMetrics();
+  }]);
+
+  mesosApp.controller('HomeCtrl', function($scope, $http) {
+    var hostname = $scope.$location.host() + ':' + $scope.$location.port();
+
+    var update = function() {
+      $scope.streamLogs = function(_$event) {
+        pailer(
+            '//' + hostname,
+            '/master/log',
+            'Mesos Master (' + hostname + ')');
+      };
+
+      // We must query the '/flags' endpoint of *this* master since
+      // `$scope.state` contains the leader master state.
+      $http.jsonp('//' + hostname + '/flags?jsonp=JSON_CALLBACK')
+        .success(function (response) {
+          // The master attaches a "/master/log" file when either
+          // of these flags are set.
+          $scope.log_file_attached = response.flags.external_log_file || 
response.flags.log_dir;
+        })
+        .error(function(reason) {
+          $scope.alert_message = 'Failed to get master flags: ' + reason;
+          $('#alert').show();
+        });
+    };
+
+    if ($scope.state) {
+      update();
+    }
+
+    var removeListener = $scope.$on('state_updated', update);
+    $scope.$on('$routeChangeStart', removeListener);
+  });
+
+  mesosApp.controller('FrameworksCtrl', function() {});
+
+  mesosApp.controller('RolesCtrl', function($scope, $http) {
+    var update = function() {
+      // TODO(haosdent): Send requests to the leading master directly
+      // once `leadingMasterURL` is public.
+      $http.jsonp('master/roles?jsonp=JSON_CALLBACK')
+      .success(function(response) {
+        $scope.roles = response;
+      })
+      .error(function() {
+        if ($scope.isErrorModalOpen === false) {
+          popupErrorModal();
+        }
+      });
+    };
+
+    if ($scope.state) {
+      update();
+    }
+
+    var removeListener = $scope.$on('state_updated', update);
+    $scope.$on('$routeChangeStart', removeListener);
+  });
+
+  mesosApp.controller('OffersCtrl', function() {});
+
+  mesosApp.controller('MaintenanceCtrl', function($scope, $http) {
+    var update = function() {
+      // TODO(haosdent): Send requests to the leading master directly
+      // once `leadingMasterURL` is public.
+      $http.jsonp('master/maintenance/schedule?jsonp=JSON_CALLBACK')
+      .success(function(response) {
+        $scope.maintenance = response;
+      })
+      .error(function() {
+        if ($scope.isErrorModalOpen === false) {
+          popupErrorModal();
+        }
+      });
+    };
+
+    if ($scope.state) {
+      update();
+    }
+
+    var removeListener = $scope.$on('state_updated', update);
+    $scope.$on('$routeChangeStart', removeListener);
+  });
+
+  mesosApp.controller('FrameworkCtrl', function($scope, $routeParams) {
+    var update = function() {
+      if ($routeParams.id in $scope.completed_frameworks) {
+        $scope.framework = $scope.completed_frameworks[$routeParams.id];
+        $scope.alert_message = 'This framework has terminated!';
+        $('#alert').show();
+        $('#framework').show();
+      } else if ($routeParams.id in $scope.frameworks) {
+        $scope.framework = $scope.frameworks[$routeParams.id];
+        $('#framework').show();
+      } else {
+        $scope.alert_message = 'No framework found with ID: ' + 
$routeParams.id;
+        $('#alert').show();
+      }
+    };
+
+    if ($scope.state) {
+      update();
+    }
+
+    var removeListener = $scope.$on('state_updated', update);
+    $scope.$on('$routeChangeStart', removeListener);
+  });
+
+
+  mesosApp.controller('AgentsCtrl', function() {});
+
+
+  mesosApp.controller('AgentCtrl', [
+      '$scope', '$routeParams', '$http', '$q', '$timeout', 'top',
+      function($scope, $routeParams, $http, $q, $timeout, $top) {
+    $scope.agent_id = $routeParams.agent_id;
+
+    var update = function() {
+      if (!($routeParams.agent_id in $scope.agents)) {
+        $scope.alert_message = 'No agent found with ID: ' + 
$routeParams.agent_id;
+        $('#alert').show();
+        return;
+      }
+
+      var agent = $scope.agents[$routeParams.agent_id];
+
+      $scope.streamLogs = function(_$event) {
+        pailer(agentURLPrefix(agent, false), '/slave/log', 'Mesos Agent (' + 
agent.id + ')');
+      };
+
+
+      // Set up polling for the monitor if this is the first update.
+      if (!$top.started()) {
+        $top.start(
+          agentURLPrefix(agent, true) + 
'/monitor/statistics?jsonp=JSON_CALLBACK',
+          $scope
+        );
+      }
+
+      $http.jsonp(agentURLPrefix(agent, true) + '/state?jsonp=JSON_CALLBACK')
+        .success(function (response) {
+          $scope.state = response;
+
+          $scope.agent = {};
+          $scope.agent.frameworks = {};
+          $scope.agent.completed_frameworks = {};
+          $scope.agent.url_prefix = agentURLPrefix(agent, false);
+
+          // The agent attaches a "/slave/log" file when either
+          // of these flags are set.
+          $scope.agent.log_file_attached = $scope.state.external_log_file || 
$scope.state.log_dir;
+
+          // Convert the reserved resources map into an array for inclusion
+          // in an `ng-repeat` table.
+          $scope.agent.reserved_resources_as_array = 
_($scope.state.reserved_resources)
+            .map(function(reservation, role) {
+              reservation.role = role;
+              return reservation;
+            });
+
+          // Computes framework stats by setting new attributes on the 
'framework'
+          // object.
+          function computeFrameworkStats(framework) {
+            framework.num_tasks = 0;
+            framework.cpus = 0;
+            framework.gpus = 0;
+            framework.mem = 0;
+            framework.disk = 0;
+
+            _.each(framework.executors, function(executor) {
+              framework.num_tasks += _.size(executor.tasks);
+              framework.cpus += executor.resources.cpus;
+              framework.gpus += executor.resources.gpus;
+              framework.mem += executor.resources.mem;
+              framework.disk += executor.resources.disk;
+            });
+          }
+
+          // Compute framework stats and update agent's mappings of those
+          // frameworks.
+          _.each($scope.state.frameworks, function(framework) {
+            $scope.agent.frameworks[framework.id] = framework;
+            computeFrameworkStats(framework);
+
+            // Fill in the `roles` field for non-MULTI_ROLE schedulers.
+            if (framework.role) {
+              framework.roles = [framework.role];
+            }
+          });
+
+          _.each($scope.state.completed_frameworks, function(framework) {
+            $scope.agent.completed_frameworks[framework.id] = framework;
+            computeFrameworkStats(framework);
+
+            // Fill in the `roles` field for non-MULTI_ROLE schedulers.
+            if (framework.role) {
+              framework.roles = [framework.role];
+            }
+          });
+
+          $scope.state.allocated_resources = {};
+          $scope.state.allocated_resources.cpus = 0;
+          $scope.state.allocated_resources.gpus = 0;
+          $scope.state.allocated_resources.mem = 0;
+          $scope.state.allocated_resources.disk = 0;
+
+          // Currently the agent does not expose the total allocated
+          // resources across all frameworks, so we sum manually.
+          _.each($scope.state.frameworks, function(framework) {
+            $scope.state.allocated_resources.cpus += framework.cpus;
+            $scope.state.allocated_resources.gpus += framework.gpus;
+            $scope.state.allocated_resources.mem += framework.mem;
+            $scope.state.allocated_resources.disk += framework.disk;
+          });
+
+          $('#agent').show();
+        })
+        .error(function(reason) {
+          $scope.alert_message = 'Failed to get agent usage / state: ' + 
reason;
+          $('#alert').show();
+        });
+
+      $http.jsonp(agentURLPrefix(agent, false) + 
'/metrics/snapshot?jsonp=JSON_CALLBACK')
+        .success(function (response) {
+          $scope.staging_tasks = response['slave/tasks_staging'];
+          $scope.starting_tasks = response['slave/tasks_starting'];
+          $scope.running_tasks = response['slave/tasks_running'];
+          $scope.killing_tasks = response['slave/tasks_killing'];
+          $scope.finished_tasks = response['slave/tasks_finished'];
+          $scope.killed_tasks = response['slave/tasks_killed'];
+          $scope.failed_tasks = response['slave/tasks_failed'];
+          $scope.lost_tasks = response['slave/tasks_lost'];
+        })
+        .error(function(reason) {
+          $scope.alert_message = 'Failed to get agent metrics: ' + reason;
+          $('#alert').show();
+        });
+    };
+
+    if ($scope.state) {
+      update();
+    }
+
+    var removeListener = $scope.$on('state_updated', update);
+    $scope.$on('$routeChangeStart', removeListener);
+  }]);
+
+
+  mesosApp.controller('AgentFrameworkCtrl', [
+      '$scope', '$routeParams', '$http', '$q', '$timeout', 'top',
+      function($scope, $routeParams, $http, $q, $timeout, $top) {
+    $scope.agent_id = $routeParams.agent_id;
+    $scope.framework_id = $routeParams.framework_id;
+
+    var update = function() {
+      if (!($routeParams.agent_id in $scope.agents)) {
+        $scope.alert_message = 'No agent found with ID: ' + 
$routeParams.agent_id;
+        $('#alert').show();
+        return;
+      }
+
+      var agent = $scope.agents[$routeParams.agent_id];
+
+      // Set up polling for the monitor if this is the first update.
+      if (!$top.started()) {
+        $top.start(
+          agentURLPrefix(agent, true) + 
'/monitor/statistics?jsonp=JSON_CALLBACK',
+          $scope
+        );
+      }
+
+      $http.jsonp(agentURLPrefix(agent, true) + '/state?jsonp=JSON_CALLBACK')
+        .success(function (response) {
+          $scope.state = response;
+
+          $scope.agent = {};
+
+          function matchFramework(framework) {
+            return $scope.framework_id === framework.id;
+          }
+
+          // Find the framework; it's either active or completed.
+          $scope.framework =
+              _.find($scope.state.frameworks, matchFramework) ||
+              _.find($scope.state.completed_frameworks, matchFramework);
+
+          if (!$scope.framework) {
+            $scope.alert_message = 'No framework found with ID: ' + 
$routeParams.framework_id;
+            $('#alert').show();
+            return;
+          }
+
+          // Fill in the `roles` field for non-MULTI_ROLE schedulers.
+          if ($scope.framework.role) {
+            $scope.framework.roles = [$scope.framework.role];
+          }
+
+          // Compute the framework stats.
+          $scope.framework.num_tasks = 0;
+          $scope.framework.cpus = 0;
+          $scope.framework.gpus = 0;
+          $scope.framework.mem = 0;
+          $scope.framework.disk = 0;
+
+          _.each($scope.framework.executors, function(executor) {
+            $scope.framework.num_tasks += _.size(executor.tasks);
+            $scope.framework.cpus += executor.resources.cpus;
+            $scope.framework.gpus += executor.resources.gpus;
+            $scope.framework.mem += executor.resources.mem;
+            $scope.framework.disk += executor.resources.disk;
+
+            // If 'role' is not present in executor, we are talking
+            // to a non-MULTI_ROLE capable agent. This means that we
+            // can use the 'role' of the framework.
+            executor.role = executor.role || $scope.framework.role;
+          });
+
+          $('#agent').show();
+        })
+        .error(function (reason) {
+          $scope.alert_message = 'Failed to get agent usage / state: ' + 
reason;
+          $('#alert').show();
+        });
+    };
+
+    if ($scope.state) {
+      update();
+    }
+
+    var removeListener = $scope.$on('state_updated', update);
+    $scope.$on('$routeChangeStart', removeListener);
+  }]);
+
+
+  mesosApp.controller('AgentExecutorCtrl', [
+      '$scope', '$routeParams', '$http', '$q', '$timeout', 'top',
+      function($scope, $routeParams, $http, $q, $timeout, $top) {
+    $scope.agent_id = $routeParams.agent_id;
+    $scope.framework_id = $routeParams.framework_id;
+    $scope.executor_id = $routeParams.executor_id;
+
+    var update = function() {
+      if (!($routeParams.agent_id in $scope.agents)) {
+        $scope.alert_message = 'No agent found with ID: ' + 
$routeParams.agent_id;
+        $('#alert').show();
+        return;
+      }
+
+      var agent = $scope.agents[$routeParams.agent_id];
+
+      // Set up polling for the monitor if this is the first update.
+      if (!$top.started()) {
+        $top.start(
+          agentURLPrefix(agent, true) + 
'/monitor/statistics?jsonp=JSON_CALLBACK',
+          $scope
+        );
+      }
+
+      $http.jsonp(agentURLPrefix(agent, true) + '/state?jsonp=JSON_CALLBACK')
+        .success(function (response) {
+          $scope.state = response;
+
+          $scope.agent = {};
+
+          function matchFramework(framework) {
+            return $scope.framework_id === framework.id;
+          }
+
+          // Find the framework; it's either active or completed.
+          $scope.framework =
+            _.find($scope.state.frameworks, matchFramework) ||
+            _.find($scope.state.completed_frameworks, matchFramework);
+
+          if (!$scope.framework) {
+            $scope.alert_message = 'No framework found with ID: ' + 
$routeParams.framework_id;
+            $('#alert').show();
+            return;
+          }
+
+          function matchExecutor(executor) {
+            return $scope.executor_id === executor.id;
+          }
+
+          function setRole(tasks) {
+            _.each(tasks, function(task) {
+              task.role = $scope.framework.role;
+            });
+          }
+
+          function setHealth(tasks) {
+            _.each(tasks, function(task) {
+              var lastStatus = _.last(task.statuses);
+              if (lastStatus) {
+                task.healthy = lastStatus.healthy;
+              }
+            })
+          }
+
+          // Look for the executor; it's either active or completed.
+          $scope.executor =
+            _.find($scope.framework.executors, matchExecutor) ||
+            _.find($scope.framework.completed_executors, matchExecutor);
+
+          if (!$scope.executor) {
+            $scope.alert_message = 'No executor found with ID: ' + 
$routeParams.executor_id;
+            $('#alert').show();
+            return;
+          }
+
+          // If 'role' is not present in the task, we are talking
+          // to a non-MULTI_ROLE capable agent. This means that we
+          // can use the 'role' of the framework.
+          if (!("role" in $scope.executor)) {
+            $scope.executor.role = $scope.framework.role;
+
+            setRole($scope.executor.tasks);
+            setRole($scope.executor.queued_tasks);
+            setRole($scope.executor.completed_tasks);
+          }
+
+          setHealth($scope.executor.tasks);
+          setTaskSandbox($scope.executor);
+
+          $('#agent').show();
+        })
+        .error(function (reason) {
+          $scope.alert_message = 'Failed to get agent usage / state: ' + 
reason;
+          $('#alert').show();
+        });
+    };
+
+    if ($scope.state) {
+      update();
+    }
+
+    var removeListener = $scope.$on('state_updated', update);
+    $scope.$on('$routeChangeStart', removeListener);
+  }]);
+
+
+  // Reroutes requests like:
+  //   * 
'/agents/:agent_id/frameworks/:framework_id/executors/:executor_id/browse'
+  //   * 
'/agents/:agent_id/frameworks/:framework_id/executors/:executor_id/tasks/:task_id/browse'
+  // to the sandbox directory of the executor or the task respectively. This
+  // requires a second request because the directory to browse is known by the
+  // agent but not by the master. Request the directory from the agent, and 
then
+  // redirect to it.
+  //
+  // TODO(ssorallen): Add `executor.directory` to the master's state endpoint
+  // output so this controller of rerouting is no longer necessary.
+  mesosApp.controller('AgentTaskAndExecutorRerouterCtrl',
+      function($alert, $http, $location, $routeParams, $scope, $window) {
+
+    function goBack(flashMessageOrOptions) {
+      if (flashMessageOrOptions) {
+        $alert.danger(flashMessageOrOptions);
+      }
+
+      if ($window.history.length > 1) {
+        // If the browser has something in its history, just go back.
+        $window.history.back();
+      } else {
+        // Otherwise navigate to the framework page, which is likely the
+        // previous page anyway.
+        $location.path('/frameworks/' + $routeParams.framework_id).replace();
+      }
+    }
+
+    var reroute = function() {
+      var agent = $scope.agents[$routeParams.agent_id];
+
+      // If the agent doesn't exist, send the user back.
+      if (!agent) {
+        return goBack("Agent with ID '" + $routeParams.agent_id + "' does not 
exist.");
+      }
+
+      // Request agent details to get access to the route executor's 
"directory"
+      // to navigate directly to the executor's sandbox.
+      var url = agentURLPrefix(agent, true) + '/state?jsonp=JSON_CALLBACK';
+      $http.jsonp(url)
+        .success(function(response) {
+
+          function matchFramework(framework) {
+            return $routeParams.framework_id === framework.id;
+          }
+
+          var framework =
+            _.find(response.frameworks, matchFramework) ||
+            _.find(response.completed_frameworks, matchFramework);
+
+          if (!framework) {
+            return goBack(
+              "Framework with ID '" + $routeParams.framework_id +
+                "' does not exist on agent with ID '" + $routeParams.agent_id +
+                "'."
+            );
+          }
+
+          function matchExecutor(executor) {
+            return $routeParams.executor_id === executor.id;
+          }
+
+          var executor =
+            _.find(framework.executors, matchExecutor) ||
+            _.find(framework.completed_executors, matchExecutor);
+
+          if (!executor) {
+            return goBack(
+              "Executor with ID '" + $routeParams.executor_id +
+                "' does not exist on agent with ID '" + $routeParams.agent_id +
+                "'."
+            );
+          }
+
+          var sandboxDirectory = executor.directory;
+
+          function matchTask(task) {
+            return $routeParams.task_id === task.id;
+          }
+
+          // Continue to navigate to the task's sandbox if the task id is
+          // specified in route parameters.
+          if ($routeParams.task_id) {
+            setTaskSandbox(executor);
+
+            var task =
+              _.find(executor.tasks, matchTask) ||
+              _.find(executor.queued_tasks, matchTask) ||
+              _.find(executor.completed_tasks, matchTask);
+
+            if (!task) {
+              return goBack(
+                "Task with ID '" + $routeParams.task_id +
+                  "' does not exist on agent with ID '" + 
$routeParams.agent_id +
+                  "'."
+              );
+            }
+
+            sandboxDirectory = task.directory;
+          }
+
+          // Navigate to a path like '/agents/:id/browse?path=%2Ftmp%2F', the
+          // recognized "browse" endpoint for an agent.
+          $location.path('/agents/' + $routeParams.agent_id + '/browse')
+            .search({path: sandboxDirectory})
+            .replace();
+        })
+        .error(function(_response) {
+          $alert.danger({
+            bullets: [
+             "The agent is not accessible",
+             "The agent timed out or went offline"
+            ],
+            message: "Potential reasons:",
+            title: "Failed to connect to agent '" + $routeParams.agent_id +
+              "' on '" + url + "'."
+          });
+
+          // Is the agent dead? Navigate home since returning to the agent 
might
+          // end up in an endless loop.
+          $location.path('/').replace();
+        });
+    };
+
+    // When navigating directly to this page, e.g. pasting the URL into the
+    // browser, the previous page is not a page in Mesos. The agents
+    // information may not ready when loading this page, we start to reroute
+    // the sandbox request after the agents information loaded.
+    if ($scope.state) {
+      reroute();
+    }
+
+    // `reroute` is expected to always route away from the current page
+    // and the listener would be removed after the first state update.
+    var removeListener = $scope.$on('state_updated', reroute);
+    $scope.$on('$routeChangeStart', removeListener);
+  });
+
+
+  mesosApp.controller('BrowseCtrl', function($scope, $routeParams, $http) {
+    var update = function() {
+      if ($routeParams.agent_id in $scope.agents && $routeParams.path) {
+        $scope.agent_id = $routeParams.agent_id;
+        $scope.path = $routeParams.path;
+
+        var agent = $scope.agents[$routeParams.agent_id];
+
+        // This variable is used in 'browse.html' to generate the '/files'
+        // links, so we have to pass `includeProcessId=false` (see
+        // `agentURLPrefix`for more details).
+        $scope.agent_url_prefix = agentURLPrefix(agent, false);
+
+        $scope.pail = function($event, path) {
+          pailer(
+            agentURLPrefix(agent, false),
+            path,
+            decodeURIComponent(path) + ' (' + agent.id + ')');
+        };
+
+        var url = agentURLPrefix(agent, false) + 
'/files/browse?jsonp=JSON_CALLBACK';
+
+        // TODO(bmahler): Try to get the error code / body in the error 
callback.
+        // This wasn't working with the current version of angular.
+        $http.jsonp(url, {params: {path: $routeParams.path}})
+          .success(function(data) {
+            $scope.listing = data;
+            $('#listing').show();
+          })
+          .error(function() {
+            $scope.alert_message = 'Error browsing path: ' + $routeParams.path;
+            $('#alert').show();
+          });
+      } else {
+        if (!($routeParams.agent_id in $scope.agents)) {
+          $scope.alert_message = 'No agent found with ID: ' + 
$routeParams.agent_id;
+        } else {
+          $scope.alert_message = 'Missing "path" request parameter.';
+        }
+        $('#alert').show();
+      }
+    };
+
+    if ($scope.state) {
+      update();
+    }
+
+    var removeListener = $scope.$on('state_updated', update);
+    $scope.$on('$routeChangeStart', removeListener);
+  });
+})();

http://git-wip-us.apache.org/repos/asf/mesos/blob/c7685917/src/webui/app/frameworks/framework.html
----------------------------------------------------------------------
diff --git a/src/webui/app/frameworks/framework.html 
b/src/webui/app/frameworks/framework.html
new file mode 100644
index 0000000..82f6b27
--- /dev/null
+++ b/src/webui/app/frameworks/framework.html
@@ -0,0 +1,261 @@
+<ol class="breadcrumb">
+  <li>
+    <a class="badge badge-type" href="#">Master</a>
+  </li>
+  <li class="active">
+    <span class="badge badge-type">Framework</span>
+    {{framework.id}}
+  </li>
+</ol>
+
+<div class="alert alert-error hidden" id="alert">
+  <button class="close" data-dismiss="alert">×</button>
+  <strong>{{alert_message}}</strong>
+</div>
+
+<div class="row" id="framework">
+  <div class="col-md-3">
+    <div class="well">
+      <dl class="inline clearfix">
+        <dt>Name:</dt>
+        <dd>{{framework.name}}</dd>
+        <dt ng-show="framework.webui_url">Web UI:</dt>
+        <dd ng-show="framework.webui_url"><a 
href="{{framework.webui_url}}">{{framework.webui_url}}</a></dd>
+        <dt>User:</dt>
+        <dd>{{framework.user}}</dd>
+        <!-- TODO(bmahler): Consider having a break between each role
+             in order to increase readability. Also, this doesn't
+             display well when there are a lot of roles (e.g. a large
+             organization with a lot of teams & services, using roles
+             like /engineering/frontend/webserver, etc). -->
+        <dt>Roles:</dt>
+        <dd>{{framework.roles.toString()}}</dd>
+        <dt>Principal:</dt>
+        <dd>{{framework.principal}}</dd>
+        <dt>Registered:</dt>
+        <dd>
+          <m-timestamp value="{{framework.registered_time * 
1000}}"></m-timestamp>
+        </dd>
+        <dt>Re-registered:</dt>
+        <dd ng-if="!framework.reregistered_time">-</dd>
+        <dd ng-if="framework.reregistered_time">
+          <m-timestamp value="{{framework.reregistered_time * 
1000}}"></m-timestamp>
+        </dd>
+        <dt>Active tasks:</dt>
+        <dd>{{framework.tasks.length | number}}</dd>
+      </dl>
+
+      <h4>Tasks</h4>
+      <table class="table table-condensed">
+        <tbody>
+          <tr>
+            <td>Staging</td>
+            <td class="text-right">{{framework.staging_tasks | number}}</td>
+          </tr>
+          <tr>
+            <td>Starting</td>
+            <td class="text-right">{{framework.starting_tasks | number}}</td>
+          </tr>
+          <tr>
+            <td>Running</td>
+            <td class="text-right">{{framework.running_tasks | number}}</td>
+          </tr>
+          <tr>
+            <td>Unreachable</td>
+            <td class="text-right">{{framework.unreachable_tasks.length | 
number}}</td>
+          </tr>
+          <tr>
+            <td>Killing</td>
+            <td class="text-right">{{framework.killing_tasks | number}}</td>
+          </tr>
+          <tr>
+            <td>Finished</td>
+            <td class="text-right">{{framework.finished_tasks | number}}</td>
+          </tr>
+          <tr>
+            <td>Killed</td>
+            <td class="text-right">{{framework.killed_tasks | number}}</td>
+          </tr>
+          <tr>
+            <td>Failed</td>
+            <td class="text-right">{{framework.failed_tasks | number}}</td>
+          </tr>
+          <tr>
+            <td>Lost</td>
+            <td class="text-right">{{framework.lost_tasks | number}}</td>
+          </tr>
+        </tbody>
+      </table>
+
+      <h4>Resources</h4>
+      <table class="table table-condensed">
+        <thead>
+          <tr>
+            <td></td>
+            <td class="text-right">Allocated</td>
+            <td class="text-right">Offered</td>
+          </tr>
+        </thead>
+        <tbody>
+          <tr>
+            <td>CPUs</td>
+            <td class="text-right">
+              {{framework.used_resources.cpus | number}}
+            </td>
+            <td class="text-right">
+              {{framework.offered_resources.cpus | number}}
+            </td>
+          </tr>
+          <tr>
+            <td>GPUs</td>
+            <td class="text-right">
+              {{framework.used_resources.gpus | number}}
+            </td>
+            <td class="text-right">
+              {{framework.offered_resources.gpus | number}}
+            </td>
+          </tr>
+          <tr>
+            <td>Memory</td>
+            <td class="text-right">
+              {{framework.used_resources.mem * (1024 * 1024) | dataSize}}
+            </td>
+            <td class="text-right">
+              {{framework.offered_resources.mem * (1024 * 1024) | dataSize}}
+            </td>
+          </tr>
+          <tr>
+            <td>Disk</td>
+            <td class="text-right">
+              {{framework.used_resources.mem * (1024 * 1024) | dataSize}}
+            </td>
+            <td class="text-right">
+              {{framework.offered_resources.mem * (1024 * 1024) | dataSize}}
+            </td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+  </div>
+
+  <div class="col-md-9">
+    <table m-table table-content="framework.tasks" title="Active Tasks"
+      class="table table-striped table-bordered table-condensed">
+      <thead>
+        <tr>
+          <th data-key="id">ID</th>
+          <th data-key="name">Name</th>
+          <th data-key="role">Role</th>
+          <th data-key="state">State</th>
+          <th data-key="healthy">Health</th>
+          <th data-key="start_time">Started</th>
+          <th data-key="host">Host</th>
+          <th></th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr ng-repeat="task in $data">
+          <td>
+            <a 
href="#/agents/{{task.slave_id}}/frameworks/{{task.framework_id}}/executors/{{task.executor_id}}">
+              {{task.id}}
+            </a>
+          </td>
+          <td>{{task.name}}</td>
+          <td>{{task.role}}</td>
+          <td>{{task.state | truncateMesosState}}</td>
+          <td class="task-{{task.healthy | taskHealth}}">{{task.healthy | 
taskHealth}}</td>
+          <td>
+            <m-timestamp value="{{task.start_time}}"></m-timestamp>
+          </td>
+          <td>
+            <span data-ng-show="agents[task.slave_id]">
+              {{agents[task.slave_id].hostname}}
+            </span>
+            <span class="text-muted" data-ng-show="!agents[task.slave_id]">
+              Agent offline
+            </span>
+          </td>
+          <td>
+            <a data-ng-show="agents[task.slave_id]" 
href="#/agents/{{task.slave_id}}/frameworks/{{task.framework_id}}/executors/{{task.executor_id}}/tasks/{{task.id}}/browse">
+              Sandbox
+            </a>
+            <span class="text-muted" data-ng-show="!agents[task.slave_id]">
+              Agent offline
+            </span>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+
+    <table m-table table-content="framework.unreachable_tasks" 
title="Unreachable Tasks"
+      class="table table-striped table-bordered table-condensed">
+      <thead>
+        <tr>
+          <th data-key="id">ID</th>
+          <th data-key="name">Name</th>
+          <th data-key="role">Role</th>
+          <th data-key="start_time">Started</th>
+          <th data-key="agent_id">Agent ID</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr ng-repeat="task in $data">
+          <td>{{task.id}}</td>
+          <td>{{task.name}}</td>
+          <td>{{task.role}}</td>
+          <td>
+            <m-timestamp value="{{task.start_time}}"></m-timestamp>
+          </td>
+          <td>{{task.slave_id}}</td>
+        </tr>
+      </tbody>
+    </table>
+
+    <table m-table table-content="framework.completed_tasks" title="Completed 
Tasks"
+      class="table table-striped table-bordered table-condensed">
+      <thead>
+        <tr>
+          <th data-key="id">ID</th>
+          <th data-key="name">Name</th>
+          <th data-key="role">Role</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 $data">
+          <td>{{task.id}}</td>
+          <td>{{task.name}}</td>
+          <td>{{task.role}}</td>
+          <td>{{task.state | truncateMesosState}}</td>
+          <td>
+            <m-timestamp value="{{task.start_time}}"></m-timestamp>
+          </td>
+          <td>
+            <m-timestamp value="{{task.finish_time}}"></m-timestamp>
+          </td>
+          <td>
+            <a data-ng-show="agents[task.slave_id]"
+                
href="#/agents/{{task.slave_id}}/frameworks/{{task.framework_id}}/executors/{{task.executor_id}}">
+              {{agents[task.slave_id].hostname}}
+            </a>
+            <span class="text-muted" data-ng-show="!agents[task.slave_id]">
+              Agent offline
+            </span>
+          </td>
+          <td>
+            <a data-ng-show="agents[task.slave_id]" 
href="#/agents/{{task.slave_id}}/frameworks/{{task.framework_id}}/executors/{{task.executor_id}}/tasks/{{task.id}}/browse">
+              Sandbox
+            </a>
+            <span class="text-muted" data-ng-show="!agents[task.slave_id]">
+              Agent offline
+            </span>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+</div>

http://git-wip-us.apache.org/repos/asf/mesos/blob/c7685917/src/webui/app/frameworks/frameworks.html
----------------------------------------------------------------------
diff --git a/src/webui/app/frameworks/frameworks.html 
b/src/webui/app/frameworks/frameworks.html
new file mode 100644
index 0000000..d37c613
--- /dev/null
+++ b/src/webui/app/frameworks/frameworks.html
@@ -0,0 +1,179 @@
+<ol class="breadcrumb">
+  <li><a class="badge badge-type" href="#">Master</a></li>
+  <li class="active">
+    <span class="badge badge-type">Frameworks</span>
+  </li>
+</ol>
+
+<table m-table table-content="frameworks" title="Active Frameworks"
+  class="table table-striped table-bordered table-condensed">
+  <thead>
+    <tr>
+      <th data-key="id">ID</th>
+      <th data-key="hostname">Host</th>
+      <th data-key="user">User</th>
+      <th data-key="name">Name</th>
+      <th data-key="roles">Roles</th>
+      <th data-key="principal">Principal</th>
+      <th data-key="tasks.length">Active Tasks</th>
+      <th data-key="used_resources.cpus">CPUs</th>
+      <th data-key="used_resources.gpus">GPUs</th>
+      <th data-key="used_resources.mem">Mem</th>
+      <th data-key="used_resources.disk">Disk</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 $data  | filter: { active: true }">
+      <td>
+        <a href="{{'#/frameworks/' + framework.id}}">
+          {{framework.id | truncateMesosID}}
+        </a>
+        <button class="btn btn-xs btn-toggle btn-default"
+                clipboard
+                data-clipboard-text="{{framework.id}}"
+                tooltip="Copy ID"
+                tooltip-placement="right"
+                tooltip-trigger="clipboardhover">
+        </button>
+      </td>
+      <td ng-switch="!!framework.webui_url">
+        <a ng-href="{{framework.webui_url}}" 
ng-switch-when="true">{{framework.hostname}}</a>
+        <span ng-switch-when="false">{{framework.hostname}}</span>
+      </td>
+      <td>{{framework.user}}</td>
+      <td>{{framework.name}}</td>
+      <!-- TODO(bmahler): This doesn't display well when there are a lot
+           of roles (e.g. a large organization with a lot of teams &
+           services, using roles like /engineering/frontend/webserver, etc).
+           Figure out a way to display this without bloating the table. -->
+      <td>{{framework.roles.toString()}}</td>
+      <td>{{framework.principal}}</td>
+      <td>{{framework.tasks.length}}</td>
+      <td>{{framework.used_resources.cpus | number}}</td>
+      <td>{{framework.used_resources.gpus | number}}</td>
+      <td>{{framework.used_resources.mem * (1024 * 1024) | dataSize}}</td>
+      <td>{{framework.used_resources.disk * (1024 * 1024) | dataSize}}</td>
+      <td>{{framework.max_share * 100 | number}}%</td>
+      <td>
+        <m-timestamp value="{{framework.registered_time * 
1000}}"></m-timestamp>
+      </td>
+      <td ng-show="!framework.reregistered_time">-</td>
+      <td ng-show="framework.reregistered_time">
+        <m-timestamp value="{{framework.reregistered_time * 
1000}}"></m-timestamp>
+      </td>
+    </tr>
+  </tbody>
+</table>
+
+<table m-table table-content="frameworks" title="Inactive Frameworks"
+       class="table table-striped table-bordered table-condensed">
+  <thead>
+  <tr>
+    <th data-key="id">ID</th>
+    <th data-key="hostname">Host</th>
+    <th data-key="user">User</th>
+    <th data-key="name">Name</th>
+    <th data-key="roles">Roles</th>
+    <th data-key="principal">Principal</th>
+    <th data-key="tasks.length">Active Tasks</th>
+    <th data-key="used_resources.cpus">CPUs</th>
+    <th data-key="used_resources.gpus">GPUs</th>
+    <th data-key="used_resources.mem">Mem</th>
+    <th data-key="used_resources.disk">Disk</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 $data  | filter: { active: false }">
+    <td>
+      <a href="{{'#/frameworks/' + framework.id}}">
+        {{framework.id | truncateMesosID}}
+      </a>
+      <button class="btn btn-xs btn-toggle btn-default"
+              clipboard
+              data-clipboard-text="{{framework.id}}"
+              tooltip="Copy ID"
+              tooltip-placement="right"
+              tooltip-trigger="clipboardhover">
+      </button>
+    </td>
+    <td ng-switch="!!framework.webui_url">
+      <a ng-href="{{framework.webui_url}}" 
ng-switch-when="true">{{framework.hostname}}</a>
+      <span ng-switch-when="false">{{framework.hostname}}</span>
+    </td>
+    <td>{{framework.user}}</td>
+    <td>{{framework.name}}</td>
+    <!-- TODO(bmahler): This doesn't display well when there are a lot
+         of roles (e.g. a large organization with a lot of teams &
+         services, using roles like /engineering/frontend/webserver, etc).
+         Figure out a way to display this without bloating the table. -->
+    <td>{{framework.roles.toString()}}</td>
+    <td>{{framework.principal}}</td>
+    <td>{{framework.tasks.length}}</td>
+    <td>{{framework.used_resources.cpus | number}}</td>
+    <td>{{framework.used_resources.gpus | number}}</td>
+    <td>{{framework.used_resources.mem * (1024 * 1024) | dataSize}}</td>
+    <td>{{framework.used_resources.disk * (1024 * 1024) | dataSize}}</td>
+    <td>{{framework.max_share * 100 | number}}%</td>
+    <td>
+      <m-timestamp value="{{framework.registered_time * 1000}}"></m-timestamp>
+    </td>
+    <td ng-show="!framework.reregistered_time">-</td>
+    <td ng-show="framework.reregistered_time">
+      <m-timestamp value="{{framework.reregistered_time * 
1000}}"></m-timestamp>
+    </td>
+  </tr>
+  </tbody>
+</table>
+
+<table m-table table-content="completed_frameworks" title="Completed 
Frameworks"
+  class="table table-striped table-bordered table-condensed">
+  <thead>
+    <tr>
+      <th data-key="id">ID</th>
+      <th data-key="hostname">Host</th>
+      <th data-key="user">User</th>
+      <th data-key="name">Name</th>
+      <th data-key="roles">Roles</th>
+      <th data-key="principal">Principal</th>
+      <th data-key="registered_time">Registered</th>
+      <th data-key="unregistered_time">Unregistered</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr ng-repeat="framework in $data">
+      <td>
+        <a href="{{'#/frameworks/' + framework.id}}" title="{{framework.id}}">
+          {{framework.id | truncateMesosID}}
+        </a>
+        <button class="btn btn-xs btn-toggle btn-default"
+                clipboard
+                data-clipboard-text="{{framework.id}}"
+                tooltip="Copy ID"
+                tooltip-placement="right"
+                tooltip-trigger="clipboardhover">
+        </button>
+      </td>
+      <td>{{framework.hostname}}</td>
+      <td>{{framework.user}}</td>
+      <td>{{framework.name}}</td>
+      <!-- TODO(bmahler): This doesn't display well when there are a lot
+           of roles (e.g. a large organization with a lot of teams &
+           services, using roles like /engineering/frontend/webserver, etc).
+           Figure out a way to display this without bloating the table. -->
+      <td>{{framework.roles.toString()}}</td>
+      <td>{{framework.principal}}</td>
+      <td>
+        <m-timestamp value="{{framework.registered_time * 
1000}}"></m-timestamp>
+      </td>
+      <td>
+        <m-timestamp value="{{framework.unregistered_time * 
1000}}"></m-timestamp>
+      </td>
+    </tr>
+  </tbody>
+</table>

http://git-wip-us.apache.org/repos/asf/mesos/blob/c7685917/src/webui/app/home.html
----------------------------------------------------------------------
diff --git a/src/webui/app/home.html b/src/webui/app/home.html
new file mode 100644
index 0000000..ff96595
--- /dev/null
+++ b/src/webui/app/home.html
@@ -0,0 +1,334 @@
+<ol class="breadcrumb">
+  <li class="active">
+    <span class="badge badge-type">Master</span>
+    {{state.id}}
+  </li>
+</ol>
+
+<div class="row">
+  <div class="col-sm-5 col-md-4 col-lg-3">
+    <div class="well">
+      <dl class="inline inline-toggle clearfix">
+        <dt>Cluster:</dt>
+        <dd>
+          <span ng-show="clusterNamed">{{state.cluster}}</span>
+          <span ng-show="!clusterNamed">
+            (Unnamed)
+            <i class="icon-info-sign"
+              tooltip="To name this cluster, set the --cluster flag when 
starting the master."
+              tooltip-placement="right"></i>
+          </span>
+        </dd>
+        <dt>Leader:</dt>
+        <dd>
+          <a ng-show="state.leader_info"
+             
href="//{{state.leader_info.hostname}}:{{state.leader_info.port}}">
+            {{state.leader_info.hostname + ':' + state.leader_info.port}}
+          </a>
+          <span ng-show="!state.leader_info">(Unknown)</span>
+        </dd>
+        <dt>Version:</dt>
+        <dd>{{state.version}}</dd>
+        <dt>Built:</dt>
+        <dd>
+          <m-timestamp value="{{state.build_time * 1000}}">
+            by <i>{{state.build_user}}</i>
+          </m-timestamp>
+        </dd>
+        <dt>Started:</dt>
+        <dd>
+          <m-timestamp value="{{state.start_time * 1000}}"></m-timestamp>
+        </dd>
+        <dt>Elected:</dt>
+        <dd>
+          <m-timestamp value="{{state.elected_time * 1000}}"></m-timestamp>
+        </dd>
+      </dl>
+
+      <p ng-if="log_file_attached">
+        <b>Master Log:</b>
+        <span class="btn-group">
+          <!-- Links can look like buttons using Bootstrap classes. -->
+          <a class="btn btn-xs btn-default" 
href="//{{$location.host()}}:{{$location.port()}}/files/download?path=/master/log">
+            Download
+          </a>
+          <button class="btn btn-xs btn-default" ng-click="streamLogs($event)">
+            View
+          </button>
+        </span>
+      </p>
+
+      <h4>Agents</h4>
+      <table class="table table-condensed">
+        <tbody>
+          <tr>
+            <td>Activated</td>
+            <td class="text-right">{{activated_agents | number}}</td>
+          </tr>
+          <tr>
+            <td>Deactivated</td>
+            <td class="text-right">{{deactivated_agents | number}}</td>
+          </tr>
+          <tr>
+            <td>Unreachable</td>
+            <td class="text-right">{{unreachable_agents | number}}</td>
+          </tr>
+        </tbody>
+      </table>
+
+      <h4>Tasks</h4>
+      <table class="table table-condensed">
+        <tbody>
+          <tr>
+            <td>Staging</td>
+            <td class="text-right">{{staging_tasks | number}}</td>
+          </tr>
+          <tr>
+            <td>Starting</td>
+            <td class="text-right">{{starting_tasks | number}}</td>
+          </tr>
+          <tr>
+            <td>Running</td>
+            <td class="text-right">{{running_tasks | number}}</td>
+          </tr>
+          <tr>
+            <td>Unreachable</td>
+            <td class="text-right">{{unreachable_tasks.length | number}}</td>
+          </tr>
+          <tr>
+            <td>Killing</td>
+            <td class="text-right">{{killing_tasks | number}}</td>
+          </tr>
+          <tr>
+            <td>Finished</td>
+            <td class="text-right">{{finished_tasks | number}}</td>
+          </tr>
+          <tr>
+            <td>Killed</td>
+            <td class="text-right">{{killed_tasks | number}}</td>
+          </tr>
+          <tr>
+            <td>Failed</td>
+            <td class="text-right">{{failed_tasks | number}}</td>
+          </tr>
+          <tr>
+            <td>Lost</td>
+            <td class="text-right">{{lost_tasks | number}}</td>
+          </tr>
+        </tbody>
+      </table>
+
+      <h4>Resources</h4>
+      <table class="table table-condensed">
+        <thead>
+          <tr>
+            <td></td>
+            <td class="text-right">CPUs</td>
+            <td class="text-right">GPUs</td>
+            <td class="text-right">Mem</td>
+            <td class="text-right">Disk</td>
+          </tr>
+        </thead>
+        <tbody>
+          <tr>
+            <td>Total</td>
+            <td class="text-right">{{total_cpus | decimalFloat}}</td>
+            <td class="text-right">{{total_gpus | decimalFloat}}</td>
+            <td class="text-right">{{total_mem * (1024 * 1024) | 
dataSize}}</td>
+            <td class="text-right">{{total_disk * (1024 * 1024) | 
dataSize}}</td>
+          </tr>
+          <tr>
+            <td>Allocated</td>
+            <td class="text-right">{{used_cpus | decimalFloat}}</td>
+            <td class="text-right">{{used_gpus | decimalFloat}}</td>
+            <td class="text-right">{{used_mem * (1024 * 1024) | dataSize}}</td>
+            <td class="text-right">{{used_disk * (1024 * 1024) | 
dataSize}}</td>
+          </tr>
+          <tr>
+            <td>Offered</td>
+            <td class="text-right">{{offered_cpus | decimalFloat}}</td>
+            <td class="text-right">{{offered_gpus | decimalFloat}}</td>
+            <td class="text-right">{{offered_mem * (1024 * 1024) | 
dataSize}}</td>
+            <td class="text-right">{{offered_disk * (1024 * 1024) | 
dataSize}}</td>
+          </tr>
+          <tr>
+            <td>Idle</td>
+            <td class="text-right">{{idle_cpus | decimalFloat}}</td>
+            <td class="text-right">{{idle_gpus | decimalFloat}}</td>
+            <td class="text-right">{{idle_mem * (1024 * 1024) | dataSize}}</td>
+            <td class="text-right">{{idle_disk * (1024 * 1024) | 
dataSize}}</td>
+          </tr>
+        </tbody>
+      </table>
+    </div>
+  </div>
+
+  <div class="col-sm-7 col-md-8 col-lg-9">
+    <table m-table table-content="active_tasks" title="Active Tasks"
+      class="table table-striped table-bordered table-condensed">
+      <thead>
+        <tr>
+          <th data-key="framework_id">Framework ID</th>
+          <th data-key="id">Task ID</th>
+          <th data-key="name">Task Name</th>
+          <th data-key="role">Role</th>
+          <th data-key="state">State</th>
+          <th data-key="healthy">Health</th>
+          <th data-key="start_time" data-sort>Started</th>
+          <th data-key="host">Host</th>
+          <th></th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr data-ng-if="active_tasks.length === 0">
+          <td colspan="8">No active tasks.</td>
+        </tr>
+        <tr ng-repeat="task in $data">
+          <td>
+            <a href="#/frameworks/{{task.framework_id}}/">
+              {{task.framework_id | truncateMesosID}}
+            </a>
+            <button class="btn btn-xs btn-toggle btn-default"
+                    clipboard
+                    data-clipboard-text="{{task.framework_id}}"
+                    tooltip="Copy ID"
+                    tooltip-placement="right"
+                    tooltip-trigger="clipboardhover">
+            </button>
+          </td>
+          <td>
+            <a 
href="#/agents/{{task.slave_id}}/frameworks/{{task.framework_id}}/executors/{{task.executor_id}}">
+              {{task.id}}
+            </a>
+          </td>
+          <td>{{task.name}}</td>
+          <td>{{task.role}}</td>
+          <td>{{task.state | truncateMesosState}}</td>
+          <td class="task-{{task.healthy | taskHealth}}">{{task.healthy | 
taskHealth}}</td>
+          <td>
+            <m-timestamp value="{{task.start_time}}"></m-timestamp>
+          </td>
+          <td>
+            <span data-ng-show="agents[task.slave_id]">
+              {{agents[task.slave_id].hostname}}
+            </span>
+            <span class="text-muted" data-ng-show="!agents[task.slave_id]">
+              Agent offline
+            </span>
+          </td>
+          <td>
+            <a data-ng-show="agents[task.slave_id]" 
href="#/agents/{{task.slave_id}}/frameworks/{{task.framework_id}}/executors/{{task.executor_id}}/tasks/{{task.id}}/browse">
+              Sandbox
+            </a>
+            <span class="text-muted" data-ng-show="!agents[task.slave_id]">
+              Agent offline
+            </span>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+
+    <table m-table table-content="unreachable_tasks" title="Unreachable Tasks"
+      class="table table-striped table-bordered table-condensed">
+      <thead>
+        <tr>
+          <th data-key="framework_id">Framework ID</th>
+          <th data-key="id">Task ID</th>
+          <th data-key="name">Task Name</th>
+          <th data-key="role">Role</th>
+          <th data-key="start_time" data-sort>Started</th>
+          <th data-key="agent_id">Agent ID</th>
+        </tr>
+      </thead>
+      <tbody>
+        <tr data-ng-if="unreachable_tasks.length === 0">
+          <td colspan="8">No unreachable tasks.</td>
+        </tr>
+        <tr ng-repeat="task in $data">
+          <td>
+            <a href="#/frameworks/{{task.framework_id}}/">
+              {{task.framework_id | truncateMesosID}}
+            </a>
+            <button class="btn btn-xs btn-toggle btn-default"
+                    clipboard
+                    data-clipboard-text="{{task.framework_id}}"
+                    tooltip="Copy ID"
+                    tooltip-placement="right"
+                    tooltip-trigger="clipboardhover">
+            </button>
+          </td>
+          <td>{{task.id}}</td>
+          <td>{{task.name}}</td>
+          <td>{{task.role}}</td>
+          <td>
+            <m-timestamp value="{{task.start_time}}"></m-timestamp>
+          </td>
+          <td>{{task.slave_id}}</td>
+        </tr>
+      </tbody>
+    </table>
+
+    <table m-table table-content="completed_tasks" title="Completed Tasks"
+      class="table table-striped table-bordered table-condensed">
+      <thead>
+        <tr>
+          <th data-key="framework_id">Framework ID</th>
+          <th data-key="id">Task ID</th>
+          <th data-key="name">Task Name</th>
+          <th data-key="role">Role</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="8">No completed tasks.</td>
+        </tr>
+        <tr ng-repeat="task in $data">
+         <td>
+            <a href="#/frameworks/{{task.framework_id}}/">
+              {{task.framework_id | truncateMesosID}}
+            </a>
+            <button class="btn btn-xs btn-toggle btn-default"
+                    clipboard
+                    data-clipboard-text="{{task.framework_id}}"
+                    tooltip="Copy ID"
+                    tooltip-placement="right"
+                    tooltip-trigger="clipboardhover">
+            </button>
+          </td>
+          <td>{{task.id}}</td>
+          <td>{{task.name}}</td>
+          <td>{{task.role}}</td>
+          <td>{{task.state | truncateMesosState}}</td>
+          <td>
+            <m-timestamp value="{{task.start_time}}"></m-timestamp>
+          </td>
+          <td>
+            <m-timestamp value="{{task.finish_time}}"></m-timestamp>
+          </td>
+          <td>
+            <a data-ng-show="_.has(agents, task.slave_id)"
+                
href="#/agents/{{task.slave_id}}/frameworks/{{task.framework_id}}/executors/{{task.executor_id}}">
+              {{agents[task.slave_id].hostname}}
+            </a>
+            <span class="text-muted" data-ng-show="!_.has(agents, 
task.slave_id)">
+              Agent offline
+            </span>
+          </td>
+          <td>
+            <a data-ng-show="agents[task.slave_id]" 
href="#/agents/{{task.slave_id}}/frameworks/{{task.framework_id}}/executors/{{task.executor_id}}/tasks/{{task.id}}/browse">
+              Sandbox
+            </a>
+            <span class="text-muted" data-ng-show="!agents[task.slave_id]">
+              Agent offline
+            </span>
+          </td>
+        </tr>
+      </tbody>
+    </table>
+  </div>
+</div>

http://git-wip-us.apache.org/repos/asf/mesos/blob/c7685917/src/webui/app/maintenance/maintenance.html
----------------------------------------------------------------------
diff --git a/src/webui/app/maintenance/maintenance.html 
b/src/webui/app/maintenance/maintenance.html
new file mode 100644
index 0000000..d06ddd2
--- /dev/null
+++ b/src/webui/app/maintenance/maintenance.html
@@ -0,0 +1,36 @@
+<ol class="breadcrumb">
+  <li>
+    <a class="badge badge-type" href="#/">Master</a>
+  </li>
+  <li class="active">
+    <span class="badge badge-type">Maintenance Schedule</span>
+  </li>
+</ol>
+
+<table m-table table-content="maintenance.windows" title="Maintenance Schedule"
+  class="table table-striped table-bordered table-condensed">
+  <thead>
+    <tr>
+      <th data-key="unavailability.start.nanoseconds" data-sort>Begin</th>
+      <th>End</th>
+      <th>Hosts</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr ng-repeat="window in $data">
+      <td>
+        <m-timestamp value="{{window.unavailability.start.nanoseconds / 
1000000}}">
+        </m-timestamp>
+      </td>
+      <td>
+        <m-timestamp value="{{(window.unavailability.duration.nanoseconds + 
window.unavailability.start.nanoseconds) / 1000000}}">
+        </m-timestamp>
+      </td>
+      <td>
+          <div ng-repeat="machine_ids in window.machine_ids">
+            {{machine_ids.hostname ? machine_ids.hostname : machine_ids.ip}}
+          </div>
+      </td>
+    </tr>
+  </tbody>
+</table>

http://git-wip-us.apache.org/repos/asf/mesos/blob/c7685917/src/webui/app/offers/offers.html
----------------------------------------------------------------------
diff --git a/src/webui/app/offers/offers.html b/src/webui/app/offers/offers.html
new file mode 100644
index 0000000..0586a21
--- /dev/null
+++ b/src/webui/app/offers/offers.html
@@ -0,0 +1,47 @@
+<ol class="breadcrumb">
+  <li>
+    <a class="badge badge-type" href="#/">Master</a>
+  </li>
+  <li class="active">
+    <span class="badge badge-type">Outstanding Offers</span>
+  </li>
+</ol>
+
+<table m-table table-content="offers" title="Outstanding Offers"
+  class="table table-striped table-bordered table-condensed">
+  <thead>
+    <tr>
+      <th data-key="id">ID</th>
+      <th data-key="allocation_info.role">Role</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.gpus">GPUs</th>
+      <th data-key="resources.mem">Mem</th>
+      <th data-key="resources.disk">Disk</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr ng-repeat="offer in $data">
+      <td>
+        <abbr title="{{offer.id}}">{{offer.id | truncateMesosID}}</abbr>
+      </td>
+      <!-- Make this a link to the role overview -->
+      <td>{{offer.allocation_info.role}}</td>
+      <td>
+        <a href="{{'#/frameworks/' + offer.framework_id}}">
+          {{offer.framework_name}}
+        </a>
+      </td>
+      <td>
+        <a href="#/agents/{{agents[offer.slave_id].id}}">
+          {{offer.hostname}}
+        </a>
+      </td>
+      <td>{{offer.resources.cpus | number}}</td>
+      <td>{{offer.resources.gpus | number}}</td>
+      <td>{{offer.resources.mem * (1024 * 1024) | dataSize}}</td>
+      <td>{{offer.resources.disk * (1024 * 1024) | dataSize}}</td>
+    </tr>
+  </tbody>
+</table>

http://git-wip-us.apache.org/repos/asf/mesos/blob/c7685917/src/webui/app/roles/roles.html
----------------------------------------------------------------------
diff --git a/src/webui/app/roles/roles.html b/src/webui/app/roles/roles.html
new file mode 100644
index 0000000..c8ac1ef
--- /dev/null
+++ b/src/webui/app/roles/roles.html
@@ -0,0 +1,60 @@
+<ol class="breadcrumb">
+  <li>
+    <a class="badge badge-type" href="#/">Master</a>
+  </li>
+  <li class="active">
+    <span class="badge badge-type">Roles</span>
+  </li>
+</ol>
+
+<table m-table table-content="roles.roles" title="Roles"
+  class="table table-striped table-bordered table-condensed">
+  <thead>
+    <tr>
+      <th data-key="name" data-sort class="ascending vertically-center" 
rowspan="2">Role</th>
+      <th data-key="weight" class="vertically-center" rowspan="2">Weight</th>
+      <th data-key="frameworks.length" class="vertically-center" 
rowspan="2">Frameworks</th>
+      <th data-key="resources.cpus" class="group-column" 
colspan="4">Allocation</th>
+      <th data-key="resources.gpus" class="group-column" 
colspan="4">Guarantee</th>
+      <th data-key="resources.mem" class="group-column" colspan="4">Limit</th>
+    </tr>
+    <tr>
+      <th data-key="resources.cpus" class="begin-group-column">CPU</th>
+      <th data-key="resources.gpus">GPU</th>
+      <th data-key="resources.mem">Mem</th>
+      <th data-key="resources.disk" class="end-group-column">Disk</th>
+      <th data-key="resources.cpus" class="begin-group-column">CPU</th>
+      <th data-key="resources.gpus">GPU</th>
+      <th data-key="resources.mem">Mem</th>
+      <th data-key="resources.disk" class="end-group-column">Disk</th>
+      <th data-key="resources.cpus" class="begin-group-column">CPU</th>
+      <th data-key="resources.gpus">GPU</th>
+      <th data-key="resources.mem">Mem</th>
+      <th data-key="resources.disk" class="end-group-column">Disk</th>
+    </tr>
+  </thead>
+  <tbody>
+    <tr ng-repeat="role in $data">
+      <td>{{role.name}}</td>
+      <td>{{role.weight | number}}</td>
+      <td>{{role.frameworks.length | number}}</td>
+      <td class="begin-group-column">{{role.resources.cpus | 
decimalFloat}}</td>
+      <td>{{role.resources.gpus | decimalFloat}}</td>
+      <td>{{role.resources.mem * (1024 * 1024) | dataSize}}</td>
+      <td class="end-group-column">{{role.resources.disk * (1024 * 1024) | 
dataSize}}</td>
+
+      <td class="begin-group-column">{{(role.quota.guarantee.cpus | 
decimalFloat) || "-"}}</td>
+      <td>{{(role.quota.guarantee.gpus | decimalFloat) || "-"}}</td>
+      <td>{{(role.quota.guarantee.mem * (1024 * 1024) | dataSize) || "-"}}</td>
+      <td class="end-group-column">{{(role.quota.guarantee.disk * (1024 * 
1024) | dataSize) || "-"}}</td>
+
+      <!-- TODO(ArmandGrillet): Replace by
+        (role.quota.limit.<> | decimalFloat) || (role.quota.guarantee.<> | 
decimalFloat) || "-"
+        once the limit field is introduced. -->
+      <td class="begin-group-column">{{(role.quota.guarantee.cpus | 
decimalFloat) || "-"}}</td>
+      <td>{{(role.quota.guarantee.gpus | decimalFloat) || "-"}}</td>
+      <td>{{(role.quota.guarantee.mem * (1024 * 1024) | dataSize) || "-"}}</td>
+      <td class="end-group-column">{{(role.quota.guarantee.disk * (1024 * 
1024) | dataSize) || "-"}}</td>
+    </tr>
+  </tbody>
+</table>

Reply via email to