You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@eagle.apache.org by ji...@apache.org on 2016/09/28 05:38:54 UTC

[13/14] incubator-eagle git commit: [EAGLE-574] UI refactor for support 0.5 api

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/index.js
----------------------------------------------------------------------
diff --git a/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/index.js b/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/index.js
new file mode 100644
index 0000000..fafe699
--- /dev/null
+++ b/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/index.js
@@ -0,0 +1,489 @@
+/*
+ * 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 () {
+	/**
+	 * `register` is global function that for application to set up 'controller', 'service', 'directive', 'route' in Eagle
+	 */
+	var jpmApp = register(['ngRoute', 'ngAnimate', 'ui.router', 'eagle.service']);
+
+	jpmApp.route("jpmList", {
+		url: "/jpm/list?startTime&endTime",
+		site: true,
+		templateUrl: "partials/job/list.html",
+		controller: "listCtrl",
+		resolve: { time: true }
+	}).route("jpmOverview", {
+		url: "/jpm/overview?startTime&endTime",
+		site: true,
+		templateUrl: "partials/job/overview.html",
+		controller: "overviewCtrl",
+		resolve: { time: true }
+	}).route("jpmStatistics", {
+		url: "/jpm/statistics",
+		site: true,
+		templateUrl: "partials/job/statistic.html",
+		controller: "statisticCtrl"
+	}).route("jpmDetail", {
+		url: "/jpm/detail/:jobId",
+		site: true,
+		templateUrl: "partials/job/detail.html",
+		controller: "detailCtrl"
+	}).route("jpmJobTask", {
+		url: "/jpm/jobTask/:jobId?startTime&endTime",
+		site: true,
+		templateUrl: "partials/job/task.html",
+		controller: "jobTaskCtrl"
+	}).route("jpmCompare", {
+		url: "/jpm/compare/:jobDefId?from&to",
+		site: true,
+		reloadOnSearch: false,
+		templateUrl: "partials/job/compare.html",
+		controller: "compareCtrl"
+	});
+
+	jpmApp.portal({name: "YARN Jobs", icon: "taxi", list: [
+		{name: "Overview", path: "jpm/overview"},
+		{name: "Job Statistics", path: "jpm/statistics"},
+		{name: "Job List", path: "jpm/list"}
+	]}, true);
+
+	jpmApp.service("JPM", function ($q, $http, Time, Site, Application) {
+		var JPM = window._JPM = {};
+
+		// TODO: timestamp support
+		JPM.QUERY_LIST = '${baseURL}/rest/entities?query=${query}[${condition}]{${fields}}&pageSize=${limit}&startTime=${startTime}&endTime=${endTime}';
+		JPM.QUERY_GROUPS = '${baseURL}/rest/entities?query=${query}[${condition}]<${groups}>{${field}}${order}${top}&pageSize=${limit}&startTime=${startTime}&endTime=${endTime}';
+		JPM.QUERY_GROUPS_INTERVAL = '${baseURL}/rest/entities?query=${query}[${condition}]<${groups}>{${field}}${order}${top}&pageSize=${limit}&startTime=${startTime}&endTime=${endTime}&intervalmin=${intervalMin}&timeSeries=true';
+		JPM.QUERY_METRICS = '${baseURL}/rest/entities?query=GenericMetricService[${condition}]{*}&metricName=${metric}&pageSize=${limit}&startTime=${startTime}&endTime=${endTime}';
+		JPM.QUERY_METRICS_AGG = '${baseURL}/rest/entities?query=GenericMetricService[${condition}]<${groups}>{${field}}${order}${top}&metricName=${metric}&pageSize=${limit}&startTime=${startTime}&endTime=${endTime}';
+		JPM.QUERY_METRICS_INTERVAL = '${baseURL}/rest/entities?query=GenericMetricService[${condition}]<${groups}>{${field}}${order}${top}&metricName=${metric}&pageSize=${limit}&startTime=${startTime}&endTime=${endTime}&intervalmin=${intervalMin}&timeSeries=true';
+		JPM.QUERY_MR_JOBS = '${baseURL}/rest/mrJobs/search';
+		JPM.QUERY_JOB_LIST = '${baseURL}/rest/mrJobs?query=%s[${condition}]{${fields}}&pageSize=${limit}&startTime=${startTime}&endTime=${endTime}';
+		JPM.QUERY_JOB_STATISTIC = '${baseURL}/rest/mrJobs/jobCountsByDuration?site=${site}&timeDistInSecs=${times}&startTime=${startTime}&endTime=${endTime}&jobType=${jobType}';
+		JPM.QUERY_TASK_STATISTIC = '${baseURL}/rest/mrTasks/taskCountsByDuration?jobId=${jobId}&site=${site}&timeDistInSecs=${times}&top=${top}';
+
+		JPM.QUERY_MR_JOB_COUNT = '${baseURL}/rest/mrJobs/runningJobCounts';
+		//JPM.QUERY_MR_JOB_METRIC_TOP = '${baseURL}eagle-service/rest/mrJobs/jobMetrics/entities';
+
+		/**
+		 * Fetch query content with current site application configuration
+		 * @param {string} queryName
+		 */
+		var getQuery = JPM.getQuery = function(queryName, siteId) {
+			var baseURL;
+			siteId = siteId || Site.current().siteId;
+			var app = Application.find("JPM_WEB_APP", siteId)[0];
+			var host = app.configuration["service.host"];
+			var port = app.configuration["service.port"];
+
+			if(!host && !port) {
+				baseURL = "";
+			} else {
+				if(host === "localhost" || !host) {
+					host = location.hostname;
+				}
+				if(!port) {
+					port = location.port;
+				}
+				baseURL = "http://" + host + ":" + port;
+			}
+
+			return common.template(JPM["QUERY_" + queryName], {baseURL: baseURL});
+		};
+
+
+		function wrapList(promise) {
+			var _list = [];
+			_list._done = false;
+
+			_list._promise = promise.then(
+				/**
+				 * @param {{}} res
+				 * @param {{}} res.data
+				 * @param {{}} res.data.obj
+				 */
+				function (res) {
+				_list.splice(0);
+				Array.prototype.push.apply(_list, res.data.obj);
+				_list._done = true;
+				return _list;
+			});
+			return _list;
+		}
+
+		function toFields(fields) {
+			return (fields || []).length > 0 ? $.map(fields, function (field) {
+				return "@" + field;
+			}).join(",") : "*";
+		}
+
+		JPM.get = function (url, params) {
+			return $http({
+				url: url,
+				method: "GET",
+				params: params
+			});
+		};
+
+		JPM.condition = function (condition) {
+			return $.map(condition, function (value, key) {
+				return "@" + key + '="' + value + '"';
+			}).join(" AND ");
+		};
+
+		/**
+		 * Fetch eagle query list
+		 * @param query
+		 * @param condition
+		 * @param {[]?} groups
+		 * @param {string} field
+		 * @param {number|null} intervalMin
+		 * @param startTime
+		 * @param endTime
+		 * @param {(number|null)?} top
+		 * @param {number?} limit
+		 * @return {[]}
+		 */
+		JPM.groups = function (query, condition, groups, field, intervalMin, startTime, endTime, top, limit) {
+			var fields = field.split(/\s*,\s*/);
+			var orderId = -1;
+			var fieldStr = $.map(fields, function (field, index) {
+				var matches = field.match(/^([^\s]*)(\s+.*)?$/);
+				if(matches[2]) {
+					orderId = index;
+				}
+				return matches[1];
+			}).join(", ");
+
+			var config = {
+				query: query,
+				condition: JPM.condition(condition),
+				startTime: Time.format(startTime),
+				endTime: Time.format(endTime),
+				groups: toFields(groups),
+				field: fieldStr,
+				order: orderId === -1 ? "" : ".{" + fields[orderId] + "}",
+				top: top ? "&top=" + top : "",
+				intervalMin: intervalMin,
+				limit: limit || 100000
+			};
+
+			var metrics_url = common.template(intervalMin ? getQuery("GROUPS_INTERVAL") : getQuery("GROUPS"), config);
+			var _list = wrapList(JPM.get(metrics_url));
+			_list._aggInfo = {
+				groups: groups,
+				startTime: Time(startTime).valueOf(),
+				interval: intervalMin * 60 * 1000
+			};
+			_list._promise.then(function () {
+				if(top) _list.reverse();
+			});
+			return _list;
+		};
+
+		/**
+		 * Fetch eagle query list
+		 * @param {string} query
+		 * @param {{}?} condition
+		 * @param {(string|number|{})?} startTime
+		 * @param {(string|number|{})?} endTime
+		 * @param {[]?} fields
+		 * @param {number?} limit
+		 * @return {[]}
+		 */
+		JPM.list = function (query, condition, startTime, endTime, fields, limit) {
+			var config = {
+				query: query,
+				condition: JPM.condition(condition),
+				startTime: Time.format(startTime),
+				endTime: Time.format(endTime),
+				fields: toFields(fields),
+				limit: limit || 10000
+			};
+
+			return wrapList(JPM.get(common.template(getQuery("LIST"), config)));
+		};
+
+		/**
+		 * Fetch job list
+		 * @param condition
+		 * @param startTime
+		 * @param endTime
+		 * @param {[]?} fields
+		 * @param {number?} limit
+		 * @return {[]}
+		 */
+		JPM.jobList = function (condition, startTime, endTime, fields, limit) {
+			var config = {
+				condition: JPM.condition(condition),
+				startTime: Time.format(startTime),
+				endTime: Time.format(endTime),
+				fields: toFields(fields),
+				limit: limit || 10000
+			};
+
+			var jobList_url = common.template(getQuery("JOB_LIST"), config);
+			return wrapList(JPM.get(jobList_url));
+		};
+
+		/**
+		 * Fetch job metric list
+		 * @param condition
+		 * @param metric
+		 * @param startTime
+		 * @param endTime
+		 * @param {number?} limit
+		 * @return {[]}
+		 */
+		JPM.metrics = function (condition, metric, startTime, endTime, limit) {
+			var config = {
+				condition: JPM.condition(condition),
+				startTime: Time.format(startTime),
+				endTime: Time.format(endTime),
+				metric: metric,
+				limit: limit || 10000
+			};
+
+			var metrics_url = common.template(getQuery("METRICS"), config);
+			var _list = wrapList(JPM.get(metrics_url));
+			_list._promise.then(function () {
+				_list.reverse();
+			});
+			return _list;
+		};
+
+		/**
+		 * Fetch job metric list
+		 * @param {{}} condition
+		 * @param {string} metric
+		 * @param {[]} groups
+		 * @param {string} field
+		 * @param {number|null|false} intervalMin
+		 * @param startTime
+		 * @param endTime
+		 * @param {number?} top
+		 * @param {number?} limit
+		 * @return {[]}
+		 */
+		JPM.aggMetrics = function (condition, metric, groups, field, intervalMin, startTime, endTime, top, limit) {
+			var fields = field.split(/\s*,\s*/);
+			var orderId = -1;
+			var fieldStr = $.map(fields, function (field, index) {
+				var matches = field.match(/^([^\s]*)(\s+.*)?$/);
+				if(matches[2]) {
+					orderId = index;
+				}
+				return matches[1];
+			}).join(", ");
+
+			var config = {
+				condition: JPM.condition(condition),
+				startTime: Time.format(startTime),
+				endTime: Time.format(endTime),
+				metric: metric,
+				groups: toFields(groups),
+				field: fieldStr,
+				order: orderId === -1 ? "" : ".{" + fields[orderId] + "}",
+				top: top ? "&top=" + top : "",
+				intervalMin: intervalMin,
+				limit: limit || 100000
+			};
+
+			var metrics_url = common.template(intervalMin ? getQuery("METRICS_INTERVAL") : getQuery("METRICS_AGG"), config);
+			var _list = wrapList(JPM.get(metrics_url));
+			_list._aggInfo = {
+				groups: groups,
+				startTime: Time(startTime).valueOf(),
+				interval: intervalMin * 60 * 1000
+			};
+			_list._promise.then(function () {
+				_list.reverse();
+			});
+			return _list;
+		};
+
+		JPM.aggMetricsToEntities = function (list, flatten) {
+			var _list = [];
+			_list.done = false;
+			_list._promise = list._promise.then(function () {
+				var _startTime = list._aggInfo.startTime;
+				var _interval = list._aggInfo.interval;
+
+				$.each(list, function (i, obj) {
+					var tags = {};
+					$.each(list._aggInfo.groups, function (j, group) {
+						tags[group] = obj.key[j];
+					});
+
+					var _subList = $.map(obj.value[0], function (value, index) {
+						return {
+							timestamp: _startTime + index * _interval,
+							value: [value],
+							tags: tags
+						};
+					});
+
+					if(flatten) {
+						_list.push.apply(_list, _subList);
+					} else {
+						_list.push(_subList);
+					}
+				});
+				_list.done = true;
+				return _list;
+			});
+			return _list;
+		};
+
+		/**
+		 * Fetch job duration distribution
+		 * @param {string} site
+		 * @param {string} jobType
+		 * @param {string} times
+		 * @param {{}} startTime
+		 * @param {{}} endTime
+		 */
+		JPM.jobDistribution = function (site, jobType, times, startTime, endTime) {
+			var url = common.template(getQuery("JOB_STATISTIC"), {
+				site: site,
+				jobType: jobType,
+				times: times,
+				startTime: Time.format(startTime),
+				endTime: Time.format(endTime)
+			});
+			return JPM.get(url);
+		};
+
+		JPM.taskDistribution = function (site, jobId, times, top) {
+			var url = common.template(getQuery("TASK_STATISTIC"), {
+				site: site,
+				jobId: jobId,
+				times: times,
+				top: top || 10
+			});
+			return JPM.get(url);
+		};
+
+		/**
+		 * Get job list by sam jobDefId
+		 * @param {string} site
+		 * @param {string|undefined?} jobDefId
+		 * @param {string|undefined?} jobId
+		 * @return {[]}
+		 */
+		JPM.findMRJobs = function (site, jobDefId, jobId) {
+			return wrapList(JPM.get(getQuery("MR_JOBS"), {
+				site: site,
+				jobDefId: jobDefId,
+				jobId: jobId
+			}));
+		};
+
+		/**
+		 * Convert Entity list data to Chart supported series
+		 * @param name
+		 * @param metrics
+		 * @param {{}|boolean?} rawData
+		 * @param {{}?} option
+		 * @return {{name: *, symbol: string, type: string, data: *}}
+		 */
+		JPM.metricsToSeries = function(name, metrics, rawData, option) {
+			if(arguments.length === 3 && typeof rawData === "object") {
+				option = rawData;
+				rawData = false;
+			}
+
+			var data = $.map(metrics, function (metric) {
+				return rawData ? metric.value[0] : {
+					x: metric.timestamp,
+					y: metric.value[0]
+				};
+			});
+			return $.extend({
+				name: name,
+				symbol: 'none',
+				type: "line",
+				data: data
+			}, option || {});
+		};
+
+		JPM.metricsToInterval = function (metricList, interval) {
+			if(metricList.length === 0) return [];
+
+			var list = $.map(metricList, function (metric) {
+				var timestamp = Math.floor(metric.timestamp / interval) * interval;
+				var remainderPtg = (metric.timestamp % interval) / interval;
+				return {
+					timestamp: remainderPtg < 0.5 ? timestamp : timestamp + interval,
+					value: [metric.value[0]]
+				};
+			});
+
+			var resultList = [list[0]];
+			for(var i = 1 ; i < list.length ; i += 1) {
+				var start = list[i - 1];
+				var end = list[i];
+
+				var distance = (end.timestamp - start.timestamp);
+				if(distance > 0) {
+					var steps = distance / interval;
+					var des = (end.value[0] - start.value[0]) / steps;
+					for (var j = 1; j <= steps; j += 1) {
+						resultList.push({
+							timestamp: start.timestamp + j * interval,
+							value: [start.value[0] + des * j]
+						});
+					}
+				}
+			}
+			return resultList;
+		};
+
+		JPM.getStateClass = function (state) {
+			switch ((state || "").toUpperCase()) {
+				case "NEW":
+				case "NEW_SAVING":
+				case "SUBMITTED":
+				case "ACCEPTED":
+					return "warning";
+				case "RUNNING":
+					return "info";
+				case "SUCCESS":
+				case "SUCCEEDED":
+					return "success";
+				case "FINISHED":
+					return "primary";
+				case "FAILED":
+					return "danger";
+			}
+			return "default";
+		};
+
+		return JPM;
+	});
+
+	jpmApp.requireCSS("style/index.css");
+	jpmApp.require("widget/jobStatistic.js");
+	jpmApp.require("ctrl/overviewCtrl.js");
+	jpmApp.require("ctrl/statisticCtrl.js");
+	jpmApp.require("ctrl/listCtrl.js");
+	jpmApp.require("ctrl/detailCtrl.js");
+	jpmApp.require("ctrl/jobTaskCtrl.js");
+	jpmApp.require("ctrl/compareCtrl.js");
+})();

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/partials/job/compare.html
----------------------------------------------------------------------
diff --git a/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/partials/job/compare.html b/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/partials/job/compare.html
new file mode 100644
index 0000000..4ab8140
--- /dev/null
+++ b/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/partials/job/compare.html
@@ -0,0 +1,274 @@
+<!--
+  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.
+  -->
+
+<div class="row flex">
+	<div class="col-sm-12 col-md-3">
+		<div class="box box-primary">
+			<div class="box-header with-border">
+				<h3 class="box-title">
+					Summary
+				</h3>
+			</div>
+			<div class="box-body">
+				<table class="table table-striped">
+					<tbody>
+						<tr>
+							<th>Def Id</th>
+							<td class="text-break">{{jobDefId}}</td>
+						</tr>
+						<tr>
+							<th>Type</th>
+							<td>{{jobList[0].tags.jobType}}</td>
+						</tr>
+						<tr>
+							<th>Site</th>
+							<td>{{jobList[0].tags.site}}</td>
+						</tr>
+						<tr>
+							<th>Owner</th>
+							<td>{{jobList[0].tags.user}}</td>
+						</tr>
+						<tr>
+							<th>Queue</th>
+							<td>{{jobList[0].tags.queue}}</td>
+						</tr>
+					</tbody>
+				</table>
+			</div>
+		</div>
+	</div>
+	<div class="col-sm-12 col-md-9">
+		<div class="box box-primary">
+			<div class="box-header with-border">
+				<h3 class="box-title">
+					Comparison
+					<small>
+						Click to compare job
+						(ctrl + click: set <strong>from Job</strong>, shift + click: set <strong>to Job</strong>)
+					</small>
+				</h3>
+			</div>
+			<div class="box-body">
+				<div class="jpm-chart">
+					<div chart="trendChart" class="jpm-chart-container" series="jobTrendSeries" category="jobTrendCategory"
+						 ng-click="compareJobSelect" option="jobTrendOption"></div>
+					<div ng-if="(jobTrendSeries || []).length === 0" class="overlay">
+						<i class="fa fa-refresh fa-spin"></i>
+					</div>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+
+<div class="box box-primary" ng-if="fromJob && toJob">
+	<div class="box-header with-border">
+		<h3 class="box-title">
+			Comparison
+		</h3>
+		<div class="box-tools pull-right">
+			<button type="button" class="btn btn-box-tool" data-widget="collapse">
+				<i class="fa fa-minus"></i>
+			</button>
+		</div>
+	</div>
+	<div class="box-body">
+		<table class="table table-striped">
+			<thead>
+			<tr>
+				<th>Field</th>
+				<th>From</th>
+				<th>To</th>
+				<th>Field</th>
+				<th>From</th>
+				<th>To</th>
+			</tr>
+			</thead>
+			<tbody>
+			<tr>
+				<th>
+					Job Id
+					<a class="fa fa-retweet" ng-click="exchangeJobs()"></a>
+				</th>
+				<td><a ui-sref="jpmDetail({siteId: site, jobId: fromJob.tags.jobId})">{{fromJob.tags.jobId}}</a></td>
+				<td><a ui-sref="jpmDetail({siteId: site, jobId: toJob.tags.jobId})">{{toJob.tags.jobId}}</a></td>
+				<th>Duration</th>
+				<td>{{Time.diffStr(fromJob.durationTime)}}</td>
+				<td>
+					{{Time.diffStr(toJob.durationTime)}}
+					<span class="{{jobCompareClass('durationTime')}}">{{jobCompareValue('durationTime')}}</span>
+				</td>
+			</tr>
+			<tr>
+				<th>Total Maps</th>
+				<td>{{common.number.toFixed(fromJob.numTotalMaps)}}</td>
+				<td>
+					{{common.number.toFixed(toJob.numTotalMaps)}}
+					<span class="{{jobCompareClass('numTotalMaps')}}">{{jobCompareValue('numTotalMaps')}}</span>
+				</td>
+				<th>Total Reduces</th>
+				<td>{{common.number.toFixed(fromJob.numTotalReduces)}}</td>
+				<td>
+					{{common.number.toFixed(toJob.numTotalReduces)}}
+					<span class="{{jobCompareClass('numTotalReduces')}}">{{jobCompareValue('numTotalReduces')}}</span>
+				</td>
+			</tr>
+			<tr>
+				<th>HDFS Read Bytes</th>
+				<td>{{common.number.toFixed(fromJob.jobCounters.counters["org.apache.hadoop.mapreduce.FileSystemCounter"].HDFS_BYTES_READ)}}</td>
+				<td>
+					{{common.number.toFixed(toJob.jobCounters.counters["org.apache.hadoop.mapreduce.FileSystemCounter"].HDFS_BYTES_READ)}}
+					<span class="{{jobCompareClass(['jobCounters','counters','org.apache.hadoop.mapreduce.FileSystemCounter','HDFS_BYTES_READ'])}}">
+							{{jobCompareValue(['jobCounters','counters','org.apache.hadoop.mapreduce.FileSystemCounter','HDFS_BYTES_READ'])}}
+						</span>
+				</td>
+				<th>HDFS Write Bytes</th>
+				<td>{{common.number.toFixed(fromJob.jobCounters.counters["org.apache.hadoop.mapreduce.FileSystemCounter"].HDFS_BYTES_WRITTEN)}}</td>
+				<td>
+					{{common.number.toFixed(toJob.jobCounters.counters["org.apache.hadoop.mapreduce.FileSystemCounter"].HDFS_BYTES_WRITTEN)}}
+					<span class="{{jobCompareClass(['jobCounters','counters','org.apache.hadoop.mapreduce.FileSystemCounter','HDFS_BYTES_WRITTEN'])}}">
+							{{jobCompareValue(['jobCounters','counters','org.apache.hadoop.mapreduce.FileSystemCounter','HDFS_BYTES_WRITTEN'])}}
+						</span>
+				</td>
+			</tr>
+			<tr>
+				<th>Local Read Bytes</th>
+				<td>{{common.number.toFixed(fromJob.jobCounters.counters["org.apache.hadoop.mapreduce.FileSystemCounter"].FILE_BYTES_READ)}}</td>
+				<td>
+					{{common.number.toFixed(toJob.jobCounters.counters["org.apache.hadoop.mapreduce.FileSystemCounter"].FILE_BYTES_READ)}}
+					<span class="{{jobCompareClass(['jobCounters','counters','org.apache.hadoop.mapreduce.FileSystemCounter','FILE_BYTES_READ'])}}">
+							{{jobCompareValue(['jobCounters','counters','org.apache.hadoop.mapreduce.FileSystemCounter','FILE_BYTES_READ'])}}
+						</span>
+				</td>
+				<th>Local Write Bytes</th>
+				<td>{{common.number.toFixed(fromJob.jobCounters.counters["org.apache.hadoop.mapreduce.FileSystemCounter"].FILE_BYTES_WRITTEN)}}</td>
+				<td>
+					{{common.number.toFixed(toJob.jobCounters.counters["org.apache.hadoop.mapreduce.FileSystemCounter"].FILE_BYTES_WRITTEN)}}
+					<span class="{{jobCompareClass(['jobCounters','counters','org.apache.hadoop.mapreduce.FileSystemCounter','FILE_BYTES_WRITTEN'])}}">
+							{{jobCompareValue(['jobCounters','counters','org.apache.hadoop.mapreduce.FileSystemCounter','FILE_BYTES_WRITTEN'])}}
+						</span>
+				</td>
+			</tr>
+			<tr>
+				<th>Last Map Duration</th>
+				<td>{{common.number.toFixed(fromJob.lastMapDuration)}}</td>
+				<td>
+					{{common.number.toFixed(toJob.lastMapDuration)}}
+					<span class="{{jobCompareClass('lastMapDuration')}}">{{jobCompareValue('lastMapDuration')}}</span>
+				</td>
+				<th>Last Reduce Duration</th>
+				<td>{{common.number.toFixed(fromJob.lastReduceDuration)}}</td>
+				<td>
+					{{common.number.toFixed(toJob.lastReduceDuration)}}
+					<span class="{{jobCompareClass('lastReduceDuration')}}">{{jobCompareValue('lastReduceDuration')}}</span>
+				</td>
+			</tr>
+			<tr>
+				<th>Data Local Maps</th>
+				<td>{{common.number.toFixed(fromJob.dataLocalMapsPercentage * 100)}}%</td>
+				<td>
+					{{common.number.toFixed(toJob.dataLocalMapsPercentage * 100)}}%
+					<span class="{{jobCompareClass('dataLocalMapsPercentage')}}">{{jobCompareValue('dataLocalMapsPercentage')}}</span>
+				</td>
+				<th>Rack Local Maps</th>
+				<td>{{common.number.toFixed(fromJob.rackLocalMapsPercentage * 100)}}%</td>
+				<td>
+					{{common.number.toFixed(toJob.rackLocalMapsPercentage * 100)}}%
+					<span class="{{jobCompareClass('rackLocalMapsPercentage')}}">{{jobCompareValue('rackLocalMapsPercentage')}}</span>
+				</td>
+			</tr>
+			</tbody>
+		</table>
+
+		<div class="row">
+			<div class="col-lg-6 col-md-12">
+				<div class="jpm-chart">
+					<div chart class="jpm-chart-container" series="comparisonChart_Container.series"
+						 category="comparisonChart_Container.categories"></div>
+					<div ng-if="(comparisonChart_Container.series || []).length === 0" class="overlay">
+						<i class="fa fa-refresh fa-spin"></i>
+					</div>
+				</div>
+			</div>
+			<div class="col-lg-6 col-md-12">
+				<div class="jpm-chart">
+					<div chart class="jpm-chart-container" series="comparisonChart_allocatedMB.series"
+						 category="comparisonChart_allocatedMB.categories"></div>
+					<div ng-if="(comparisonChart_allocatedMB.series || []).length === 0" class="overlay">
+						<i class="fa fa-refresh fa-spin"></i>
+					</div>
+				</div>
+			</div>
+			<div class="col-lg-6 col-md-12">
+				<div class="jpm-chart">
+					<div chart class="jpm-chart-container" series="comparisonChart_vCores.series"
+						 category="comparisonChart_vCores.categories"></div>
+					<div ng-if="(comparisonChart_vCores.series || []).length === 0" class="overlay">
+						<i class="fa fa-refresh fa-spin"></i>
+					</div>
+				</div>
+			</div>
+			<div class="col-lg-6 col-md-12">
+				<div class="jpm-chart">
+					<div chart class="jpm-chart-container" series="comparisonChart_taskDistribution.series"
+						 category="comparisonChart_taskDistribution.categories"></div>
+					<div ng-if="(comparisonChart_taskDistribution.series || []).length === 0" class="overlay">
+						<i class="fa fa-refresh fa-spin"></i>
+					</div>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+
+<div class="box box-primary" ng-if="jobList.length">
+	<div class="box-header with-border">
+		<h3 class="box-title">
+			History Jobs
+		</h3>
+	</div>
+	<div class="box-body">
+		<div sort-table="jobList" sortpath="-startTime">
+			<table class="table table-bordered table-striped">
+				<thead>
+					<tr>
+						<th width="10" sortpath="currentState">Status</th>
+						<th sortpath="tags.jobId">Id</th>
+						<th sortpath="tags.jobName">Name</th>
+						<th width="140" sortpath="startTime">Start Time</th>
+						<th width="140" sortpath="durationTime">Duration</th>
+					</tr>
+				</thead>
+				<tbody>
+					<tr>
+						<td><span class="label label-{{getStateClass(item.currentState)}}">{{item.currentState}}</span></td>
+						<td class="text-no-break">
+							<span ng-if="item.tags.jobId === fromJob.tags.jobId">[From]</span>
+							<span ng-if="item.tags.jobId === toJob.tags.jobId">[To]</span>
+							<a ng-click="compareJobSelect($event, item)">{{item.tags.jobId}}</a>
+							<a class="fa fa-link" ui-sref="jpmDetail({siteId: site, jobId: item.tags.jobId})" target="_blank"></a>
+						</td>
+						<td class="text-break">{{item.tags.jobName}}</td>
+						<td>{{Time.format(item.startTime)}}</td>
+						<td>{{Time.diffStr(item.durationTime)}}</td>
+					</tr>
+				</tbody>
+			</table>
+		</div>
+	</div>
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/partials/job/detail.html
----------------------------------------------------------------------
diff --git a/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/partials/job/detail.html b/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/partials/job/detail.html
new file mode 100644
index 0000000..57561ba
--- /dev/null
+++ b/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/partials/job/detail.html
@@ -0,0 +1,256 @@
+<!--
+  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.
+  -->
+
+<div class="row flex">
+	<div class="col-lg-6 col-md-12">
+		<div class="box box-primary">
+			<div class="box-header with-border">
+				<h3 class="box-title">
+					Job Info
+					<span class="label label-{{getStateClass(job.currentState)}}">{{job.currentState}}</span>
+				</h3>
+				<div class="pull-right box-tools">
+					<a ui-sref="jpmCompare({siteId: site, jobDefId: job.tags.jobDefId, to: job.tags.jobId})" class="btn btn-primary btn-xs">
+						<span class="fa fa-code-fork"></span>
+						Compare
+					</a>
+				</div>
+			</div>
+			<div class="box-body">
+				<table class="table table-striped">
+					<tbody>
+						<tr>
+							<th>Job Name</th>
+							<td class="text-break">{{job.tags.jobName}}</td>
+							<th>Job Def Id</th>
+							<td class="text-break">{{job.tags.jobDefId}}</td>
+						</tr>
+						<tr>
+							<th>Job Id</th>
+							<td class="text-break">
+								{{job.tags.jobId}}
+								<a class="fa fa-link" href="{{job.trackingUrl}}" target="_blank" ng-if="job.trackingUrl"></a>
+							</td>
+							<th>Job Exec Id</th>
+							<td class="text-break">{{job.tags.jobExecId}}</td>
+						</tr>
+						<tr>
+							<th>User</th>
+							<td>{{job.tags.user}}</td>
+							<th>Queue</th>
+							<td>{{job.tags.queue}}</td>
+						</tr>
+						<tr>
+							<th>Site</th>
+							<td>{{job.tags.site}}</td>
+							<th>Job Type</th>
+							<td>{{job.tags.jobType}}</td>
+						</tr>
+						<tr>
+							<th>Submission Time</th>
+							<td>{{Time.format(job.submissionTime)}}</td>
+							<th>Duration</th>
+							<td class="text-light-blue">{{Time.diffStr(job.durationTime)}}</td>
+						</tr>
+						<tr>
+							<th>Start Time</th>
+							<td>{{Time.format(job.startTime)}}</td>
+							<th>End Time</th>
+							<td>{{Time.format(job.endTime)}}</td>
+						</tr>
+					</tbody>
+				</table>
+			</div>
+
+			<div ng-if="!job" class="overlay">
+				<i class="fa fa-refresh fa-spin"></i>
+			</div>
+		</div>
+	</div>
+
+	<div class="col-lg-6 col-md-12">
+		<div class="box box-primary">
+			<div class="box-header with-border">
+				<h3 class="box-title">
+					Map Reduce
+				</h3>
+			</div>
+			<div class="box-body">
+				<table class="table table-striped">
+					<tbody>
+						<tr>
+							<th>Finished Maps</th>
+							<td class="text-success">{{common.number.toFixed(job.numFinishedMaps)}}</td>
+							<th>Failed Maps</th>
+							<td class="text-danger">{{common.number.toFixed(job.numFailedMaps)}}</td>
+							<th>Total Maps</th>
+							<td>{{common.number.toFixed(job.numTotalMaps)}}</td>
+						</tr>
+						<tr>
+							<th>Finished Reduces</th>
+							<td class="text-success">{{common.number.toFixed(job.numFinishedReduces)}}</td>
+							<th>Failed Reduces</th>
+							<td class="text-danger">{{common.number.toFixed(job.numFailedReduces)}}</td>
+							<th>Total Reduces</th>
+							<td>{{common.number.toFixed(job.numTotalReduces)}}</td>
+						</tr>
+						<tr>
+							<th>Data Local Maps</th>
+							<td>
+								{{common.number.toFixed(job.dataLocalMaps)}}
+								({{common.number.toFixed(job.dataLocalMapsPercentage * 100)}}%)
+							</td>
+							<th>Rack Local Maps</th>
+							<td>
+								{{common.number.toFixed(job.rackLocalMaps)}}
+								({{common.number.toFixed(job.rackLocalMapsPercentage * 100)}}%)
+							</td>
+							<th>Total Launched Maps</th>
+							<td>{{common.number.toFixed(job.totalLaunchedMaps)}}</td>
+						</tr>
+						<tr>
+							<th>Map vCores</th>
+							<td>{{common.number.toFixed(job.jobCounters.counters["org.apache.hadoop.mapreduce.JobCounter"].VCORES_MILLIS_MAPS)}}</td>
+							<th>Map CPU</th>
+							<td>{{common.number.toFixed(job.jobCounters.counters.MapTaskAttemptCounter.CPU_MILLISECONDS)}}</td>
+							<th>HDFS Read Bytes</th>
+							<td>{{common.number.toFixed(job.jobCounters.counters["org.apache.hadoop.mapreduce.FileSystemCounter"].HDFS_BYTES_READ)}}</td>
+						</tr>
+						<tr>
+							<th>Reduce vCores</th>
+							<td>{{common.number.toFixed(job.jobCounters.counters["org.apache.hadoop.mapreduce.JobCounter"].VCORES_MILLIS_REDUCES)}}</td>
+							<th>Map CPU</th>
+							<td>{{common.number.toFixed(job.jobCounters.counters.ReduceTaskAttemptCounter.CPU_MILLISECONDS)}}</td>
+							<th>HDFS Write Bytes</th>
+							<td>{{common.number.toFixed(job.jobCounters.counters["org.apache.hadoop.mapreduce.FileSystemCounter"].HDFS_BYTES_WRITTEN)}}</td>
+						</tr>
+						<tr ng-if="!isRunning">
+							<th>Last Map Duration</th>
+							<td>{{Time.diffStr(job.lastMapDuration)}}</td>
+							<th>Last Reduce Duration</th>
+							<td>{{Time.diffStr(job.lastReduceDuration)}}</td>
+							<th></th>
+							<td></td>
+						</tr>
+						<tr ng-if="isRunning">
+							<th>Map Progress</th>
+							<td>{{common.number.toFixed(job.mapProgress)}}%</td>
+							<th>Reduce Progress</th>
+							<td>{{common.number.toFixed(job.reduceProgress)}}%</td>
+							<th></th>
+							<td></td>
+						</tr>
+					</tbody>
+				</table>
+			</div>
+
+			<div ng-if="!job" class="overlay">
+				<i class="fa fa-refresh fa-spin"></i>
+			</div>
+		</div>
+	</div>
+</div>
+
+<div class="box box-primary">
+	<div class="box-header with-border">
+		<h3 class="box-title">
+			Dashboards
+		</h3>
+		<div class="pull-right box-tools">
+			<a ui-sref="jpmJobTask({siteId: site, jobId: job.tags.jobId, startTime: startTimestamp, endTime: endTimestamp})"
+			   class="btn btn-primary btn-xs" target="_blank" ng-if="!isRunning">
+				<span class="fa fa-map"></span>
+				Task Statistic
+			</a>
+		</div>
+	</div>
+	<div class="box-body">
+		<div class="row">
+			<div class="col-sm-12 col-md-6">
+				<div class="jpm-chart">
+					<div chart class="jpm-chart-container" series="allocatedSeries"></div>
+					<div ng-if="(allocatedSeries || []).length === 0" class="overlay">
+						<i class="fa fa-refresh fa-spin"></i>
+					</div>
+				</div>
+			</div>
+
+			<div class="col-sm-12 col-md-6">
+				<div class="jpm-chart">
+					<div chart class="jpm-chart-container" series="vCoresSeries"></div>
+					<div ng-if="(vCoresSeries || []).length === 0" class="overlay">
+						<i class="fa fa-refresh fa-spin"></i>
+					</div>
+				</div>
+			</div>
+
+			<div class="col-sm-12 col-md-6" ng-hide="taskBucket">
+				<div class="jpm-chart">
+					<div chart class="jpm-chart-container" series="taskSeries" category="taskCategory" ng-click="taskSeriesClick"></div>
+					<div ng-if="(taskSeries || []).length === 0" class="overlay">
+						<i class="fa fa-refresh fa-spin"></i>
+					</div>
+				</div>
+			</div>
+
+			<div class="col-sm-12 col-md-6" ng-show="taskBucket">
+				<div class="jpm-chart">
+					<div class="jpm-chart-container scroll">
+						<h3>
+							<a class="fa fa-arrow-circle-o-left" ng-click="backToTaskSeries()"></a>
+							Top Tasks
+						</h3>
+
+						<table class="table table-sm table-bordered no-margin">
+							<thead>
+								<tr>
+									<!--th>Task</th-->
+									<th>Host</th>
+									<th>HDFS Read</th>
+									<th>HDFS Write</th>
+									<th>Local Read</th>
+									<th>Local Write</th>
+								</tr>
+							</thead>
+							<tbody>
+								<tr ng-repeat="task in taskBucket.topEntities track by $index">
+									<!--td>{{task.tags.taskId}}</td-->
+									<td>{{task.host || "[" + task.tags.taskId + "]"}}</td>
+									<td>{{common.number.format(task.jobCounters.counters["org.apache.hadoop.mapreduce.FileSystemCounter"].HDFS_BYTES_READ)}}</td>
+									<td>{{common.number.format(task.jobCounters.counters["org.apache.hadoop.mapreduce.FileSystemCounter"].HDFS_BYTES_WRITTEN)}}</td>
+									<td>{{common.number.format(task.jobCounters.counters["org.apache.hadoop.mapreduce.FileSystemCounter"].FILE_BYTES_READ)}}</td>
+									<td>{{common.number.format(task.jobCounters.counters["org.apache.hadoop.mapreduce.FileSystemCounter"].FILE_BYTES_WRITTEN)}}</td>
+								</tr>
+							</tbody>
+						</table>
+					</div>
+				</div>
+			</div>
+
+			<div class="col-sm-12 col-md-6">
+				<div class="jpm-chart">
+					<div chart class="jpm-chart-container" series="nodeTaskCountSeries" category="nodeTaskCountCategory"
+					></div>
+					<div ng-if="(nodeTaskCountSeries || []).length === 0" class="overlay">
+						<i class="fa fa-refresh fa-spin"></i>
+					</div>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/partials/job/list.html
----------------------------------------------------------------------
diff --git a/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/partials/job/list.html b/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/partials/job/list.html
new file mode 100644
index 0000000..d64afe3
--- /dev/null
+++ b/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/partials/job/list.html
@@ -0,0 +1,131 @@
+<!--
+  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.
+  -->
+
+<style>
+	.box .box-header .box-title small a {
+		cursor: pointer;
+		padding: 0 5px;
+		border-right: 1px solid #999;
+	}
+	.box .box-header .box-title small a:last-child {
+		border-right: none;
+	}
+	.box .box-header .box-title small a.text-default {
+		color: #999;
+	}
+
+	.box .box-header .box-title small a.active {
+		font-weight: bolder;
+		text-decoration: underline;
+	}
+</style>
+
+<div class="box box-primary">
+	<div class="box-header with-border">
+		<h3 class="box-title">
+			Job List
+			<small>
+				<a class="no-select text-{{getStateClass(state.key)}}" ng-class="{active: (tableScope.search || '').toUpperCase() === state.key}"
+					  ng-repeat="state in jobStateList" ng-click="fillSearch(state.key)">
+					{{state.key}}: {{state.value}}
+				</a>
+			</small>
+			<span ng-show="!jobList._done || isSorting" class="fa fa-refresh fa-spin no-animate"></span>
+		</h3>
+	</div>
+	<div class="box-body">
+		<div id="jobList" sort-table="jobList" is-sorting="isSorting" search-path-list="searchPathList" scope="tableScope">
+			<table class="table table-bordered">
+				<thead>
+					<tr>
+						<th sortpath="tags.jobId">Job ID</th>
+						<th sortpath="currentState">Status</th>
+						<th sortpath="tags.user" width="10">User</th>
+						<th sortpath="tags.queue">Queue</th>
+						<th sortpath="submissionTime">Submission Time</th>
+						<th sortpath="startTime">Start Time</th>
+						<th sortpath="endTime">End Time</th>
+						<th sortpath="duration">Duration</th>
+						<th sortpath="numTotalMaps">Map Tasks</th>
+						<th sortpath="numTotalReduces">Reduce Tasks</th>
+						<th sortpath="runningContainers">Containers</th>
+					</tr>
+				</thead>
+				<tbody>
+					<tr ng-repeat="item in jobList">
+						<td>
+							<a ui-sref="jpmDetail({siteId: site, jobId: item.tags.jobId})" target="_blank">{{item.tags.jobId}}</a>
+						</td>
+						<td class="text-center">
+							<span class="label label-sm label-{{getStateClass(item.currentState)}}">
+								{{item.currentState}}
+							</span>
+						</td>
+						<td>{{item.tags.user}}</td>
+						<td>{{item.tags.queue}}</td>
+						<td>{{Time.format(item.submissionTime)}}</td>
+						<td>{{Time.format(item.startTime)}}</td>
+						<td>{{Time.format(item.endTime)}}</td>
+						<td>{{Time.diffStr(item.duration)}}</td>
+						<td>{{item.numTotalMaps}}</td>
+						<td>{{item.numTotalReduces}}</td>
+						<td>{{item.runningContainers || "-"}}</td>
+					</tr>
+				</tbody>
+			</table>
+		</div>
+	</div>
+</div>
+
+<div class="box box-primary">
+	<div class="box-header with-border">
+		<h3 class="box-title">
+			Running Metrics
+		</h3>
+	</div>
+	<div class="box-body no-padding">
+		<div class="row border-split">
+			<div class="col-sm-12 col-md-6">
+				<div class="jpm-chart">
+					<h3 class="text-center">Number of Running Jobs</h3>
+					<div chart class="jpm-chart-container" series="runningTrendSeries" option="chartLeftOption"></div>
+				</div>
+			</div>
+			<div class="col-sm-12 col-md-6">
+				<div class="jpm-chart">
+					<h3 class="text-center">Running Containers</h3>
+					<div chart class="jpm-chart-container" series="runningContainersSeries" option="chartRightOption"></div>
+				</div>
+			</div>
+		</div>
+		<div class="row border-split">
+			<div class="col-sm-12 col-md-6">
+				<div class="jpm-chart">
+					<h3 class="text-center">Allocated vCores</h3>
+					<div chart class="jpm-chart-container" series="allocatedvcoresSeries" option="chartLeftOption"></div>
+				</div>
+			</div>
+			<div class="col-sm-12 col-md-6">
+				<div class="jpm-chart">
+					<h3 class="text-center">Allocated Memory (GB)</h3>
+					<div chart class="jpm-chart-container" series="allocatedMBSeries" option="allocatedMBOption"></div>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/partials/job/overview.html
----------------------------------------------------------------------
diff --git a/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/partials/job/overview.html b/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/partials/job/overview.html
new file mode 100644
index 0000000..06e85ea
--- /dev/null
+++ b/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/partials/job/overview.html
@@ -0,0 +1,347 @@
+<!--
+  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.
+  -->
+
+<div class="nav-tabs-custom">
+	<ul class="nav nav-tabs">
+		<li class="active"><a href="#hdfsBytes" data-toggle="tab">HDFS IO Bytes</a></li>
+		<li><a href="#hdfsOPs" data-toggle="tab">HDFS IO OPs</a></li>
+		<li><a href="#diskIO" data-toggle="tab">Disk IO</a></li>
+		<li><a href="#cpu" data-toggle="tab">CPU Usage</a></li>
+		<li><a href="#memory" data-toggle="tab">Memory Usage</a></li>
+		<li class="pull-right">
+			<select class="form-control" ng-model="type" ng-change="typeChange()">
+				<option ng-repeat="(type, value) in aggregationMap track by $index" value="{{type}}">By {{common.string.capitalize(type)}}</option>
+			</select>
+		</li>
+	</ul>
+	<div class="tab-content keepContent with-border">
+		<div class="tab-pane active" id="hdfsBytes">
+			<div class="row">
+				<div class="col-sm-6 col-md-8 col-lg-9">
+					<div class="jpm-chart chart-lg overlay-wrapper">
+						<h3 class="text-center">Top HDFS Bytes Read</h3>
+						<div chart class="jpm-chart-container" series="hdfsBtyesReadSeries" option="commonOption"></div>
+						<div ng-if="!hdfsBtyesReadSeries._done" class="overlay">
+							<i class="fa fa-refresh fa-spin"></i>
+						</div>
+					</div>
+				</div>
+				<div class="col-sm-6 col-md-4 col-lg-3">
+					<table class="table table-striped">
+						<thead>
+							<tr>
+								<th>Name</th>
+								<th>Total</th>
+							</tr>
+						</thead>
+						<tbody>
+							<tr ng-repeat="item in hdfsBtyesReadSeriesList track by $index">
+								<td class="text-break">
+									<a ui-sref="jpmDetail({siteId: site, jobId: item.name})" ng-if="type === 'job'" target="_blank">
+										{{item.name}}
+									</a>
+									<span ng-if="type !== 'job'">{{item.name}}</span>
+								</td>
+								<td title="{{item.total}}">{{common.number.abbr(item.total, true)}}</td>
+							</tr>
+						</tbody>
+					</table>
+				</div>
+			</div>
+
+			<hr/>
+
+			<div class="row">
+				<div class="col-sm-6 col-md-8 col-lg-9">
+					<div class="jpm-chart chart-lg overlay-wrapper">
+						<h3 class="text-center">Top HDFS Bytes Written</h3>
+						<div chart class="jpm-chart-container" series="hdfsBtyesWrittenSeries"
+							 option="commonOption"></div>
+						<div ng-if="!hdfsBtyesWrittenSeries._done" class="overlay">
+							<i class="fa fa-refresh fa-spin"></i>
+						</div>
+					</div>
+				</div>
+				<div class="col-sm-6 col-md-4 col-lg-3">
+					<table class="table table-striped">
+						<thead>
+						<tr>
+							<th>Name</th>
+							<th>Total</th>
+						</tr>
+						</thead>
+						<tbody>
+						<tr ng-repeat="item in hdfsBtyesWrittenSeriesList track by $index">
+							<td class="text-break">
+								<a ui-sref="jpmDetail({siteId: site, jobId: item.name})" ng-if="type === 'job'" target="_blank">
+									{{item.name}}
+								</a>
+								<span ng-if="type !== 'job'">{{item.name}}</span>
+							</td>
+							<td title="{{item.total}}">{{common.number.abbr(item.total, true)}}</td>
+						</tr>
+						</tbody>
+					</table>
+				</div>
+			</div>
+		</div>
+		<div class="tab-pane" id="hdfsOPs">
+			<div class="row">
+				<div class="col-sm-6 col-md-8 col-lg-9">
+					<div class="jpm-chart chart-lg overlay-wrapper">
+						<h3 class="text-center">Top HDFS Read OPs</h3>
+						<div chart class="jpm-chart-container" series="hdfsReadOpsSeries" option="commonOption"></div>
+						<div ng-if="!hdfsReadOpsSeries._done" class="overlay">
+							<i class="fa fa-refresh fa-spin"></i>
+						</div>
+					</div>
+				</div>
+				<div class="col-sm-6 col-md-4 col-lg-3">
+					<table class="table table-striped">
+						<thead>
+						<tr>
+							<th>Name</th>
+							<th>Total</th>
+						</tr>
+						</thead>
+						<tbody>
+						<tr ng-repeat="item in hdfsReadOpsSeriesList track by $index">
+							<td class="text-break">
+								<a ui-sref="jpmDetail({siteId: site, jobId: item.name})" ng-if="type === 'job'" target="_blank">
+									{{item.name}}
+								</a>
+								<span ng-if="type !== 'job'">{{item.name}}</span>
+							</td>
+							<td title="{{item.total}}">{{common.number.abbr(item.total, true)}}</td>
+						</tr>
+						</tbody>
+					</table>
+				</div>
+			</div>
+
+			<hr/>
+
+			<div class="row">
+				<div class="col-sm-6 col-md-8 col-lg-9">
+					<div class="jpm-chart chart-lg overlay-wrapper">
+						<h3 class="text-center">Top HDFS Write OPs</h3>
+						<div chart class="jpm-chart-container" series="hdfsWriteOpsSeries" option="commonOption"></div>
+						<div ng-if="!hdfsWriteOpsSeries._done" class="overlay">
+							<i class="fa fa-refresh fa-spin"></i>
+						</div>
+					</div>
+				</div>
+				<div class="col-sm-6 col-md-4 col-lg-3">
+					<table class="table table-striped">
+						<thead>
+						<tr>
+							<th>Name</th>
+							<th>Total</th>
+						</tr>
+						</thead>
+						<tbody>
+						<tr ng-repeat="item in hdfsWriteOpsSeriesList track by $index">
+							<td class="text-break">
+								<a ui-sref="jpmDetail({siteId: site, jobId: item.name})" ng-if="type === 'job'" target="_blank">
+									{{item.name}}
+								</a>
+								<span ng-if="type !== 'job'">{{item.name}}</span>
+							</td>
+							<td title="{{item.total}}">{{common.number.abbr(item.total, true)}}</td>
+						</tr>
+						</tbody>
+					</table>
+				</div>
+			</div>
+		</div>
+		<div class="tab-pane" id="diskIO">
+			<div class="row">
+				<div class="col-sm-6 col-md-8 col-lg-9">
+					<div class="jpm-chart chart-lg overlay-wrapper">
+						<h3 class="text-center">Top File Bytes Read</h3>
+						<div chart class="jpm-chart-container" series="fileBytesReadSeries" option="commonOption"></div>
+						<div ng-if="!fileBytesReadSeries._done" class="overlay">
+							<i class="fa fa-refresh fa-spin"></i>
+						</div>
+					</div>
+				</div>
+				<div class="col-sm-6 col-md-4 col-lg-3">
+					<table class="table table-striped">
+						<thead>
+						<tr>
+							<th>Name</th>
+							<th>Total</th>
+						</tr>
+						</thead>
+						<tbody>
+						<tr ng-repeat="item in fileBytesReadSeriesList track by $index">
+							<td class="text-break">
+								<a ui-sref="jpmDetail({siteId: site, jobId: item.name})" ng-if="type === 'job'" target="_blank">
+									{{item.name}}
+								</a>
+								<span ng-if="type !== 'job'">{{item.name}}</span>
+							</td>
+							<td title="{{item.total}}">{{common.number.abbr(item.total, true)}}</td>
+						</tr>
+						</tbody>
+					</table>
+				</div>
+			</div>
+
+			<hr/>
+
+			<div class="row">
+				<div class="col-sm-6 col-md-8 col-lg-9">
+					<div class="jpm-chart chart-lg overlay-wrapper">
+						<h3 class="text-center">Top File Bytes Written</h3>
+						<div chart class="jpm-chart-container" series="fileBytesWrittenSeries"
+							 option="commonOption"></div>
+						<div ng-if="!fileBytesWrittenSeries._done" class="overlay">
+							<i class="fa fa-refresh fa-spin"></i>
+						</div>
+					</div>
+				</div>
+				<div class="col-sm-6 col-md-4 col-lg-3">
+					<table class="table table-striped">
+						<thead>
+						<tr>
+							<th>Name</th>
+							<th>Total</th>
+						</tr>
+						</thead>
+						<tbody>
+						<tr ng-repeat="item in fileBytesWrittenSeriesList track by $index">
+							<td class="text-break">
+								<a ui-sref="jpmDetail({siteId: site, jobId: item.name})" ng-if="type === 'job'" target="_blank">
+									{{item.name}}
+								</a>
+								<span ng-if="type !== 'job'">{{item.name}}</span>
+							</td>
+							<td title="{{item.total}}">{{common.number.abbr(item.total, true)}}</td>
+						</tr>
+						</tbody>
+					</table>
+				</div>
+			</div>
+		</div>
+		<div class="tab-pane" id="cpu">
+			<div class="row">
+				<div class="col-sm-6 col-md-8 col-lg-9">
+					<div class="jpm-chart chart-lg overlay-wrapper">
+						<h3 class="text-center">Top CPU Usage</h3>
+						<div chart class="jpm-chart-container" series="cpuUsageSeries" option="commonOption"></div>
+						<div ng-if="!cpuUsageSeries._done" class="overlay">
+							<i class="fa fa-refresh fa-spin"></i>
+						</div>
+					</div>
+				</div>
+				<div class="col-sm-6 col-md-4 col-lg-3">
+					<table class="table table-striped">
+						<thead>
+						<tr>
+							<th>Name</th>
+							<th>Total</th>
+						</tr>
+						</thead>
+						<tbody>
+						<tr ng-repeat="item in cpuUsageSeriesList track by $index">
+							<td class="text-break">
+								<a ui-sref="jpmDetail({siteId: site, jobId: item.name})" ng-if="type === 'job'" target="_blank">
+									{{item.name}}
+								</a>
+								<span ng-if="type !== 'job'">{{item.name}}</span>
+							</td>
+							<td title="{{item.total}}">{{common.number.abbr(item.total, true)}}</td>
+						</tr>
+						</tbody>
+					</table>
+				</div>
+			</div>
+		</div>
+		<div class="tab-pane" id="memory">
+			<div class="row">
+				<div class="col-sm-6 col-md-8 col-lg-9">
+					<div class="jpm-chart chart-lg overlay-wrapper">
+						<h3 class="text-center">Top Physical Memory Usage</h3>
+						<div chart class="jpm-chart-container" series="physicalMemorySeries"
+							 option="commonOption"></div>
+						<div ng-if="!physicalMemorySeries._done" class="overlay">
+							<i class="fa fa-refresh fa-spin"></i>
+						</div>
+					</div>
+				</div>
+				<div class="col-sm-6 col-md-4 col-lg-3">
+					<table class="table table-striped">
+						<thead>
+						<tr>
+							<th>Name</th>
+							<th>Total</th>
+						</tr>
+						</thead>
+						<tbody>
+						<tr ng-repeat="item in physicalMemorySeriesList track by $index">
+							<td class="text-break">
+								<a ui-sref="jpmDetail({siteId: site, jobId: item.name})" ng-if="type === 'job'" target="_blank">
+									{{item.name}}
+								</a>
+								<span ng-if="type !== 'job'">{{item.name}}</span>
+							</td>
+							<td title="{{item.total}}">{{common.number.abbr(item.total, true)}}</td>
+						</tr>
+						</tbody>
+					</table>
+				</div>
+			</div>
+
+			<hr/>
+
+			<div class="row">
+				<div class="col-sm-6 col-md-8 col-lg-9">
+					<div class="jpm-chart chart-lg overlay-wrapper">
+						<h3 class="text-center">Top Virtual Memory Usage</h3>
+						<div chart class="jpm-chart-container" series="virtualMemorySeries" option="commonOption"></div>
+						<div ng-if="!virtualMemorySeries._done" class="overlay">
+							<i class="fa fa-refresh fa-spin"></i>
+						</div>
+					</div>
+				</div>
+				<div class="col-sm-6 col-md-4 col-lg-3">
+					<table class="table table-striped">
+						<thead>
+						<tr>
+							<th>Name</th>
+							<th>Total</th>
+						</tr>
+						</thead>
+						<tbody>
+						<tr ng-repeat="item in virtualMemorySeriesList track by $index">
+							<td class="text-break">
+								<a ui-sref="jpmDetail({siteId: site, jobId: item.name})" ng-if="type === 'job'" target="_blank">
+									{{item.name}}
+								</a>
+								<span ng-if="type !== 'job'">{{item.name}}</span>
+							</td>
+							<td title="{{item.total}}">{{common.number.abbr(item.total, true)}}</td>
+						</tr>
+						</tbody>
+					</table>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/partials/job/statistic.html
----------------------------------------------------------------------
diff --git a/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/partials/job/statistic.html b/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/partials/job/statistic.html
new file mode 100644
index 0000000..9ce721a
--- /dev/null
+++ b/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/partials/job/statistic.html
@@ -0,0 +1,120 @@
+<!--
+  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.
+  -->
+
+<div class="nav-tabs-custom">
+	<ul class="nav nav-tabs">
+		<li ng-class="{active: type === 'hourly'}"><a ng-click="switchType('hourly')">Hourly</a></li>
+		<li ng-class="{active: type === 'daily'}"><a ng-click="switchType('daily')">Daily</a></li>
+		<li ng-class="{active: type === 'weekly'}"><a ng-click="switchType('weekly')">Weekly</a></li>
+		<li ng-class="{active: type === 'monthly'}"><a ng-click="switchType('monthly')">Monthly</a></li>
+	</ul>
+	<div class="tab-content">
+		<div class="jpm-chart">
+			<h3 class="text-center">Number of Submitted Jobs</h3>
+			<div chart class="jpm-chart-container"
+				 series="jobDistributionSeries"
+				 category-func="jobDistributionCategoryFunc"
+				 option="jobDistributionSeriesOption"
+				 ng-click="distributionClick"></div>
+			<div ng-if="(jobDistributionSeries || []).length === 0" class="overlay">
+				<i class="fa fa-refresh fa-spin"></i>
+			</div>
+		</div>
+	</div>
+	<div class="box-body no-padding" ng-show="distributionSelectedIndex !== -1">
+		<div class="row border-split">
+			<div class="col-sm-12 col-md-6">
+				<div class="jpm-chart overlay-wrapper">
+					<h3 class="text-center">[{{distributionSelectedType}}] Top Job Count By User</h3>
+					<div chart class="jpm-chart-container" series="topUserJobCountSeries" category="topUserJobCountSeriesCategory" option="commonChartOption"></div>
+					<div ng-if="topUserJobCountSeries.length === 0" class="overlay">
+						<i class="fa fa-refresh fa-spin"></i>
+					</div>
+				</div>
+			</div>
+			<div class="col-sm-12 col-md-6">
+				<div class="jpm-chart overlay-wrapper">
+					<h3 class="text-center">[{{distributionSelectedType}}] Top Job Count By Type</h3>
+					<div chart class="jpm-chart-container" series="topTypeJobCountSeries" category="topTypeJobCountSeriesCategory" option="commonChartOption"></div>
+					<div ng-if="topUserJobCountSeries.length === 0" class="overlay">
+						<i class="fa fa-refresh fa-spin"></i>
+					</div>
+				</div>
+			</div>
+			<div class="col-sm-12 col-md-6">
+				<div class="jpm-chart overlay-wrapper">
+					<h3 class="text-center">[{{distributionSelectedType}}] Top Job Count Trend By User</h3>
+					<div chart class="jpm-chart-container" series="topUserJobCountTrendSeries" category-func="drillDownCategoryFunc" option="commonTrendChartOption"></div>
+					<div ng-if="topUserJobCountTrendSeries.length === 0" class="overlay">
+						<i class="fa fa-refresh fa-spin"></i>
+					</div>
+				</div>
+			</div>
+			<div class="col-sm-12 col-md-6">
+				<div class="jpm-chart overlay-wrapper">
+					<h3 class="text-center">[{{distributionSelectedType}}] Top Job Count Trend By Type</h3>
+					<div chart class="jpm-chart-container" series="topTypeJobCountTrendSeries" category-func="drillDownCategoryFunc" option="commonTrendChartOption"></div>
+					<div ng-if="topUserJobCountTrendSeries.length === 0" class="overlay">
+						<i class="fa fa-refresh fa-spin"></i>
+					</div>
+				</div>
+			</div>
+			<div class="col-sm-12 col-md-6" ng-show="!jobList">
+				<div class="jpm-chart overlay-wrapper">
+					<h3 class="text-center">[{{distributionSelectedType}}] Job Duration Distribution</h3>
+					<div chart class="jpm-chart-container" series="jobDurationDistributionSeries" category="bucketDurationCategory" option="commonChartOption"></div>
+					<div ng-if="jobDurationDistributionSeries.length === 0" class="overlay">
+						<i class="fa fa-refresh fa-spin"></i>
+					</div>
+				</div>
+			</div>
+			<div class="col-sm-12 col-md-12" ng-show="jobList">
+				<div class="overlay-wrapper">
+					<div sort-table="jobList" style="margin-top: 10px;">
+						<table class="table table-bordered table-striped">
+							<thead>
+								<tr>
+									<th>Job Id</th>
+									<th>Job Name</th>
+									<th>Type</th>
+									<th>User</th>
+									<th width="135">Start Time</th>
+								</tr>
+							</thead>
+							<tbody>
+								<tr>
+									<td>
+										<a ui-sref="jpmDetail({siteId: site, jobId: item.tags.jobId})" target="_blank">{{item.tags.jobId}}</a>
+									</td>
+									<td>{{item.tags.jobName}}</td>
+									<td>{{item.tags.jobType}}</td>
+									<td>{{item.tags.user}}</td>
+									<td>{{Time.format(item.startTime)}}</td>
+								</tr>
+							</tbody>
+						</table>
+					</div>
+
+					<div ng-if="!jobList._done" class="overlay">
+						<i class="fa fa-refresh fa-spin"></i>
+					</div>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/partials/job/task.html
----------------------------------------------------------------------
diff --git a/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/partials/job/task.html b/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/partials/job/task.html
new file mode 100644
index 0000000..9460db6
--- /dev/null
+++ b/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/partials/job/task.html
@@ -0,0 +1,149 @@
+<!--
+  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.
+  -->
+
+<div class="box box-primary">
+	<div class="box-header with-border">
+		<h3 class="box-title">
+			Task Schedule Trend
+		</h3>
+	</div>
+	<div class="box-body">
+		<div class="jpm-chart">
+			<div chart class="jpm-chart-container" series="scheduleSeries" category="scheduleCategory"></div>
+			<div ng-if="(scheduleSeries || []).length === 0" class="overlay">
+				<i class="fa fa-refresh fa-spin"></i>
+			</div>
+		</div>
+	</div>
+</div>
+
+<div class="nav-tabs-custom">
+	<ul class="nav nav-tabs">
+		<li class="active"><a href="#scheduleDistribution" data-toggle="tab">Schedule Distribution</a></li>
+		<li><a href="#durationDistribution" data-toggle="tab">Duration Distribution</a></li>
+	</ul>
+	<div class="tab-content keepContent">
+		<!-- By Schedule Distribution -->
+		<div class="tab-pane fade in active" id="scheduleDistribution">
+			<div class="row">
+				<div class="col-sm-12 col-md-6">
+					<div class="jpm-chart">
+						<div chart class="jpm-chart-container" series="statusSeries" category="bucketScheduleCategory" option="statusOption"></div>
+						<div ng-if="(statusSeries || []).length === 0" class="overlay">
+							<i class="fa fa-refresh fa-spin"></i>
+						</div>
+					</div>
+				</div>
+				<div class="col-sm-12 col-md-6">
+					<div class="jpm-chart">
+						<div chart class="jpm-chart-container" series="durationSeries" category="bucketScheduleCategory" option="durationOption"></div>
+						<div ng-if="(durationSeries || []).length === 0" class="overlay">
+							<i class="fa fa-refresh fa-spin"></i>
+						</div>
+					</div>
+				</div>
+				<div class="col-sm-12 col-md-6">
+					<div class="jpm-chart">
+						<div chart class="jpm-chart-container" series="hdfsReadSeries" category="bucketScheduleCategory" option="hdfsReadOption"></div>
+						<div ng-if="(hdfsReadSeries || []).length === 0" class="overlay">
+							<i class="fa fa-refresh fa-spin"></i>
+						</div>
+					</div>
+				</div>
+				<div class="col-sm-12 col-md-6">
+					<div class="jpm-chart">
+						<div chart class="jpm-chart-container" series="hdfsWriteSeries" category="bucketScheduleCategory" option="hdfsWriteOption"></div>
+						<div ng-if="(hdfsWriteSeries || []).length === 0" class="overlay">
+							<i class="fa fa-refresh fa-spin"></i>
+						</div>
+					</div>
+				</div>
+				<div class="col-sm-12 col-md-6">
+					<div class="jpm-chart">
+						<div chart class="jpm-chart-container" series="localReadSeries" category="bucketScheduleCategory" option="localReadOption"></div>
+						<div ng-if="(localReadSeries || []).length === 0" class="overlay">
+							<i class="fa fa-refresh fa-spin"></i>
+						</div>
+					</div>
+				</div>
+				<div class="col-sm-12 col-md-6">
+					<div class="jpm-chart">
+						<div chart class="jpm-chart-container" series="localWriteSeries" category="bucketScheduleCategory" option="localWriteOption"></div>
+						<div ng-if="(localWriteSeries || []).length === 0" class="overlay">
+							<i class="fa fa-refresh fa-spin"></i>
+						</div>
+					</div>
+				</div>
+			</div>
+		</div>
+
+		<!-- By Duration Distribution -->
+		<div class="tab-pane fade" id="durationDistribution">
+			<div class="row">
+				<div class="col-sm-12 col-md-6">
+					<div class="jpm-chart">
+						<div chart class="jpm-chart-container" series="durationStatusSeries" category="bucketDurationCategory" option="durationStatusOption"></div>
+						<div ng-if="(durationStatusSeries || []).length === 0" class="overlay">
+							<i class="fa fa-refresh fa-spin"></i>
+						</div>
+					</div>
+				</div>
+				<div class="col-sm-12 col-md-6">
+					<div class="jpm-chart">
+						<div chart class="jpm-chart-container" series="durationMapReduceSeries" category="bucketDurationCategory" option="durationMapReduceOption"></div>
+						<div ng-if="(durationMapReduceSeries || []).length === 0" class="overlay">
+							<i class="fa fa-refresh fa-spin"></i>
+						</div>
+					</div>
+				</div>
+				<div class="col-sm-12 col-md-6">
+					<div class="jpm-chart">
+						<div chart class="jpm-chart-container" series="durationHdfsReadSeries" category="bucketDurationCategory" option="durationHdfsReadOption"></div>
+						<div ng-if="(durationHdfsReadSeries || []).length === 0" class="overlay">
+							<i class="fa fa-refresh fa-spin"></i>
+						</div>
+					</div>
+				</div>
+				<div class="col-sm-12 col-md-6">
+					<div class="jpm-chart">
+						<div chart class="jpm-chart-container" series="durationHdfsWriteSeries" category="bucketDurationCategory" option="durationHdfsWriteOption"></div>
+						<div ng-if="(durationHdfsWriteSeries || []).length === 0" class="overlay">
+							<i class="fa fa-refresh fa-spin"></i>
+						</div>
+					</div>
+				</div>
+				<div class="col-sm-12 col-md-6">
+					<div class="jpm-chart">
+						<div chart class="jpm-chart-container" series="durationLocalReadSeries" category="bucketDurationCategory" option="durationLocalReadOption"></div>
+						<div ng-if="(durationLocalReadSeries || []).length === 0" class="overlay">
+							<i class="fa fa-refresh fa-spin"></i>
+						</div>
+					</div>
+				</div>
+				<div class="col-sm-12 col-md-6">
+					<div class="jpm-chart">
+						<div chart class="jpm-chart-container" series="durationLocalWriteSeries" category="bucketDurationCategory" option="durationLocalWriteOption"></div>
+						<div ng-if="(durationLocalWriteSeries || []).length === 0" class="overlay">
+							<i class="fa fa-refresh fa-spin"></i>
+						</div>
+					</div>
+				</div>
+			</div>
+		</div>
+	</div>
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/style/index.css
----------------------------------------------------------------------
diff --git a/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/style/index.css b/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/style/index.css
new file mode 100644
index 0000000..fbe238f
--- /dev/null
+++ b/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/style/index.css
@@ -0,0 +1,76 @@
+@CHARSET "UTF-8";
+/*
+ * 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.
+ */
+
+.jpm-chart {
+	position: relative;
+	margin-bottom: 15px;
+}
+
+.jpm-chart h3 {
+	margin: 10px 0 10px 0;
+}
+
+.jpm-chart .jpm-chart-container {
+	height: 300px;
+	position: relative;
+}
+
+.jpm-chart .jpm-chart-container.scroll {
+	overflow-y: auto;
+}
+
+.jpm-chart.chart-lg .jpm-chart-container {
+	height: 350px;
+}
+
+.with-border .jpm-chart {
+	padding-bottom: 15px;
+	margin-bottom: 15px;
+	border-bottom: 1px solid #f4f4f4;
+}
+
+.with-border .jpm-chart:last-child {
+	padding-bottom: 0;
+	margin-bottom: 0;
+	border-bottom: 0;
+}
+
+.jpm-chart .overlay {
+	top: 0;
+	bottom: 0;
+	position: absolute;
+	width: 100%;
+	background: rgba(255,255,255,0.7);
+}
+
+.jpm-chart .overlay > .fa {
+	position: absolute;
+	top: 50%;
+	left: 50%;
+	margin-left: -15px;
+	margin-top: -15px;
+	color: #000;
+	font-size: 30px;
+}
+
+.small-box.jpm {
+	margin: 0;
+	height: 100%;
+	min-height: 110px;
+}

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/widget/jobStatistic.js
----------------------------------------------------------------------
diff --git a/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/widget/jobStatistic.js b/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/widget/jobStatistic.js
new file mode 100644
index 0000000..1572d5e
--- /dev/null
+++ b/eagle-jpm/eagle-jpm-web/src/main/webapp/app/apps/jpm/widget/jobStatistic.js
@@ -0,0 +1,108 @@
+/*
+ * 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 () {
+	/**
+	 * `register` without params will load the module which using require
+	 */
+	register(function (jpmApp) {
+		jpmApp.directive("jpmWidget", function () {
+			return {
+				restrict: 'AE',
+				controller: function($scope, $interval, Application, JPM, Time) {
+					var site = $scope.site;
+					var refreshInterval;
+
+					if(!site) {
+						$scope.list = $.map(Application.find("JPM_WEB_APP"), function (app) {
+							return {
+								siteId: app.site.siteId,
+								siteName: app.site.siteName || app.site.siteId,
+								count: -1
+							};
+						});
+					} else {
+						$scope.list = [{
+							siteId: site.siteId,
+							siteName: site.siteName || site.siteId,
+							count: -1
+						}];
+					}
+
+					function refresh() {
+						$.each($scope.list, function (i, site) {
+							var query = JPM.getQuery("GROUPS", site.siteId);
+							var url = common.template(query, {
+								query: "RunningJobExecutionService",
+								condition: '@site="' + site.siteId + '" AND @internalState="RUNNING"',
+								groups: "@site",
+								field: "count",
+								order: "",
+								top: "",
+								limit: 100000,
+								startTime: Time.format(Time().subtract(3, "d")),
+								endTime: Time.format(Time().add(1, "d"))
+							});
+							JPM.get(url).then(function (res) {
+								site.count = common.getValueByPath(res, ["data", "obj", 0, "value", 0]);
+							});
+						});
+					}
+
+					refresh();
+					refreshInterval = $interval(refresh, 30 * 1000);
+
+					$scope.$on('$destroy', function() {
+						$interval.cancel(refreshInterval);
+					});
+				},
+				template:
+				'<div class="small-box bg-aqua jpm">' +
+					'<div class="inner">' +
+						'<h3>JPM</h3>' +
+						'<p ng-repeat="site in list track by $index">' +
+							'<a ui-sref="jpmList({siteId: site.siteId})">' +
+								'<strong>{{site.siteName}}</strong>: ' +
+								'<span ng-show="site.count === -1" class="fa fa-refresh fa-spin no-animate"></span>' +
+								'<span ng-show="site.count !== -1">{{site.count}}</span> Running Jobs' +
+							'</a>' +
+						'</p>' +
+					'</div>' +
+					'<div class="icon">' +
+						'<i class="fa fa-taxi"></i>' +
+					'</div>' +
+				'</div>',
+				replace: true
+			};
+		});
+
+		/**
+		 * Customize the widget content. Return false will prevent auto compile.
+		 * @param {{}} $element
+		 * @param {function} $element.append
+		 */
+		function registerWidget($element) {
+			$element.append(
+				$("<div jpm-widget data-site='site'>")
+			);
+		}
+
+		jpmApp.widget("jobStatistic", registerWidget);
+		jpmApp.widget("jobStatistic", registerWidget, true);
+	});
+})();

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-server/.gitignore
----------------------------------------------------------------------
diff --git a/eagle-server/.gitignore b/eagle-server/.gitignore
new file mode 100644
index 0000000..f3d085a
--- /dev/null
+++ b/eagle-server/.gitignore
@@ -0,0 +1,7 @@
+/bin/
+/target/
+/src/main/webapp/app/dev/apps
+grunt.json
+node_modules
+ui
+tmp

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-server/pom.xml
----------------------------------------------------------------------
diff --git a/eagle-server/pom.xml b/eagle-server/pom.xml
index 31b219d..13cdff6 100644
--- a/eagle-server/pom.xml
+++ b/eagle-server/pom.xml
@@ -223,9 +223,30 @@
         </profile>
     </profiles>
     <build>
+        <plugins>
+            <plugin>
+                <groupId>org.codehaus.mojo</groupId>
+                <artifactId>exec-maven-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>exec-ui-install</id>
+                        <phase>generate-sources</phase>
+                        <goals>
+                            <goal>exec</goal>
+                        </goals>
+                        <configuration>
+                            <executable>bash</executable>
+                            <arguments>
+                                <argument>${basedir}/ui-build.sh</argument>
+                            </arguments>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
         <resources>
             <resource>
-                <directory>src/main/webapp/app</directory>
+                <directory>src/main/webapp/app/ui</directory>
                 <targetPath>assets</targetPath>
             </resource>
             <resource>

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-server/src/main/webapp/app/.editorconfig
----------------------------------------------------------------------
diff --git a/eagle-server/src/main/webapp/app/.editorconfig b/eagle-server/src/main/webapp/app/.editorconfig
new file mode 100644
index 0000000..42a9b69
--- /dev/null
+++ b/eagle-server/src/main/webapp/app/.editorconfig
@@ -0,0 +1,27 @@
+# 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.
+
+root = true
+
+[*]
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
+indent_style = tab
+indent_size = 4
+
+[*.md]
+trim_trailing_whitespace = false

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-server/src/main/webapp/app/Gruntfile.js
----------------------------------------------------------------------
diff --git a/eagle-server/src/main/webapp/app/Gruntfile.js b/eagle-server/src/main/webapp/app/Gruntfile.js
new file mode 100644
index 0000000..3606d84
--- /dev/null
+++ b/eagle-server/src/main/webapp/app/Gruntfile.js
@@ -0,0 +1,190 @@
+/*
+ * 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.
+*/
+'use strict';
+
+module.exports = function (grunt) {
+	// ==========================================================
+	// =                     Parse Resource                     =
+	// ==========================================================
+	/*console.log('Generating resource tree...');
+
+	var env = require('jsdom').env;
+	var fs = require('fs');
+
+	var html = fs.readFileSync('dev/index.html', 'utf8');
+
+	console.log("1111", env);
+	env(html, function (err, window) {
+		console.log(">>>!!!");
+		if (err) console.log(err);
+
+		var $ = require('jquery')(window);
+		var $cssList = $('link[href][rel="stylesheet"]');
+		var cssList = $.map($cssList, function (ele) {
+			return $(ele).attr("href");
+		});
+
+		console.log(">>>", cssList);
+	});
+	console.log(">>>222");*/
+
+	// ==========================================================
+	// =                      Grunt Config                      =
+	// ==========================================================
+	grunt.initConfig({
+		config: grunt.file.readJSON('grunt.json'),
+
+		jshint: {
+			options: {
+				browser: true,
+				globals: {
+					$: true,
+					jQuery: true,
+					moment: true
+				}
+			},
+			all: [
+				'dev/**/*.js'
+			]
+		},
+
+		clean: {
+			build: ['ui/', 'tmp/'],
+			tmp: ['tmp/'],
+			ui: ['ui/']
+		},
+
+		copy: {
+			worker: {
+				files: [
+					{expand: true, cwd: 'dev/', src: '<%= config.copy.js.worker %>', dest: 'tmp'}
+				]
+			},
+			ui: {
+				files: [
+					{expand: true, cwd: 'tmp/', src: ['**'], dest: 'ui'},
+					{expand: true, cwd: 'dev/', src: ['public/images/**', 'partials/**'], dest: 'ui'},
+					{expand: true, cwd: 'node_modules/font-awesome/', src: ['fonts/**'], dest: 'ui/public'},
+					{expand: true, cwd: 'node_modules/bootstrap/', src: ['fonts/**'], dest: 'ui/public'}
+				]
+			}
+		},
+
+		concat: {
+			js_project: '<%= config.concat.js.project %>',
+			js_require: '<%= config.concat.js.require %>',
+			css_require: {
+				options: {
+					separator: '\n',
+					process: function(src) {
+						return "@import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,300italic,400italic,600italic);" +
+							src.replace('@import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,300italic,400italic,600italic);', '');
+					}
+				},
+				src: '<%= config.concat.css.require.src %>',
+				dest: '<%= config.concat.css.require.dest %>'
+			}
+		},
+
+		'regex-replace': {
+			strict: {
+				src: ['tmp/public/js/project.js'],
+				actions: [
+					{
+						name: 'use strict',
+						search: '\\\'use strict\\\';?',
+						replace: '',
+						flags: 'gmi'
+					},
+					{
+						name: 'build timestamp',
+						search: '\\/\\/ GRUNT REPLACEMENT\\: Module\\.buildTimestamp \\= TIMESTAMP',
+						replace: 'Module.buildTimestamp = ' + (+new Date()) + ';',
+						flags: 'gmi'
+					}
+				]
+			}
+		},
+
+		uglify: {
+			project: {
+				options: {
+					mangle: false,
+					sourceMap: true,
+					sourceMapIncludeSources: true
+				},
+				files: [
+					{
+						src: 'tmp/public/js/doc.js',
+						dest: 'tmp/public/js/doc.min.js'
+					}
+				]
+			}
+		},
+
+		cssmin: {
+			project: {
+				files: {
+					'tmp/public/css/project.min.css': '<%= config.concat.css.project.src %>',
+				}
+			}
+		},
+
+		htmlrefs: {
+			project: {
+				src: 'dev/index.html',
+				dest: "tmp/index.html"
+			}
+		},
+	});
+
+	grunt.loadNpmTasks('grunt-contrib-jshint');
+	grunt.loadNpmTasks('grunt-contrib-clean');
+	grunt.loadNpmTasks('grunt-contrib-concat');
+	grunt.loadNpmTasks('grunt-contrib-uglify');
+	grunt.loadNpmTasks('grunt-contrib-cssmin');
+	grunt.loadNpmTasks('grunt-htmlrefs');
+	grunt.loadNpmTasks('grunt-regex-replace');
+	grunt.loadNpmTasks('grunt-contrib-copy');
+
+	grunt.registerTask('default', [
+		// jshint
+		'jshint:all',
+
+		// Clean Env
+		'clean:build',
+
+		// Compress JS
+		'concat:js_require',
+		'copy:worker',
+		'concat:js_project',
+		'regex-replace:strict',
+		'uglify',
+
+		// Compress CSS
+		'cssmin:project',
+		'concat:css_require',
+
+		// Pass HTML Resources
+		'htmlrefs',
+		'copy:ui',
+
+		// Clean Env
+		'clean:tmp'
+	]);
+};

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-server/src/main/webapp/app/README.md
----------------------------------------------------------------------
diff --git a/eagle-server/src/main/webapp/app/README.md b/eagle-server/src/main/webapp/app/README.md
new file mode 100644
index 0000000..b4168d5
--- /dev/null
+++ b/eagle-server/src/main/webapp/app/README.md
@@ -0,0 +1,4 @@
+Apache Eagle Web APP
+==
+
+Web client for Apache Eagle
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-server/src/main/webapp/app/build/index.js
----------------------------------------------------------------------
diff --git a/eagle-server/src/main/webapp/app/build/index.js b/eagle-server/src/main/webapp/app/build/index.js
new file mode 100644
index 0000000..bacbf53
--- /dev/null
+++ b/eagle-server/src/main/webapp/app/build/index.js
@@ -0,0 +1,144 @@
+/*
+ * 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';
+	console.log('Generating resource tree...');
+
+	var env = require('jsdom').env;
+	var fs = require('fs');
+
+	// Parse tree
+	fs.readFile('dev/index.html', 'utf8', function (err, html) {
+		if (err) return console.log(err);
+
+		env(html, function (err, window) {
+			if (err) console.log(err);
+
+			// Get js / css resource
+			var $ = require('jquery')(window);
+			function getResList(match, attr) {
+				var $eleList = $(match);
+				var requireList = [];
+				var projectList = [];
+				var list = [];
+
+				$.each($eleList, function (i, ele) {
+					var path = $(ele).attr(attr);
+
+					if(path.match(/^apps/)) return;
+
+					if(path.match(/node_modules/)) {
+						requireList.push(path.replace(/\.\.\//, ""));
+						list.push(path.replace(/\.\.\//, ""));
+					} else {
+						projectList.push("dev/" + path);
+						list.push("dev/" + path);
+					}
+				});
+
+				return {
+					list: list,
+					requireList: requireList,
+					projectList: projectList
+				};
+			}
+
+			var cssList = getResList('link[href][rel="stylesheet"]', 'href');
+			var jsList = getResList('script[src]', 'src');
+
+			// JS Worker process
+			var workerFolderPath = 'dev/public/js/worker/';
+			var workerList = fs.readdirSync(workerFolderPath);
+			var workerRequireList = [];
+
+			workerList = workerList.map(function (path) {
+				if(!/\w+Worker\.js/.test(path)) return;
+
+				var workerPath = workerFolderPath + path;
+				var content = fs.readFileSync(workerPath, 'utf8');
+				var regex = /self\.importScripts\(["']([^"']*)["']\)/g;
+				var match;
+				while ((match = regex.exec(content)) !== null) {
+					var modulePath = match[1];
+					workerRequireList.push((workerFolderPath + modulePath).replace(/^dev\//, ""));
+				}
+
+				return workerPath.replace(/^dev\//, "");
+			}).filter(function (path) {
+				return !!path;
+			});
+
+			// Parse grunt config
+			var resJson = {
+				concat: {
+					js: {
+						require: {
+							options: {
+								separator: '\n'
+							},
+							src: jsList.requireList,
+							dest: 'tmp/public/js/modules.js'
+						},
+						project: {
+							options: {
+								separator: '\n',
+								sourceMap :true
+							},
+							src: jsList.projectList,
+							dest: 'tmp/public/js/doc.js'
+						}
+					},
+					css: {
+						require: {
+							src: cssList.requireList.concat('tmp/public/css/project.min.css'),
+							dest: 'tmp/public/css/doc.css'
+						},
+						project: {
+							options: {
+								separator: '\n'
+							},
+							src: cssList.projectList,
+							dest: 'tmp/public/js/project.min.css'
+						}
+					}
+				},
+				copy: {
+					js: {
+						worker: workerList.concat(workerRequireList)
+					}
+				}
+			};
+
+			// Save tree & call grunt
+			fs.writeFile('grunt.json', JSON.stringify(resJson, null, '\t'), 'utf8', function (err) {
+				if(err) return console.log(err);
+
+				console.log("Grunt packaging...");
+				var exec = require('child_process').exec;
+				var grunt = exec('npm run grunt');
+
+				grunt.stdout.pipe(process.stdout);
+				grunt.stderr.pipe(process.stdout);
+				grunt.on('exit', function() {
+					process.exit()
+				})
+			});
+		});
+	});
+})();