http://git-wip-us.apache.org/repos/asf/mesos/blob/c7685917/src/webui/app/services.js ---------------------------------------------------------------------- diff --git a/src/webui/app/services.js b/src/webui/app/services.js new file mode 100644 index 0000000..5e83996 --- /dev/null +++ b/src/webui/app/services.js @@ -0,0 +1,309 @@ +// 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 mesosServices = angular.module('mesos.services', []); + + mesosServices.service('$alert', ['$rootScope', function($rootScope) { + // Types taken from Bootstraps v3's "Alerts"[1] so the type can be used + // as the class name. + // + // [1] http://getbootstrap.com/components/#alerts + var TYPE_DANGER = 'danger'; + var TYPE_INFO = 'info'; + var TYPE_SUCCESS = 'success'; + var TYPE_WARNING = 'warning'; + + var nextId = 1; + + var nextAlerts = []; + var currentAlerts = $rootScope.currentAlerts = []; + + // Creates an alert to be rendered on the next page view. + // + // messageOrOptions - Either a String or an Object that will be used to + // render an alert on the next view. If a String, it will be the + // message in the alert. If an Object, "title" will be bolded, "message" + // will be normal font weight, and "bullets" will be rendered as a list. + function alert(type, messageOrOptions) { + var alertObject; + + if (angular.isObject(messageOrOptions)) { + alertObject = angular.copy(messageOrOptions); + alertObject.type = type; + } else { + alertObject = { + message: messageOrOptions, + type: type + }; + } + + alertObject.id = nextId; + nextId += 1; + return nextAlerts.push(alertObject); + } + + this.danger = function(messageOrOptions) { + return alert(TYPE_DANGER, messageOrOptions); + }; + this.info = function(messageOrOptions) { + return alert(TYPE_INFO, messageOrOptions); + }; + this.success = function(messageOrOptions) { + return alert(TYPE_SUCCESS, messageOrOptions); + }; + this.warning = function(messageOrOptions) { + return alert(TYPE_WARNING, messageOrOptions); + }; + + // Rotate alerts each time the user navigates. + $rootScope.$on('$locationChangeSuccess', function() { + if (nextAlerts.length > 0) { + // If there are alerts to be shown next, they become the current alerts. + currentAlerts = $rootScope.currentAlerts = nextAlerts; + nextAlerts = []; + } else if (currentAlerts.length > 0) { + // If there are no next alerts, the current alerts still need to expire + // if there are any so they won't display again. + currentAlerts = $rootScope.currentAlerts = []; + } + }); + }]); + + var uiModalDialog = angular.module('ui.bootstrap.dialog', ['ui.bootstrap']); + uiModalDialog + .factory('$dialog', ['$rootScope', '$modal', function ($rootScope, $modal) { + + var prompt = function(title, message, buttons) { + + if (typeof buttons === 'undefined') { + buttons = [ + {result:'cancel', label: 'Cancel'}, + {result:'yes', label: 'Yes', cssClass: 'btn-primary'} + ]; + } + + var ModalCtrl = function($scope, _$modalInstance) { + $scope.title = title; + $scope.message = message; + $scope.buttons = buttons; + }; + + return $modal.open({ + templateUrl: 'template/dialog/message.html', + controller: ModalCtrl + }).result; + }; + + return { + prompt: prompt, + messageBox: function(title, message, buttons) { + return { + open: function() { + return prompt(title, message, buttons); + } + }; + } + }; + }]); + + function Statistics() { + this.cpus_user_time_secs = 0.0; + this.cpus_system_time_secs = 0.0; + this.cpus_limit = 0.0; + this.cpus_total_usage = 0.0; + this.mem_rss_bytes = 0.0; + this.mem_limit_bytes = 0.0; + this.disk_used_bytes = 0.0; + this.disk_limit_bytes = 0.0; + this.timestamp = 0.0; + } + + Statistics.prototype.add = function(statistics) { + this.cpus_user_time_secs += statistics.cpus_user_time_secs; + this.cpus_system_time_secs += statistics.cpus_system_time_secs; + this.cpus_total_usage += statistics.cpus_total_usage; + this.cpus_limit += statistics.cpus_limit; + this.mem_rss_bytes += statistics.mem_rss_bytes; + this.mem_limit_bytes += statistics.mem_limit_bytes; + this.disk_used_bytes += statistics.disk_used_bytes; + this.disk_limit_bytes += statistics.disk_limit_bytes; + + // Set instead of add the timestamp since this is an instantaneous view of + // CPU usage since the last poll. + this.timestamp = statistics.timestamp; + }; + + Statistics.prototype.diffUsage = function(statistics) { + var cpus_user_usage = + (this.cpus_user_time_secs - statistics.cpus_user_time_secs) / + (this.timestamp - statistics.timestamp); + var cpus_system_usage = + (this.cpus_system_time_secs - statistics.cpus_system_time_secs) / + (this.timestamp - statistics.timestamp); + this.cpus_total_usage = cpus_user_usage + cpus_system_usage; + }; + + Statistics.parseJSON = function(json) { + var statistics = new Statistics(); + statistics.add(json); + return statistics; + }; + + // Top is an abstraction for polling an agent's monitoring endpoint to + // periodically update the monitoring data. It also computes CPU usage. + // This places the following data in scope.monitor: + // + // $scope.monitor = { + // "statistics": <stats>, + // "frameworks": { + // <framework_id>: { + // "statistics": <stats>, + // "executors": { + // <executor_id>: { + // "executor_id": <executor_id>, + // "framework_id": <framework_id>, + // "executor_name: <executor_name>, + // "source": <source>, + // "statistics": <stats>, + // } + // } + // } + // } + // } + // + // To obtain agent statistics: + // $scope.monitor.statistics + // + // To obtain a framework's statistics: + // $scope.monitor.frameworks[<framework_id>].statistics + // + // To obtain an executor's statistics: + // $scope.monitor.frameworks[<framework_id>].executors[<executor_id>].statistics + // + // In the above, <stats> is the following object: + // + // { + // cpus_user_time_secs: value, + // cpus_system_time_secs: value, + // cpus_total_usage: value, // Once computed. + // mem_limit_bytes: value, + // mem_rss_bytes: value, + // } + // + // TODO(bmahler): The complexity of the monitor object is mostly in place + // until we have path-params on the monitoring endpoint to request + // statistics for the agent, or for a specific framework / executor. + // + // Arguments: + // http: $http service from Angular. + // timeout: $timeout service from Angular. + function Top($http, $timeout) { + this.http = $http; + this.timeout = $timeout; + } + + Top.prototype.poll = function() { + this.http.jsonp(this.endpoint) + + // Success! Parse the response. + .success(angular.bind(this, this.parseResponse)) + + // Do not continue polling on error. + .error(angular.noop); + }; + + Top.prototype.parseResponse = function(response) { + var this_ = this; + var monitor = { + frameworks: {}, + statistics: new Statistics() + }; + + response.forEach(function(executor) { + var executor_id = executor.executor_id; + var framework_id = executor.framework_id; + var current = executor.statistics = + Statistics.parseJSON(executor.statistics); + + // Compute CPU usage if possible. + if (this_.scope.monitor && + this_.scope.monitor.frameworks[framework_id] && + this_.scope.monitor.frameworks[framework_id].executors[executor_id]) { + var previous = this_.scope.monitor.frameworks[framework_id].executors[executor_id].statistics; + current.diffUsage(previous); + } + + // Index the data. + if (!monitor.frameworks[executor.framework_id]) { + monitor.frameworks[executor.framework_id] = { + executors: {}, + statistics: new Statistics() + }; + } + + // Aggregate these statistics into the agent and framework statistics. + monitor.statistics.add(current); + monitor.frameworks[executor.framework_id].statistics.add(current); + monitor.frameworks[executor.framework_id].executors[executor.executor_id] = { + statistics: current + }; + }); + + if (this.scope.monitor) { + // Continue polling. + this.polling = this.timeout(angular.bind(this, this.poll), 3000); + } else { + // Try to compute initial CPU usage more quickly than 3 seconds. + this.polling = this.timeout(angular.bind(this, this.poll), 500); + } + + // Update the monitoring data. + this.scope.monitor = monitor; + }; + + // Arguments: + // url: the URL of the Agent's container statistics endpoint. + // scope: $scope service from Angular. + Top.prototype.start = function(url, scope) { + if (this.started()) { + // TODO(bmahler): Consider logging a warning here. + return; + } + + this.endpoint = url; + this.scope = scope; + + // Initial poll is immediate. + this.polling = this.timeout(angular.bind(this, this.poll), 0); + + // Stop when we leave the page. + scope.$on('$routeChangeStart', angular.bind(this, this.stop)); + }; + + Top.prototype.started = function() { + return this.polling != null; + }; + + Top.prototype.stop = function() { + this.timeout.cancel(this.polling); + this.polling = null; + }; + + mesosServices.service('top', ['$http', '$timeout', Top]); +})();
http://git-wip-us.apache.org/repos/asf/mesos/blob/c7685917/src/webui/app/shared/pagination.html ---------------------------------------------------------------------- diff --git a/src/webui/app/shared/pagination.html b/src/webui/app/shared/pagination.html new file mode 100644 index 0000000..ffa16b7 --- /dev/null +++ b/src/webui/app/shared/pagination.html @@ -0,0 +1,6 @@ +<pagination + data-ng-show="filteredData.length > pageLength" + max-size="5" + items-per-page="pageLength" + page="pgNum" + total-items="filteredData.length"></pagination> http://git-wip-us.apache.org/repos/asf/mesos/blob/c7685917/src/webui/app/shared/pailer.html ---------------------------------------------------------------------- diff --git a/src/webui/app/shared/pailer.html b/src/webui/app/shared/pailer.html new file mode 100644 index 0000000..0da1306 --- /dev/null +++ b/src/webui/app/shared/pailer.html @@ -0,0 +1,80 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <title></title> + <style> + body { + line-height: 16px; + font-size: 14px; + } + + .indicator { + background: #000000; + color: #FFFFFF; + left: 5px; + position: absolute; + text-decoration: none; + top: 5px; + } + </style> + </head> + + <body style="overflow: hidden;"> + <pre id="data"></pre> + <div class="indicator" id="indicator"></div> + + <script src="../../assets/libs/jquery-3.2.1.min.js"></script> + <script src="../../assets/libs/underscore-1.4.3.min.js"></script> + <script src="../../assets/libs/jquery.pailer.js"></script> + + <script> + var $body = $('body'); + var $data = $('#data'); + + function resize() { + var margin_left = parseInt($body.css('margin-left')); + var margin_top = parseInt($body.css('margin-top')); + var margin_bottom = parseInt($body.css('margin-bottom')); + $data + .width($(window).width() - margin_left) + .height($(window).height() - margin_top - margin_bottom); + } + + $(window).resize(resize); + + // Set target URL in `sessionStorage` and clean it in `localStorage`. + (function() { + // Avoid fetching the target URL if reloading the pailer windoow. + if (sessionStorage.getItem('isReloaded') !== 'true') { + var storageKey = window.name; + sessionStorage.setItem(storageKey, localStorage.getItem(storageKey)); + localStorage.removeItem(storageKey); + + sessionStorage.setItem('isReloaded', 'true'); + } + })(); + + $(document).ready(function() { + resize(); + + var storageKey = window.name; + + $data.pailer({ + read: function(options) { + var settings = $.extend({ + 'offset': -1, + 'length': -1 + }, options); + var url = sessionStorage.getItem(storageKey) + + '&offset=' + settings.offset + + '&length=' + settings.length + + '&jsonp=?'; + return $.getJSON(url); + }, + 'indicator': $('#indicator') + }); + }); + </script> + </body> +</html> http://git-wip-us.apache.org/repos/asf/mesos/blob/c7685917/src/webui/app/shared/table-header.html ---------------------------------------------------------------------- diff --git a/src/webui/app/shared/table-header.html b/src/webui/app/shared/table-header.html new file mode 100644 index 0000000..448f67e --- /dev/null +++ b/src/webui/app/shared/table-header.html @@ -0,0 +1,20 @@ +<div class="row"> + <div class="col-md-8"> + <h3 id="frameworks">{{headerTitle}}</h3> + </div> + <div class="col-md-4"> + <div class="input-group input-group-sm input-group-header" + ng-hide="originalData.length === 0"> + <span class="input-group-addon" + ng-class="{ 'input-group-addon-success': filterTerm.length > 0 }"> + <i class="glyphicon glyphicon-filter"></i> + </span> + <input type="search" ng-model="filterTerm" placeholder="Find..." + class="form-control input-sm"> + <button ng-click="filterTerm = ''" ng-show="filterTerm.length > 0" + class="btn btn-clear btn-xs input-group-inner-right" title="Clear"> + × + </button> + </div> + </div> +</div> http://git-wip-us.apache.org/repos/asf/mesos/blob/c7685917/src/webui/app/shared/timestamp.html ---------------------------------------------------------------------- diff --git a/src/webui/app/shared/timestamp.html b/src/webui/app/shared/timestamp.html new file mode 100644 index 0000000..5e422b9 --- /dev/null +++ b/src/webui/app/shared/timestamp.html @@ -0,0 +1,5 @@ +<time datetime="{{value | isoDate}}" ng-click="toggle()" ng-show="longDate"> + {{value | isoDate}}</time> +<time datetime="{{value | isoDate}}" ng-click="toggle()" ng-hide="longDate"> + {{value | relativeDate:pollTime}}</time> +<span ng-transclude></span>