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()
+ })
+ });
+ });
+ });
+})();