eagle-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ji...@apache.org
Subject [08/14] incubator-eagle git commit: [EAGLE-574] UI refactor for support 0.5 api
Date Wed, 28 Sep 2016 05:38:49 GMT
http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/feature/metrics/controller.js
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/_app/public/feature/metrics/controller.js b/eagle-webservice/src/main/webapp/_app/public/feature/metrics/controller.js
new file mode 100644
index 0000000..d717ad1
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/_app/public/feature/metrics/controller.js
@@ -0,0 +1,571 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+(function() {
+	'use strict';
+
+	var featureControllers = angular.module('featureControllers');
+	var feature = featureControllers.register("metrics");
+
+	// ==============================================================
+	// =                       Initialization                       =
+	// ==============================================================
+
+	// ==============================================================
+	// =                          Function                          =
+	// ==============================================================
+	// Format dashboard unit. Will adjust format with old version and add miss attributes.
+	feature.service("DashboardFormatter", function() {
+		return {
+			parse: function(unit) {
+				unit = unit || {};
+				unit.groups = unit.groups || [];
+
+				$.each(unit.groups, function (i, group) {
+					group.charts = group.charts || [];
+					$.each(group.charts, function (i, chart) {
+						if (!chart.metrics && chart.metric) {
+							chart.metrics = [{
+								aggregations: chart.aggregations,
+								dataSource: chart.dataSource,
+								metric: chart.metric
+							}];
+
+							delete chart.aggregations;
+							delete chart.dataSource;
+							delete chart.metric;
+						} else if (!chart.metrics) {
+							chart.metrics = [];
+						}
+					});
+				});
+
+				return unit;
+			}
+		};
+	});
+
+	// ==============================================================
+	// =                         Controller                         =
+	// ==============================================================
+
+	// ========================= Dashboard ==========================
+	feature.navItem("dashboard", "Metrics", "line-chart");
+
+	feature.controller('dashboard', function(PageConfig, $scope, $http, $q, UI, Site, Authorization, Application, Entities, DashboardFormatter) {
+		var _siteApp = Site.currentSiteApplication();
+		var _druidConfig = _siteApp.configObj.getValueByPath("web.druid");
+		var _refreshInterval;
+
+		var _menu_newChart;
+
+		$scope.lock = false;
+
+		$scope.dataSourceListReady = false;
+		$scope.dataSourceList = [];
+		$scope.dashboard = {
+			groups: []
+		};
+		$scope.dashboardEntity = null;
+		$scope.dashboardReady = false;
+
+		$scope._newMetricFilter = "";
+		$scope._newMetricDataSrc = null;
+		$scope._newMetricDataMetric = null;
+
+		$scope.tabHolder = {};
+
+		$scope.endTime = app.time.now();
+		$scope.startTime = $scope.endTime.clone();
+
+		// =================== Initialization ===================
+		if(!_druidConfig || !_druidConfig.coordinator || !_druidConfig.broker) {
+			$.dialog({
+				title: "OPS",
+				content: "Druid configuration can't be empty!"
+			});
+			return;
+		}
+
+		$scope.autoRefreshList = [
+			{title: "Last 1 Month", timeDes: "day", getStartTime: function(endTime) {return endTime.clone().subtract(1, "month");}},
+			{title: "Last 1 Day", timeDes: "thirty_minute", getStartTime: function(endTime) {return endTime.clone().subtract(1, "day");}},
+			{title: "Last 6 Hour", timeDes: "fifteen_minute", getStartTime: function(endTime) {return endTime.clone().subtract(6, "hour");}},
+			{title: "Last 2 Hour", timeDes: "fifteen_minute", getStartTime: function(endTime) {return endTime.clone().subtract(2, "hour");}},
+			{title: "Last 1 Hour", timeDes: "minute", getStartTime: function(endTime) {return endTime.clone().subtract(1, "hour");}}
+		];
+		$scope.autoRefreshSelect = $scope.autoRefreshList[2];
+
+		// ====================== Function ======================
+		$scope.setAuthRefresh = function(item) {
+			$scope.autoRefreshSelect = item;
+			$scope.refreshAllChart(true);
+		};
+
+		$scope.refreshTimeDisplay = function() {
+			PageConfig.pageSubTitle = common.format.date($scope.startTime) + " ~ " + common.format.date($scope.endTime) + " [refresh interval: 30s]";
+		};
+		$scope.refreshTimeDisplay();
+
+		// ======================= Metric =======================
+		// Fetch metric data
+		$http.get(_druidConfig.coordinator + "/druid/coordinator/v1/metadata/datasources", {withCredentials: false}).then(function(data) {
+			var _endTime = new moment();
+			var _startTime = _endTime.clone().subtract(1, "day");
+			var _intervals = _startTime.toISOString() + "/" + _endTime.toISOString();
+
+			$scope.dataSourceList = $.map(data.data, function(dataSrc) {
+				return {
+					dataSource: dataSrc,
+					metricList: []
+				};
+			});
+
+			// List dataSource metrics
+			var _metrixList_promiseList = $.map($scope.dataSourceList, function(dataSrc) {
+				var _data = JSON.stringify({
+					"queryType": "groupBy",
+					"dataSource": dataSrc.dataSource,
+					"granularity": "all",
+					"dimensions": ["metric"],
+					"aggregations": [
+						{
+							"type":"count",
+							"name":"count"
+						}
+					],
+					"intervals": [_intervals]
+				});
+
+				return $http.post(_druidConfig.broker + "/druid/v2", _data, {withCredentials: false}).then(function(response) {
+					dataSrc.metricList = $.map(response.data, function(entity) {
+						return entity.event.metric;
+					});
+				});
+			});
+
+			$q.all(_metrixList_promiseList).finally(function() {
+				$scope.dataSourceListReady = true;
+
+				$scope._newMetricDataSrc = $scope.dataSourceList[0];
+				$scope._newMetricDataMetric = common.getValueByPath($scope._newMetricDataSrc, "metricList.0");
+			});
+		}, function() {
+			$.dialog({
+				title: "OPS",
+				content: "Fetch data source failed. Please check Site Application Metrics configuration."
+			});
+		});
+
+		// Filter data source
+		$scope.dataSourceMetricList = function(dataSrc, filter) {
+			filter = (filter || "").toLowerCase().trim().split(/\s+/);
+			return $.grep((dataSrc && dataSrc.metricList) || [], function(metric) {
+				for(var i = 0 ; i < filter.length ; i += 1) {
+					if(metric.toLowerCase().indexOf(filter[i]) === -1) return false;
+				}
+				return true;
+			});
+		};
+
+		// New metric select
+		$scope.newMetricSelectDataSource = function(dataSrc) {
+			if(dataSrc !== $scope._newMetricDataMetric) $scope._newMetricDataMetric = dataSrc.metricList[0];
+			$scope._newMetricDataSrc = dataSrc;
+		};
+		$scope.newMetricSelectMetric = function(metric) {
+			$scope._newMetricDataMetric = metric;
+		};
+
+		// Confirm new metric
+		$scope.confirmSelectMetric = function() {
+			var group = $scope.tabHolder.selectedPane.data;
+			var metric = {
+				dataSource: $scope._newMetricDataSrc.dataSource,
+				metric: $scope._newMetricDataMetric,
+				aggregations: ["max"]
+			};
+			$("#metricMDL").modal('hide');
+
+			if($scope.metricForConfigChart) {
+				$scope.configPreviewChart.metrics.push(metric);
+				$scope.refreshChart($scope.configPreviewChart, true, true);
+			} else {
+				group.charts.push({
+					chart: "line",
+					metrics: [metric]
+				});
+				$scope.refreshAllChart();
+			}
+		};
+
+		// ======================== Menu ========================
+		function _checkGroupName(entity) {
+			if(common.array.find(entity.name, $scope.dashboard.groups, "name")) {
+				return "Group name conflict";
+			}
+		}
+
+		$scope.newGroup = function() {
+			if($scope.lock) return;
+
+			UI.createConfirm("Group", {}, [{field: "name"}], _checkGroupName).then(null, null, function(holder) {
+				$scope.dashboard.groups.push({
+					name: holder.entity.name,
+					charts: []
+				});
+				holder.closeFunc();
+
+				setTimeout(function() {
+					$scope.tabHolder.setSelect(holder.entity.name);
+				}, 0);
+			});
+		};
+
+		function renameGroup() {
+			var group = $scope.tabHolder.selectedPane.data;
+			UI.updateConfirm("Group", {}, [{field: "name", name: "New Name"}], _checkGroupName).then(null, null, function(holder) {
+				group.name = holder.entity.name;
+				holder.closeFunc();
+			});
+		}
+
+		function deleteGroup() {
+			var group = $scope.tabHolder.selectedPane.data;
+			UI.deleteConfirm(group.name).then(null, null, function(holder) {
+				common.array.remove(group, $scope.dashboard.groups);
+				holder.closeFunc();
+			});
+		}
+
+		_menu_newChart = {title: "Add Metric", func: function() {$scope.newChart();}};
+		Object.defineProperties(_menu_newChart, {
+			icon: {
+				get: function() {return $scope.dataSourceListReady ? 'plus' : 'refresh fa-spin';}
+			},
+			disabled: {
+				get: function() {return !$scope.dataSourceListReady;}
+			}
+		});
+
+		$scope.menu = Authorization.isRole('ROLE_ADMIN') ? [
+			{icon: "cog", title: "Configuration", list: [
+				_menu_newChart,
+				{icon: "pencil", title: "Rename Group", func: renameGroup},
+				{icon: "trash", title: "Delete Group", danger: true, func: deleteGroup}
+			]},
+			{icon: "plus", title: "New Group", func: $scope.newGroup}
+		] : [];
+
+		// ===================== Dashboard ======================
+		$scope.dashboardList = Entities.queryEntities("GenericResourceService", {
+			site: Site.current().tags.site,
+			application: Application.current().tags.application
+		});
+		$scope.dashboardList._promise.then(function(list) {
+			$scope.dashboardEntity = list[0];
+			$scope.dashboard = DashboardFormatter.parse(common.parseJSON($scope.dashboardEntity.value));
+			$scope.refreshAllChart();
+		}).finally(function() {
+			$scope.dashboardReady = true;
+		});
+
+		$scope.saveDashboard = function() {
+			$scope.lock = true;
+
+			if(!$scope.dashboardEntity) {
+				$scope.dashboardEntity = {
+					tags: {
+						site: Site.current().tags.site,
+						application: Application.current().tags.application,
+						name: "/metric_dashboard/dashboard/default"
+					}
+				};
+			}
+			$scope.dashboardEntity.value = common.stringify($scope.dashboard);
+
+			Entities.updateEntity("GenericResourceService", $scope.dashboardEntity)._promise.then(function() {
+				$.dialog({
+					title: "Done",
+					content: "Save success!"
+				});
+			}, function() {
+				$.dialog({
+					title: "POS",
+					content: "Save failed. Please retry."
+				});
+			}).finally(function() {
+				$scope.lock = false;
+			});
+		};
+
+		// ======================= Chart ========================
+		$scope.configTargetChart = null;
+		$scope.configPreviewChart = null;
+		$scope.metricForConfigChart = false;
+		$scope.viewChart = null;
+
+		$scope.chartConfig = {
+			xType: "time"
+		};
+
+		$scope.chartTypeList = [
+			{icon: "line-chart", chart: "line"},
+			{icon: "area-chart", chart: "area"},
+			{icon: "bar-chart", chart: "column"},
+			{icon: "pie-chart", chart: "pie"}
+		];
+
+		$scope.chartSeriesList = [
+			{name: "Min", series: "min"},
+			{name: "Max", series: "max"},
+			{name: "Avg", series: "avg"},
+			{name: "Count", series: "count"},
+			{name: "Sum", series: "sum"}
+		];
+
+		$scope.newChart = function() {
+			$scope.metricForConfigChart = false;
+			$("#metricMDL").modal();
+		};
+
+		$scope.configPreviewChartMinimumCheck = function() {
+			$scope.configPreviewChart.min = $scope.configPreviewChart.min === 0 ? undefined : 0;
+		};
+
+		$scope.seriesChecked = function(metric, series) {
+			if(!metric) return false;
+			return $.inArray(series, metric.aggregations || []) !== -1;
+		};
+		$scope.seriesCheckClick = function(metric, series, chart) {
+			if(!metric || !chart) return;
+			if($scope.seriesChecked(metric, series)) {
+				common.array.remove(series, metric.aggregations);
+			} else {
+				metric.aggregations.push(series);
+			}
+			$scope.chartSeriesUpdate(chart);
+		};
+
+		$scope.chartSeriesUpdate = function(chart) {
+			chart._data = $.map(chart._oriData, function(groupData, i) {
+				var metric = chart.metrics[i];
+				return $.map(groupData, function(series) {
+					if($.inArray(series._key, metric.aggregations) !== -1) return series;
+				});
+			});
+		};
+
+		$scope.configAddMetric = function() {
+			$scope.metricForConfigChart = true;
+			$("#metricMDL").modal();
+		};
+
+		$scope.configRemoveMetric = function(metric) {
+			common.array.remove(metric, $scope.configPreviewChart.metrics);
+		};
+
+		$scope.getChartConfig = function(chart) {
+			if(!chart) return null;
+
+			var _config = chart._config = chart._config || $.extend({}, $scope.chartConfig);
+			_config.yMin = chart.min;
+
+			return _config;
+		};
+
+		$scope.configChart = function(chart) {
+			$scope.configTargetChart = chart;
+			$scope.configPreviewChart = $.extend({}, chart);
+			$scope.configPreviewChart.metrics = $.map(chart.metrics, function(metric) {
+				return $.extend({}, metric, {aggregations: (metric.aggregations || []).slice()});
+			});
+			delete $scope.configPreviewChart._config;
+			$("#chartMDL").modal();
+			setTimeout(function() {
+				$(window).resize();
+			}, 200);
+		};
+
+		$scope.confirmUpdateChart = function() {
+			$("#chartMDL").modal('hide');
+			$.extend($scope.configTargetChart, $scope.configPreviewChart);
+			$scope.chartSeriesUpdate($scope.configTargetChart);
+			if($scope.configTargetChart._holder) $scope.configTargetChart._holder.refreshAll();
+			$scope.configPreviewChart = null;
+		};
+
+		$scope.deleteChart = function(group, chart) {
+			UI.deleteConfirm(chart.metric).then(null, null, function(holder) {
+				common.array.remove(chart, group.charts);
+				holder.closeFunc();
+				$scope.refreshAllChart(false, true);
+			});
+		};
+
+		$scope.showChart = function(chart) {
+			$scope.viewChart = chart;
+			$("#chartViewMDL").modal();
+			setTimeout(function() {
+				$(window).resize();
+			}, 200);
+		};
+
+		$scope.refreshChart = function(chart, forceRefresh, refreshAll) {
+			var _intervals = $scope.startTime.toISOString() + "/" + $scope.endTime.toISOString();
+
+			function _refreshChart() {
+				if (chart._holder) {
+					if (refreshAll) {
+						chart._holder.refreshAll();
+					} else {
+						chart._holder.refresh();
+					}
+				}
+			}
+
+			var _tmpData, _metricPromiseList;
+
+			if (chart._data && !forceRefresh) {
+				// Refresh chart without reload
+				_refreshChart();
+			} else {
+				// Refresh chart with reload
+				_tmpData = [];
+				_metricPromiseList = $.map(chart.metrics, function (metric, k) {
+					// Each Metric
+					var _query = JSON.stringify({
+						"queryType": "groupBy",
+						"dataSource": metric.dataSource,
+						"granularity": $scope.autoRefreshSelect.timeDes,
+						"dimensions": ["metric"],
+						"filter": {"type": "selector", "dimension": "metric", "value": metric.metric},
+						"aggregations": [
+							{
+								"type": "max",
+								"name": "max",
+								"fieldName": "maxValue"
+							},
+							{
+								"type": "min",
+								"name": "min",
+								"fieldName": "maxValue"
+							},
+							{
+								"type": "count",
+								"name": "count",
+								"fieldName": "maxValue"
+							},
+							{
+								"type": "longSum",
+								"name": "sum",
+								"fieldName": "maxValue"
+							}
+						],
+						"postAggregations" : [
+							{
+								"type": "javascript",
+								"name": "avg",
+								"fieldNames": ["sum", "count"],
+								"function": "function(sum, cnt) { return sum / cnt;}"
+							}
+						],
+						"intervals": [_intervals]
+					});
+
+					return $http.post(_druidConfig.broker + "/druid/v2", _query, {withCredentials: false}).then(function (response) {
+						var _data = nvd3.convert.druid([response.data]);
+						_tmpData[k] = _data;
+
+						// Process series name
+						$.each(_data, function(i, series) {
+							series._key = series.key;
+							if(chart.metrics.length > 1) {
+								series.key = metric.metric.replace(/^.*\./, "") + "-" +series._key;
+							}
+						});
+					});
+				});
+
+				$q.all(_metricPromiseList).then(function() {
+					chart._oriData = _tmpData;
+					$scope.chartSeriesUpdate(chart);
+					_refreshChart();
+				});
+			}
+		};
+
+		$scope.refreshAllChart = function(forceRefresh, refreshAll) {
+			setTimeout(function() {
+				$scope.endTime = app.time.now();
+				$scope.startTime = $scope.autoRefreshSelect.getStartTime($scope.endTime);
+
+				$scope.refreshTimeDisplay();
+
+				$.each($scope.dashboard.groups, function (i, group) {
+					$.each(group.charts, function (j, chart) {
+						$scope.refreshChart(chart, forceRefresh, refreshAll);
+					});
+				});
+
+				$(window).resize();
+			}, 0);
+		};
+
+		$scope.chartSwitchRefresh = function(source, target) {
+			var _oriSize = source.size;
+			source.size = target.size;
+			target.size = _oriSize;
+
+			if(source._holder) source._holder.refreshAll();
+			if(target._holder) target._holder.refreshAll();
+
+		};
+
+		_refreshInterval = setInterval(function() {
+			if(!$scope.dashboardReady) return;
+			$scope.refreshAllChart(true);
+		}, 1000 * 30);
+
+		// > Chart UI
+		$scope.configChartSize = function(chart, sizeOffset) {
+			chart.size = (chart.size || 6) + sizeOffset;
+			if(chart.size <= 0) chart.size = 1;
+			if(chart.size > 12) chart.size = 12;
+			setTimeout(function() {
+				$(window).resize();
+			}, 1);
+		};
+
+		// ========================= UI =========================
+		$("#metricMDL").on('hidden.bs.modal', function () {
+			if($(".modal-backdrop").length) {
+				$("body").addClass("modal-open");
+			}
+		});
+
+		$("#chartViewMDL").on('hidden.bs.modal', function () {
+			$scope.viewChart = null;
+		});
+
+		// ====================== Clean Up ======================
+		$scope.$on('$destroy', function() {
+			clearInterval(_refreshInterval);
+		});
+	});
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/feature/metrics/page/dashboard.html
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/_app/public/feature/metrics/page/dashboard.html b/eagle-webservice/src/main/webapp/_app/public/feature/metrics/page/dashboard.html
new file mode 100644
index 0000000..2acb954
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/_app/public/feature/metrics/page/dashboard.html
@@ -0,0 +1,250 @@
+<!--
+  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="page-fixed">
+	<div class="dropdown inline">
+		<button data-toggle="dropdown" class="btn btn-primary">
+			<span class="fa fa-clock-o"></span> {{autoRefreshSelect.title}}
+		</button>
+		<ul class="dropdown-menu left">
+			<li ng-repeat="item in autoRefreshList track by $index">
+				<a ng-click="setAuthRefresh(item)">
+					<span class="fa fa-clock-o"></span>
+					<span ng-class="{'text-bold': item === autoRefreshSelect}">{{item.title}}</span>
+				</a>
+			</li>
+		</ul>
+	</div>
+
+	<button class="btn btn-primary" ng-if="Auth.isRole('ROLE_ADMIN')"
+			ng-click="saveDashboard()" ng-disabled="lock">
+		<span class="fa fa-floppy-o"></span> Save
+	</button>
+</div>
+
+<div class="box box-default" ng-if="!dashboard.groups.length">
+	<div class="box-header with-border">
+		<h3 class="box-title">Empty Dashboard</h3>
+	</div>
+	<div class="box-body">
+		<div ng-show="!dashboardReady">
+			Loading...
+		</div>
+		<div ng-show="dashboardReady">
+			Current dashboard is empty.
+			<span ng-if="Auth.isRole('ROLE_ADMIN')">Click <a ng-click="newGroup()">here</a> to create a new group.</span>
+		</div>
+	</div>
+	<div class="overlay" ng-show="!dashboardReady">
+		<i class="fa fa-refresh fa-spin"></i>
+	</div>
+</div>
+
+<div tabs menu="menu" holder="tabHolder" ng-show="dashboard.groups.length" data-sortable-model="Auth.isRole('ROLE_ADMIN') ? dashboard.groups : null">
+	<pane ng-repeat="group in dashboard.groups" data-data="group" data-title="{{group.name}}">
+		<div uie-sortable ng-model="group.charts" class="row narrow" sortable-update-func="chartSwitchRefresh" ng-show="group.charts.length">
+			<div ng-repeat="chart in group.charts track by $index" class="col-md-{{chart.size || 6}}">
+				<div class="nvd3-chart-wrapper">
+					<div nvd3="chart._data" data-holder="chart._holder" data-title="{{chart.title || chart.metrics[0].metric}}" data-watching="false"
+						 data-chart="{{chart.chart || 'line'}}" data-config="getChartConfig(chart)" class="nvd3-chart-cntr"></div>
+					<div class="nvd3-chart-config">
+						<a class="fa fa-expand" ng-click="showChart(chart, -1)"></a>
+						<span ng-if="Auth.isRole('ROLE_ADMIN')">
+							<a class="fa fa-minus" ng-click="configChartSize(chart, -1)"></a>
+							<a class="fa fa-plus" ng-click="configChartSize(chart, 1)"></a>
+							<a class="fa fa-cog" ng-click="configChart(chart)"></a>
+							<a class="fa fa-trash" ng-click="deleteChart(group, chart)"></a>
+						</span>
+					</div>
+				</div>
+			</div>
+		</div>
+
+		<p ng-if="!group.charts.length">
+			Empty group.
+			<span ng-if="Auth.isRole('ROLE_ADMIN')">
+				Click
+				<span ng-hide="dataSourceListReady" class="fa fa-refresh fa-spin"></span>
+				<a ng-show="dataSourceListReady" ng-click="newChart()">here</a>
+				to add metric.
+			</span>
+		</p>
+	</pane>
+</div>
+
+
+
+<!-- Modal: Chart configuration -->
+<div class="modal fade" id="chartMDL" tabindex="-1" role="dialog">
+	<div class="modal-dialog modal-lg" role="document">
+		<div class="modal-content">
+			<div class="modal-header">
+				<button type="button" class="close" data-dismiss="modal" aria-label="Close">
+					<span aria-hidden="true">&times;</span>
+				</button>
+				<h4 class="modal-title">Chart Configuration</h4>
+			</div>
+			<div class="modal-body">
+				<div class="row">
+					<div class="col-md-6">
+						<div class="nvd3-chart-wrapper">
+							<div nvd3="configPreviewChart._data" data-title="{{configPreviewChart.title || configPreviewChart.metrics[0].metric}}"
+								 data-watching="true" data-chart="{{configPreviewChart.chart || 'line'}}" data-config="getChartConfig(configPreviewChart)" class="nvd3-chart-cntr"></div>
+						</div>
+					</div>
+					<div class="col-md-6">
+						<!-- Chart Configuration -->
+						<table class="table">
+							<tbody>
+							<tr>
+								<th width="100">Name</th>
+								<td><input type="text" class="form-control input-xs" ng-model="configPreviewChart.title" placeholder="Default: {{configPreviewChart.metrics[0].metric}}" /></td>
+							</tr>
+							<tr>
+								<th>Chart Type</th>
+								<td>
+									<div class="btn-group" data-toggle="buttons">
+										<label class="btn btn-default btn-xs" ng-class="{active: (configPreviewChart.chart || 'line') === type.chart}"
+											   ng-repeat="type in chartTypeList track by $index" ng-click="configPreviewChart.chart = type.chart;">
+											<input type="radio" name="chartType" autocomplete="off"
+												   ng-checked="(configPreviewChart.chart || 'line') === type.chart">
+											<span class="fa fa-{{type.icon}}"></span>
+										</label>
+									</div>
+								</td>
+							</tr>
+							<tr>
+								<th>Minimum</th>
+								<td><input type="checkbox" ng-checked="configPreviewChart.min === 0" ng-disabled="configPreviewChart.chart === 'area' || configPreviewChart.chart === 'pie'"
+										   ng-click="configPreviewChartMinimumCheck()" /></td>
+							</tr>
+							<tr>
+								<th>Metrics</th>
+								<td>
+									<div ng-repeat="metric in configPreviewChart.metrics" class="box inner-box">
+										<div class="box-tools">
+											<button class="btn btn-box-tool" ng-click="configRemoveMetric(metric)">
+												<span class="fa fa-times"></span>
+											</button>
+										</div>
+
+										<h3 class="box-title">{{metric.metric}}</h3>
+										<div class="checkbox noMargin" ng-repeat="series in chartSeriesList track by $index">
+											<label>
+												<input type="checkbox" ng-checked="seriesChecked(metric, series.series)"
+													   ng-click="seriesCheckClick(metric, series.series, configPreviewChart)" />
+												{{series.name}}
+											</label>
+										</div>
+									</div>
+									<a ng-click="configAddMetric()">+ Add Metric</a>
+								</td>
+							</tr>
+							</tbody>
+						</table>
+					</div>
+				</div>
+			</div>
+			<div class="modal-footer">
+				<button type="button" class="btn btn-default" data-dismiss="modal">
+					Close
+				</button>
+				<button type="button" class="btn btn-primary" ng-click="confirmUpdateChart()">
+					Confirm
+				</button>
+			</div>
+		</div>
+	</div>
+</div>
+
+
+
+<!-- Modal: Metric selector -->
+<div class="modal fade" id="metricMDL" tabindex="-1" role="dialog">
+	<div class="modal-dialog modal-lg" role="document">
+		<div class="modal-content">
+			<div class="modal-header">
+				<button type="button" class="close" data-dismiss="modal" aria-label="Close">
+					<span aria-hidden="true">&times;</span>
+				</button>
+				<h4 class="modal-title">Select Metric</h4>
+			</div>
+			<div class="modal-body">
+				<div class="input-group" style="margin-bottom: 10px;">
+					<input type="text" class="form-control" placeholder="Filter..." ng-model="_newMetricFilter" />
+					<span class="input-group-btn">
+						<button class="btn btn-default btn-flat" ng-click="_newMetricFilter = '';"><span class="fa fa-times"></span></button>
+					</span>
+				</div>
+
+				<div class="row">
+					<div class="col-md-4">
+						<ul class="nav nav-pills nav-stacked fixed-height">
+							<li class="disabled"><a>Data Source</a></li>
+							<li ng-repeat="dataSrc in dataSourceList track by $index" ng-class="{active: _newMetricDataSrc === dataSrc}">
+								<a ng-click="newMetricSelectDataSource(dataSrc)">{{dataSrc.dataSource}}</a>
+							</li>
+						</ul>
+					</div>
+					<div class="col-md-8">
+						<ul class="nav nav-pills nav-stacked fixed-height">
+							<li class="disabled"><a>Metric</a></li>
+							<li ng-repeat="metric in dataSourceMetricList(_newMetricDataSrc, _newMetricFilter) track by $index" ng-class="{active: _newMetricDataMetric === metric}">
+								<a ng-click="newMetricSelectMetric(metric)">{{metric}}</a>
+							</li>
+						</ul>
+					</div>
+				</div>
+			</div>
+			<div class="modal-footer">
+				<span class="text-primary pull-left">{{_newMetricDataSrc.dataSource}} <span class="fa fa-arrow-right"></span> {{_newMetricDataMetric}}</span>
+				<button type="button" class="btn btn-default" data-dismiss="modal">
+					Close
+				</button>
+				<button type="button" class="btn btn-primary" ng-click="confirmSelectMetric()">
+					Select
+				</button>
+			</div>
+		</div>
+	</div>
+</div>
+
+
+
+<!-- Modal: Chart View -->
+<div class="modal fade" id="chartViewMDL" tabindex="-1" role="dialog">
+	<div class="modal-dialog modal-lg" role="document">
+		<div class="modal-content">
+			<div class="modal-header">
+				<button type="button" class="close" data-dismiss="modal" aria-label="Close">
+					<span aria-hidden="true">&times;</span>
+				</button>
+				<h4 class="modal-title">{{viewChart.title || viewChart.metrics[0].metric}}</h4>
+			</div>
+			<div class="modal-body">
+				<div nvd3="viewChart._data" data-title="{{viewChart.title || viewChart.metrics[0].metric}}"
+					 data-watching="true" data-chart="{{viewChart.chart || 'line'}}" data-config="getChartConfig(viewChart)" class="nvd3-chart-cntr lg"></div>
+			</div>
+			<div class="modal-footer">
+				<button type="button" class="btn btn-default" data-dismiss="modal">
+					Close
+				</button>
+			</div>
+		</div>
+	</div>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/feature/topology/controller.js
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/_app/public/feature/topology/controller.js b/eagle-webservice/src/main/webapp/_app/public/feature/topology/controller.js
new file mode 100644
index 0000000..94886c9
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/_app/public/feature/topology/controller.js
@@ -0,0 +1,257 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+(function() {
+	'use strict';
+
+	var featureControllers = angular.module('featureControllers');
+	var feature = featureControllers.register("topology", {
+		global: true	// Global Feature needn't add to applications
+	});
+
+	// ==============================================================
+	// =                       Initialization                       =
+	// ==============================================================
+
+	// ==============================================================
+	// =                          Function                          =
+	// ==============================================================
+	//feature.service("DashboardFormatter", function() {
+	//});
+
+	// ==============================================================
+	// =                         Controller                         =
+	// ==============================================================
+	feature.configNavItem("monitoring", "Topology", "usb");
+
+	// ========================= Monitoring =========================
+	feature.configController('monitoring', function(PageConfig, $scope, $interval, Entities, UI, Site, Application) {
+		var topologyRefreshInterval;
+
+		PageConfig.hideApplication = true;
+		PageConfig.hideSite = true;
+		PageConfig.pageTitle = "Topology Execution";
+
+		$scope.topologyExecutionList = null;
+
+		$scope.currentTopologyExecution = null;
+		$scope.currentTopologyExecutionOptList = [];
+
+		// ======================= Function =======================
+		function refreshExecutionList() {
+			var _list = Entities.queryEntities("TopologyExecutionService");
+			_list._promise.then(function () {
+				$scope.topologyExecutionList = _list;
+			});
+		}
+
+		$scope.showTopologyDetail = function (topologyExecution) {
+			$scope.currentTopologyExecution = topologyExecution;
+			$("#topologyMDL").modal();
+
+			$scope.currentTopologyExecutionOptList = Entities.queryEntities("TopologyOperationService", {
+				site: topologyExecution.tags.site,
+				application: topologyExecution.tags.application,
+				topology: topologyExecution.tags.topology,
+				_pageSize: 10,
+				_duration: 1000 * 60 * 60 * 24 * 30
+			});
+		};
+
+		$scope.getStatusClass = function (status) {
+			switch (status) {
+				case "NEW":
+					return "info";
+				case "STARTING":
+				case "STOPPING":
+					return "warning";
+				case "STARTED":
+					return "success";
+				case "STOPPED":
+					return "danger";
+				default:
+					return "default";
+			}
+		};
+
+		// ==================== Initialization ====================
+		refreshExecutionList();
+		topologyRefreshInterval = $interval(refreshExecutionList, 10 * 1000);
+
+		$scope.topologyList = Entities.queryEntities("TopologyDescriptionService");
+		$scope.topologyList._promise.then(function () {
+			$scope.topologyList = $.map($scope.topologyList, function (topology) {
+				return topology.tags.topology;
+			});
+		});
+
+		$scope.applicationList = $.map(Application.list, function (application) {
+			return application.tags.application;
+		});
+
+		$scope.siteList = $.map(Site.list, function (site) {
+			return site.tags.site;
+		});
+
+		// ================== Topology Execution ==================
+		$scope.newTopologyExecution = function () {
+			UI.createConfirm("Topology", {}, [
+				{field: "site", type: "select", valueList: $scope.siteList},
+				{field: "application", type: "select", valueList: $scope.applicationList},
+				{field: "topology", type: "select", valueList: $scope.topologyList}
+			], function (entity) {
+				for(var i = 0 ; i < $scope.topologyExecutionList.length; i += 1) {
+					var _entity = $scope.topologyExecutionList[i].tags;
+					if(_entity.site === entity.site && _entity.application === entity.application && _entity.topology === entity.topology) {
+						return "Topology already exist!";
+					}
+				}
+			}).then(null, null, function(holder) {
+				var _entity = {
+					tags: {
+						site: holder.entity.site,
+						application: holder.entity.application,
+						topology: holder.entity.topology
+					},
+					status: "NEW"
+				};
+				Entities.updateEntity("TopologyExecutionService", _entity)._promise.then(function() {
+					holder.closeFunc();
+					$scope.topologyExecutionList.push(_entity);
+					refreshExecutionList();
+				});
+			});
+		};
+
+		$scope.deleteTopologyExecution = function (topologyExecution) {
+			UI.deleteConfirm(topologyExecution.tags.topology).then(null, null, function(holder) {
+				Entities.deleteEntities("TopologyExecutionService", topologyExecution.tags)._promise.then(function() {
+					holder.closeFunc();
+					common.array.remove(topologyExecution, $scope.topologyExecutionList);
+				});
+			});
+		};
+
+		// ================== Topology Operation ==================
+		$scope.doTopologyOperation = function (topologyExecution, operation) {
+			$.dialog({
+				title: operation + " Confirm",
+				content: "Do you want to " + operation + " '" + topologyExecution.tags.topology + "'?",
+				confirm: true
+			}, function (ret) {
+				if(!ret) return;
+
+				var list = Entities.queryEntities("TopologyOperationService", {
+					site: topologyExecution.tags.site,
+					application: topologyExecution.tags.application,
+					topology: topologyExecution.tags.topology,
+					_pageSize: 20
+				});
+
+				list._promise.then(function () {
+					var lastOperation = common.array.find(operation, list, "tags.operation");
+					if(lastOperation && (lastOperation.status === "INITIALIZED" || lastOperation.status === "PENDING")) {
+						refreshExecutionList();
+						return;
+					}
+
+					Entities.updateEntity("rest/app/operation", {
+						tags: {
+							site: topologyExecution.tags.site,
+							application: topologyExecution.tags.application,
+							topology: topologyExecution.tags.topology,
+							operation: operation
+						},
+						status: "INITIALIZED"
+					}, {timestamp: false, hook: true});
+				});
+			});
+		};
+
+		$scope.startTopologyOperation = function (topologyExecution) {
+			$scope.doTopologyOperation(topologyExecution, "START");
+		};
+		$scope.stopTopologyOperation = function (topologyExecution) {
+			$scope.doTopologyOperation(topologyExecution, "STOP");
+		};
+
+		// ======================= Clean Up =======================
+		$scope.$on('$destroy', function() {
+			$interval.cancel(topologyRefreshInterval);
+		});
+	});
+
+	// ========================= Management =========================
+	feature.configController('management', function(PageConfig, $scope, Entities, UI) {
+		PageConfig.hideApplication = true;
+		PageConfig.hideSite = true;
+		PageConfig.pageTitle = "Topology";
+
+		var typeList = ["CLASS", "DYNAMIC"];
+		var topologyDefineAttrs = [
+			{field: "topology", name: "name"},
+			{field: "type", type: "select", valueList: typeList},
+			{field: "exeClass", name: "execution entry", description: function (entity) {
+				if(entity.type === "CLASS") return "Class implements interface TopologyExecutable";
+				if(entity.type === "DYNAMIC") return "DSL based topology definition";
+			}, type: "blob", rows: 5},
+			{field: "version", optional: true},
+			{field: "description", optional: true, type: "blob"}
+		];
+		var topologyUpdateAttrs = $.extend(topologyDefineAttrs.concat(), [{field: "topology", name: "name", readonly: true}]);
+
+		$scope.topologyList = Entities.queryEntities("TopologyDescriptionService");
+
+		$scope.newTopology = function () {
+			UI.createConfirm("Topology", {}, topologyDefineAttrs, function (entity) {
+				if(common.array.find(entity.topology, $scope.topologyList, "tags.topology", false, false)) {
+					return "Topology name conflict!";
+				}
+			}).then(null, null, function(holder) {
+				holder.entity.tags = {
+					topology: holder.entity.topology
+				};
+				Entities.updateEntity("TopologyDescriptionService", holder.entity, {timestamp: false})._promise.then(function() {
+					holder.closeFunc();
+					$scope.topologyList.push(holder.entity);
+				});
+			});
+		};
+
+		$scope.updateTopology = function (topology) {
+			UI.updateConfirm("Topology", $.extend({}, topology, {topology: topology.tags.topology}), topologyUpdateAttrs).then(null, null, function(holder) {
+				holder.entity.tags = {
+					topology: holder.entity.topology
+				};
+				Entities.updateEntity("TopologyDescriptionService", holder.entity, {timestamp: false})._promise.then(function() {
+					holder.closeFunc();
+					$.extend(topology, holder.entity);
+				});
+			});
+		};
+
+		$scope.deleteTopology = function (topology) {
+			UI.deleteConfirm(topology.tags.topology).then(null, null, function(holder) {
+				Entities.delete("TopologyDescriptionService", {topology: topology.tags.topology})._promise.then(function() {
+					holder.closeFunc();
+					common.array.remove(topology, $scope.topologyList);
+				});
+			});
+		};
+	});
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/feature/topology/page/management.html
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/_app/public/feature/topology/page/management.html b/eagle-webservice/src/main/webapp/_app/public/feature/topology/page/management.html
new file mode 100644
index 0000000..9e22c84
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/_app/public/feature/topology/page/management.html
@@ -0,0 +1,52 @@
+<div class="box box-primary">
+	<div class="box-header with-border">
+		<i class="fa fa-cog"></i>
+		<a class="pull-right" href="#/config/topology/monitoring"><span class="fa fa-angle-right"></span> Monitoring</a>
+		<h3 class="box-title">
+			Management
+		</h3>
+	</div>
+	<div class="box-body">
+		<table class="table table-bordered">
+			<thead>
+				<tr>
+					<th>Name</th>
+					<th width="20%">Execution Class</th>
+					<th>Type</th>
+					<th width="50%">Description</th>
+					<th>Version</th>
+					<th width="70"></th>
+				</tr>
+			</thead>
+			<tbody>
+				<tr ng-repeat="item in topologyList">
+					<td>{{item.tags.topology}}</td>
+					<td><pre class="noWrap">{{item.exeClass}}</pre></td>
+					<td>{{item.type}}</td>
+					<td>{{item.description}}</td>
+					<td>{{item.version}}</td>
+					<td class="text-center">
+						<button class="rm fa fa-pencil btn btn-default btn-xs" uib-tooltip="Edit" tooltip-animation="false" ng-click="updateTopology(item)"> </button>
+						<button class="rm fa fa-trash-o btn btn-danger btn-xs" uib-tooltip="Delete" tooltip-animation="false" ng-click="deleteTopology(item)"> </button>
+					</td>
+				</tr>
+				<tr ng-if="topologyList.length === 0">
+					<td colspan="6">
+						<span class="text-muted">Empty list</span>
+					</td>
+				</tr>
+			</tbody>
+		</table>
+	</div>
+
+	<div class="box-footer">
+		<button class="btn btn-primary pull-right" ng-click="newTopology()">
+			New Topology
+			<i class="fa fa-plus-circle"> </i>
+		</button>
+	</div>
+
+	<div class="overlay" ng-hide="topologyList._promise.$$state.status === 1;">
+		<i class="fa fa-refresh fa-spin"></i>
+	</div>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/feature/topology/page/monitoring.html
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/_app/public/feature/topology/page/monitoring.html b/eagle-webservice/src/main/webapp/_app/public/feature/topology/page/monitoring.html
new file mode 100644
index 0000000..0acb2c1
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/_app/public/feature/topology/page/monitoring.html
@@ -0,0 +1,151 @@
+<div class="box box-primary">
+	<div class="box-header with-border">
+		<i class="fa fa-eye"></i>
+		<a class="pull-right" href="#/config/topology/management"><span class="fa fa-angle-right"></span> Management</a>
+		<h3 class="box-title">
+			Monitoring
+		</h3>
+	</div>
+	<div class="box-body">
+		<div sorttable source="topologyExecutionList">
+			<table class="table table-bordered" ng-non-bindable>
+				<thead>
+				<tr>
+					<th width="70" sortpath="status">Status</th>
+					<th width="90" sortpath="tags.topology">Topology</th>
+					<th width="60" sortpath="tags.site">Site</th>
+					<th width="100" sortpath="tags.application">Application</th>
+					<th width="60" sortpath="mode">Mode</th>
+					<th sortpath="description">Description</th>
+					<th width="70" style="min-width: 70px;"></th>
+				</tr>
+				</thead>
+				<tbody>
+				<tr>
+					<td class="text-center">
+						<span class="label label-{{_parent.getStatusClass(item.status)}}">{{item.status}}</span>
+					</td>
+					<td><a ng-click="_parent.showTopologyDetail(item)">{{item.tags.topology}}</a></td>
+					<td>{{item.tags.site}}</td>
+					<td>{{item.tags.application}}</td>
+					<td>{{item.mode}}</td>
+					<td>{{item.description}}</td>
+					<td class="text-center">
+						<button ng-if="item.status === 'NEW' || item.status === 'STOPPED'" class="fa fa-play sm btn btn-default btn-xs" uib-tooltip="Start" tooltip-animation="false" ng-click="_parent.startTopologyOperation(item)"> </button>
+						<button ng-if="item.status === 'STARTED'" class="fa fa-stop sm btn btn-default btn-xs" uib-tooltip="Stop" tooltip-animation="false" ng-click="_parent.stopTopologyOperation(item)"> </button>
+						<button ng-if="item.status !== 'NEW' && item.status !== 'STARTED' && item.status !== 'STOPPED'" class="fa fa-ban sm btn btn-default btn-xs" disabled="disabled"> </button>
+						<button class="rm fa fa-trash-o btn btn-danger btn-xs" uib-tooltip="Delete" tooltip-animation="false" ng-click="_parent.deleteTopologyExecution(item)"> </button>
+					</td>
+				</tr>
+				</tbody>
+			</table>
+		</div>
+	</div>
+
+	<div class="box-footer">
+		<button class="btn btn-primary pull-right" ng-click="newTopologyExecution()">
+			New Topology Execution
+			<i class="fa fa-plus-circle"> </i>
+		</button>
+	</div>
+
+	<div class="overlay" ng-hide="topologyExecutionList._promise.$$state.status === 1;">
+		<i class="fa fa-refresh fa-spin"></i>
+	</div>
+</div>
+
+
+
+
+<!-- Modal: Topology Detail -->
+<div class="modal fade" id="topologyMDL" tabindex="-1" role="dialog">
+	<div class="modal-dialog modal-lg" role="document">
+		<div class="modal-content">
+			<div class="modal-header">
+				<button type="button" class="close" data-dismiss="modal" aria-label="Close">
+					<span aria-hidden="true">&times;</span>
+				</button>
+				<h4 class="modal-title">Topology Detail</h4>
+			</div>
+			<div class="modal-body">
+				<h3>Detail</h3>
+				<table class="table">
+					<tbody>
+						<tr>
+							<th>Site</th>
+							<td>{{currentTopologyExecution.tags.site}}</td>
+						</tr>
+						<tr>
+							<th>Application</th>
+							<td>{{currentTopologyExecution.tags.application}}</td>
+						</tr>
+						<tr>
+							<th>Topology</th>
+							<td>{{currentTopologyExecution.tags.topology}}</td>
+						</tr>
+						<tr>
+							<th>Full Name</th>
+							<td>{{currentTopologyExecution.fullName || "-"}}</td>
+						</tr>
+						<tr>
+							<th>Status</th>
+							<td>
+								<span class="label label-{{getStatusClass(currentTopologyExecution.status)}}">{{currentTopologyExecution.status}}</span>
+							</td>
+						</tr>
+						<tr>
+							<th>Mode</th>
+							<td>{{currentTopologyExecution.mode || "-"}}</td>
+						</tr>
+						<tr>
+							<th>Environment</th>
+							<td>{{currentTopologyExecution.environment || "-"}}</td>
+						</tr>
+						<tr>
+							<th>Url</th>
+							<td>
+								<a ng-if="currentTopologyExecution.url" href="{{currentTopologyExecution.url}}" target="_blank">{{currentTopologyExecution.url}}</a>
+								<span ng-if="!currentTopologyExecution.url">-</span>
+							</td>
+						</tr>
+						<tr>
+							<th>Description</th>
+							<td>{{currentTopologyExecution.description || "-"}}</td>
+						</tr>
+						<tr>
+							<th>Last Modified Date</th>
+							<td>{{common.format.date(currentTopologyExecution.lastModifiedDate) || "-"}}</td>
+						</tr>
+					</tbody>
+				</table>
+
+				<h3>Latest Operations</h3>
+				<div class="table-responsive">
+					<table class="table table-bordered table-sm margin-bottom-none">
+						<thead>
+							<tr>
+								<th>Date Time</th>
+								<th>Operation</th>
+								<th>Status</th>
+								<th>Message</th>
+							</tr>
+						</thead>
+						<tbody>
+							<tr ng-repeat="action in currentTopologyExecutionOptList track by $index">
+								<td>{{common.format.date(action.lastModifiedDate) || "-"}}</td>
+								<td>{{action.tags.operation}}</td>
+								<td>{{action.status}}</td>
+								<td><pre class="noWrap">{{action.message}}</pre></td>
+							</tr>
+						</tbody>
+					</table>
+				</div>
+			</div>
+			<div class="modal-footer">
+				<button type="button" class="btn btn-default" data-dismiss="modal">
+					Close
+				</button>
+			</div>
+		</div>
+	</div>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/feature/userProfile/controller.js
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/_app/public/feature/userProfile/controller.js b/eagle-webservice/src/main/webapp/_app/public/feature/userProfile/controller.js
new file mode 100644
index 0000000..ed619d3
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/_app/public/feature/userProfile/controller.js
@@ -0,0 +1,268 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+(function() {
+	'use strict';
+
+	var featureControllers = angular.module('featureControllers');
+	var feature = featureControllers.register("userProfile");
+
+	// ==============================================================
+	// =                          Function                          =
+	// ==============================================================
+
+	// ==============================================================
+	// =                        User Profile                        =
+	// ==============================================================
+
+	// ======================== Profile List ========================
+	//feature.navItem("list", "User Profiles", "graduation-cap");
+	feature.controller('list', function(PageConfig, Site, $scope, $interval, Entities) {
+		PageConfig.pageSubTitle = Site.current().tags.site;
+
+		$scope.common = common;
+		$scope.algorithms = [];
+
+		// ======================================== Algorithms ========================================
+		$scope.algorithmEntity = {};
+		Entities.queryEntities("AlertDefinitionService", {site: Site.current().tags.site, application: "userProfile"})._promise.then(function(data) {
+			$scope.algorithmEntity = common.getValueByPath(data, "obj[0]");
+			$scope.algorithmEntity.policy = common.parseJSON($scope.algorithmEntity.policyDef);
+		});
+
+		// ======================================= User profile =======================================
+		$scope.profileList = Entities.queryEntities("MLModelService", {site: Site.current().tags.site}, ["user", "algorithm", "content", "version"]);
+		$scope.profileList._promise.then(function() {
+			var _algorithms = {};
+			var _users = {};
+
+			// Map user
+			$.each($scope.profileList, function(i, unit) {
+				_algorithms[unit.tags.algorithm] = unit.tags.algorithm;
+				var _user = _users[unit.tags.user] = _users[unit.tags.user] || {user: unit.tags.user};
+				_user[unit.tags.algorithm] = {
+					version: unit.version
+				};
+
+				// DE
+				if(unit.tags.algorithm === "DE") {
+					var _statistics = common.parseJSON(unit.content);
+					_statistics = common.getValueByPath(_statistics, "statistics", []);
+					_user[unit.tags.algorithm].topCommands = $.map(common.array.top(_statistics, "mean"), function(command) {
+						return command.commandName;
+					});
+				}
+			});
+
+			// Map algorithms
+			$scope.algorithms = $.map(_algorithms, function(algorithm) {
+				return algorithm;
+			}).sort();
+
+			$scope.profileList.splice(0);
+			$scope.profileList.push.apply($scope.profileList, common.map.toArray(_users));
+		});
+
+		// =========================================== Task ===========================================
+		$scope.tasks = [];
+		function _loadTasks() {
+			var _tasks = Entities.queryEntities("ScheduleTaskService", {
+				site: Site.current().tags.site,
+				_pageSize: 100,
+				_duration: 1000 * 60 * 60 * 24 * 14,
+				__ETD: 1000 * 60 * 60 * 24
+			});
+			_tasks._promise.then(function() {
+				$scope.tasks.splice(0);
+				$scope.tasks.push.apply($scope.tasks, _tasks);
+
+				// Duration
+				$.each($scope.tasks, function(i, data) {
+					if(data.timestamp && data.updateTime) {
+						var _ms = (new moment(data.updateTime)).diff(new moment(data.timestamp));
+						var _d = moment.duration(_ms);
+						data._duration = Math.floor(_d.asHours()) + moment.utc(_ms).format(":mm:ss");
+						data.duration = _ms;
+					} else {
+						data._duration = "--";
+					}
+				});
+			});
+		}
+
+		$scope.runningTaskCount = function () {
+			return common.array.count($scope.tasks, "INITIALIZED", "status") +
+				common.array.count($scope.tasks, "PENDING", "status") +
+				common.array.count($scope.tasks, "EXECUTING", "status");
+		};
+
+		// Create task
+		$scope.updateTask = function() {
+			$.dialog({
+				title: "Confirm",
+				content: "Do you want to update now?",
+				confirm: true
+			}, function(ret) {
+				if(!ret) return;
+
+				var _entity = {
+					status: "INITIALIZED",
+					detail: "Newly created command",
+					tags: {
+						site: Site.current().tags.site,
+						type: "USER_PROFILE_TRAINING"
+					},
+					timestamp: +new Date()
+				};
+				Entities.updateEntity("ScheduleTaskService", _entity, {timestamp: false})._promise.success(function(data) {
+					if(!Entities.dialog(data)) {
+						_loadTasks();
+					}
+				});
+			});
+		};
+
+		// Show detail
+		$scope.showTaskDetail = function(task) {
+			var _content = $("<pre>").text(task.detail);
+
+			var $mdl = $.dialog({
+				title: "Detail",
+				content: _content
+			});
+
+			_content.click(function(e) {
+				if(!e.ctrlKey) return;
+
+				$.dialog({
+					title: "Confirm",
+					content: "Remove this task?",
+					confirm: true
+				}, function(ret) {
+					if(!ret) return;
+
+					$mdl.modal('hide');
+					Entities.deleteEntity("ScheduleTaskService", task)._promise.then(function() {
+						_loadTasks();
+					});
+				});
+			});
+		};
+
+		_loadTasks();
+		var _loadInterval = $interval(_loadTasks, app.time.refreshInterval);
+		$scope.$on('$destroy',function(){
+			$interval.cancel(_loadInterval);
+		});
+	});
+
+	// ======================= Profile Detail =======================
+	feature.controller('detail', function(PageConfig, Site, $scope, $wrapState, Entities) {
+		PageConfig.pageTitle = "User Profile";
+		PageConfig.pageSubTitle = Site.current().tags.site;
+		PageConfig
+			.addNavPath("User Profile", "/userProfile/list")
+			.addNavPath("Detail");
+
+		$scope.user = $wrapState.param.filter;
+
+		// User profile
+		$scope.profiles = {};
+		$scope.profileList = Entities.queryEntities("MLModelService", {site: Site.current().tags.site, user: $scope.user});
+		$scope.profileList._promise.then(function() {
+			$.each($scope.profileList, function(i, unit) {
+				unit._content = common.parseJSON(unit.content);
+				$scope.profiles[unit.tags.algorithm] = unit;
+			});
+
+			// DE
+			if($scope.profiles.DE) {
+				console.log($scope.profiles.DE);
+
+				$scope.profiles.DE._chart = {};
+
+				$scope.profiles.DE.estimates = {};
+				$.each($scope.profiles.DE._content, function(key, value) {
+					if(key !== "statistics") {
+						$scope.profiles.DE.estimates[key] = value;
+					}
+				});
+
+				var _meanList = [];
+				var _stddevList = [];
+
+				$.each($scope.profiles.DE._content.statistics, function(i, unit) {
+					_meanList[i] = {
+						x: unit.commandName,
+						y: unit.mean
+					};
+					_stddevList[i] = {
+						x: unit.commandName,
+						y: unit.stddev
+					};
+				});
+				$scope.profiles.DE._chart.series = [
+					{
+						key: "mean",
+						values: _meanList
+					},
+					{
+						key: "stddev",
+						values: _stddevList
+					}
+				];
+
+				// Percentage table list
+				$scope.profiles.DE.meanList = [];
+				var _total = common.array.sum($scope.profiles.DE._content.statistics, "mean");
+				$.each($scope.profiles.DE._content.statistics, function(i, unit) {
+					$scope.profiles.DE.meanList.push({
+						command: unit.commandName,
+						percentage: unit.mean / _total
+					});
+				});
+			}
+
+			// EigenDecomposition
+			if($scope.profiles.EigenDecomposition && $scope.profiles.EigenDecomposition._content.principalComponents) {
+				$scope.profiles.EigenDecomposition._chart = {
+					series: [],
+				};
+
+				$.each($scope.profiles.EigenDecomposition._content.principalComponents, function(z, grp) {
+					var _line = [];
+					$.each(grp, function(x, y) {
+						_line.push([x,y,z]);
+					});
+
+					$scope.profiles.EigenDecomposition._chart.series.push({
+						data: _line
+					});
+				});
+			}
+		});
+
+		// UI
+		$scope.showRawData = function(content) {
+			$.dialog({
+				title: "Raw Data",
+				content: $("<pre>").text(content)
+			});
+		};
+	});
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/feature/userProfile/page/detail.html
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/_app/public/feature/userProfile/page/detail.html b/eagle-webservice/src/main/webapp/_app/public/feature/userProfile/page/detail.html
new file mode 100644
index 0000000..0f94e03
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/_app/public/feature/userProfile/page/detail.html
@@ -0,0 +1,87 @@
+<!--
+  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">
+		<i class="fa fa-user"> </i>
+		<h3 class="box-title">
+			{{user}}
+		</h3>
+	</div>
+	<div class="box-body">
+		<div>
+			<div class="inline-group">
+				<dl><dt>User</dt><dd>{{user}}</dd></dl>
+				<dl><dt>Site</dt><dd>{{Site.current().tags.site}}</dd></dl>
+			</div>
+			<div class="inline-group">
+				<dl><dt>Other Info</dt><dd class="text-muted">N/A</dd></dl>
+			</div>
+		</div>
+
+		<div class="overlay" ng-hide="profileList._promise.$$state.status === 1;">
+			<span class="fa fa-refresh fa-spin"></span>
+		</div>
+	</div>
+</div>
+
+<!-- Analysis -->
+<div class="nav-tabs-custom">
+	<ul class="nav nav-tabs">
+		<li class="active">
+			<a href="[data-id='DE']" data-toggle="tab" ng-click=" currentTab='DE'">DE</a>
+		</li>
+		<li>
+			<a href="[data-id='EigenDecomposition']" data-toggle="tab" ng-click=" currentTab='EigenDecomposition'">EigenDecomposition</a>
+		</li>
+		<li class="pull-right">
+			<button class="btn btn-primary" ng-click="showRawData(currentTab === 'EigenDecomposition' ? profiles.EigenDecomposition.content : profiles.DE.content)">Raw Data</button>
+		</li>
+	</ul>
+	<div class="tab-content">
+		<div class="tab-pane active" data-id="DE">
+			<div class="row">
+				<div class="col-md-9">
+					<div nvd3="profiles.DE._chart.series" data-config="{chart: 'column', xType: 'text', height: 400}" class="nvd3-chart-cntr" height="400"></div>
+					<div class="inline-group text-center">
+						<dl ng-repeat="(key, value) in profiles.DE.estimates"><dt>{{key}}</dt><dd>{{value}}</dd></dl>
+					</div>
+				</div>
+
+				<div class="col-md-3">
+					<table class="table table-bordered">
+						<thead>
+							<tr>
+								<th>Command</th>
+								<th>Percentage</th>
+							</tr>
+						</thead>
+						<tbody>
+							<tr ng-repeat="unit in profiles.DE.meanList">
+								<td>{{unit.command}}</td>
+								<td class="text-right">{{(unit.percentage*100).toFixed(2)}}%</td>
+							</tr>
+						</tbody>
+					</table>
+				</div>
+			</div>
+		</div><!-- /.tab-pane -->
+		<div class="tab-pane" data-id="EigenDecomposition">
+			<div line3d-chart height="400" data="profiles.EigenDecomposition._chart.series"> </div>
+		</div><!-- /.tab-pane -->
+	</div><!-- /.tab-content -->
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/feature/userProfile/page/list.html
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/_app/public/feature/userProfile/page/list.html b/eagle-webservice/src/main/webapp/_app/public/feature/userProfile/page/list.html
new file mode 100644
index 0000000..2f14479
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/_app/public/feature/userProfile/page/list.html
@@ -0,0 +1,138 @@
+<!--
+  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">
+		<i class="fa fa-list-alt"> </i>
+		<h3 class="box-title">
+			User Profiles
+			<small><a data-toggle="collapse" href="[data-id='algorithms']">Detail</a></small>
+		</h3>
+		<div class="pull-right">
+			<a class="label label-primary" ng-class="runningTaskCount() ? 'label-primary' : 'label-default'" data-toggle="modal" data-target="#taskMDL">Update</a>
+		</div>
+	</div>
+	<div class="box-body">
+		<!-- Algorithms -->
+		<div data-id="algorithms" class="collapse">
+			<table class="table table-bordered">
+				<thead>
+					<tr>
+						<th>Name</th>
+						<td>Feature</td>
+					</tr>
+				</thead>
+				<tbody>
+					<tr ng-repeat="algorithm in algorithmEntity.policy.algorithms">
+						<td>{{algorithm.name}}</td>
+						<td>{{algorithm.features}}</td>
+					</tr>
+				</tbody>
+			</table>
+			<hr/>
+		</div>
+
+		<!-- User Profile List -->
+		<p ng-show="profileList._promise.$$state.status !== 1">
+			<span class="fa fa-refresh fa-spin"> </span>
+			Loading...
+		</p>
+
+		<div sorttable source="profileList" ng-show="profileList._promise.$$state.status === 1">
+			<table class="table table-bordered" ng-non-bindable>
+				<thead>
+					<tr>
+						<th width="10%" sortpath="user">User</th>
+						<th>Most Predominat Feature</th>
+						<th width="10"></th>
+					</tr>
+				</thead>
+				<tbody>
+					<tr>
+						<td>
+							<a href="#/userProfile/detail/{{item.user}}">{{item.user}}</a>
+						</td>
+						<td>
+							{{item.DE.topCommands.slice(0,3).join(", ")}}
+						</td>
+						<td>
+							<a href="#/userProfile/detail/{{item.user}}">Detail</a>
+						</td>
+					</tr>
+				</tbody>
+			</table>
+		</div>
+	</div>
+</div>
+
+<!-- Modal: User profile Schedule Task -->
+<div class="modal fade" id="taskMDL" tabindex="-1" role="dialog">
+	<div class="modal-dialog modal-lg" role="document">
+		<div class="modal-content">
+			<div class="modal-header">
+				<button type="button" class="close" data-dismiss="modal" aria-label="Close">
+					<span aria-hidden="true">&times;</span>
+				</button>
+				<h4 class="modal-title">Training History</h4>
+			</div>
+			<div class="modal-body">
+				<div sorttable source="tasks">
+					<table class="table table-bordered" ng-non-bindable>
+						<thead>
+							<tr>
+								<th sortpath="tags.type">Command</th>
+								<th sortpath="timestamp">Start Time</th>
+								<th sortpath="updateTime">Update Time</th>
+								<th sortpath="duration">Duration</th>
+								<th sortpath="status">Status</th>
+								<th width="10"> </th>
+							</tr>
+						</thead>
+						<tbody>
+							<tr>
+								<td>{{item.tags.type}}</td>
+								<td>{{common.format.date(item.timestamp) || "--"}}</td>
+								<td>{{common.format.date(item.updateTime) || "--"}}</td>
+								<td>{{item._duration}}</td>
+								<td class="text-nowrap">
+									<span class="fa fa-hourglass-start text-muted" ng-show="item.status === 'INITIALIZED'"></span>
+									<span class="fa fa-hourglass-half text-info" ng-show="item.status === 'PENDING'"></span>
+									<span class="fa fa-circle-o-notch text-primary" ng-show="item.status === 'EXECUTING'"></span>
+									<span class="fa fa-check-circle text-success" ng-show="item.status === 'SUCCEEDED'"></span>
+									<span class="fa fa-exclamation-circle text-danger" ng-show="item.status === 'FAILED'"></span>
+									<span class="fa fa-ban text-muted" ng-show="item.status === 'CANCELED'"></span>
+									{{item.status}}
+								</td>
+								<td>
+									<a ng-click="_parent.showTaskDetail(item)">Detail</a>
+								</td>
+							</tr>
+						</tbody>
+					</table>
+				</div>
+			</div>
+			<div class="modal-footer">
+				<button type="button" class="btn btn-primary pull-left" ng-click="updateTask()" ng-show="Auth.isRole('ROLE_ADMIN')">
+					Update Now
+				</button>
+				<button type="button" class="btn btn-default" data-dismiss="modal">
+					Close
+				</button>
+			</div>
+		</div>
+	</div>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/images/favicon.png
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/_app/public/images/favicon.png b/eagle-webservice/src/main/webapp/_app/public/images/favicon.png
new file mode 100644
index 0000000..3bede2a
Binary files /dev/null and b/eagle-webservice/src/main/webapp/_app/public/images/favicon.png differ

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/images/favicon_white.png
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/_app/public/images/favicon_white.png b/eagle-webservice/src/main/webapp/_app/public/images/favicon_white.png
new file mode 100644
index 0000000..9879e92
Binary files /dev/null and b/eagle-webservice/src/main/webapp/_app/public/images/favicon_white.png differ

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/js/app.config.js
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/_app/public/js/app.config.js b/eagle-webservice/src/main/webapp/_app/public/js/app.config.js
new file mode 100644
index 0000000..d7c4be9
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/_app/public/js/app.config.js
@@ -0,0 +1,126 @@
+/*
+ * 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';
+
+	app.config = {
+		// ============================================================================
+		// =                                   URLs                                   =
+		// ============================================================================
+		urls: {
+			HOST: '..',
+
+			updateEntity: 'rest/entities?serviceName=${serviceName}',
+			queryEntity: 'rest/entities/rowkey?serviceName=${serviceName}&value=${encodedRowkey}',
+			queryEntities: 'rest/entities?query=${serviceName}[${condition}]{${values}}&pageSize=100000',
+			deleteEntity: 'rest/entities/delete?serviceName=${serviceName}&byId=true',
+			deleteEntities: 'rest/entities?query=${serviceName}[${condition}]{*}&pageSize=100000',
+
+			queryGroup: 'rest/entities?query=${serviceName}[${condition}]<${groupBy}>{${values}}&pageSize=100000',
+			querySeries: 'rest/entities?query=${serviceName}[${condition}]<${groupBy}>{${values}}&pageSize=100000&timeSeries=true&intervalmin=${intervalmin}',
+
+			query: 'rest/',
+
+			userProfile: 'rest/authentication',
+			logout: 'logout',
+
+			maprNameResolver: '../rest/maprNameResolver',
+
+			DELETE_HOOK: {
+				FeatureDescService: 'rest/module/feature?feature=${feature}',
+				ApplicationDescService: 'rest/module/application?application=${application}',
+				SiteDescService: 'rest/module/site?site=${site}',
+				TopologyDescriptionService: 'rest/app/topology?topology=${topology}'
+			},
+			UPDATE_HOOK: {
+				SiteDescService: 'rest/module/siteApplication'
+			}
+		},
+	};
+
+	// ============================================================================
+	// =                                   URLs                                   =
+	// ============================================================================
+	app.getURL = function(name, kvs) {
+		var _path = app.config.urls[name];
+		if(!_path) throw "URL:'" + name + "' not exist!";
+		var _url = app.packageURL(_path);
+		if(kvs !== undefined) {
+			_url = common.template(_url, kvs);
+		}
+		return _url;
+	};
+
+	app.getMapRNameResolverURL = function(name,value, site) {
+		var key = "maprNameResolver";
+		var _path = app.config.urls[key];
+		if(!_path) throw "URL:'" + name + "' not exist!";
+		var _url = _path;
+		if(name == "fNameResolver") {
+			_url +=  "/" + name + "?fName=" + value + "&site=" + site;
+		} else if(name == "sNameResolver") {
+			_url +=  "/" + name + "?sName=" + value + "&site=" + site;
+		} else if (name == "vNameResolver") {
+			_url += "/" + name + "?vName=" + value + "&site=" + site;
+		} else{
+			throw "resolver:'" + name + "' not exist!";
+		}
+		return _url;
+	};
+
+	function getHookURL(hookType, serviceName) {
+		var _path = app.config.urls[hookType][serviceName];
+		if(!_path) return null;
+
+		return app.packageURL(_path);
+	}
+
+	/***
+	 * Eagle support delete function to process special entity delete. Which will delete all the relative entity.
+	 * @param serviceName
+	 */
+	app.getDeleteURL = function(serviceName) {
+		return getHookURL('DELETE_HOOK', serviceName);
+	};
+
+	/***
+	 * Eagle support update function to process special entity update. Which will update all the relative entity.
+	 * @param serviceName
+	 */
+	app.getUpdateURL = function(serviceName) {
+		return getHookURL('UPDATE_HOOK', serviceName);
+	};
+
+	app.packageURL = function (path) {
+		var _host = localStorage.getItem("HOST") || app.config.urls.HOST;
+		return (_host ? _host + "/" : '') + path;
+	};
+
+	app._Host = function(host) {
+		if(host) {
+			localStorage.setItem("HOST", host);
+			return app;
+		}
+		return localStorage.getItem("HOST");
+	};
+	app._Host.clear = function() {
+		localStorage.removeItem("HOST");
+	};
+	app._Host.sample = "http://localhost:9099/eagle-service";
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/js/app.js
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/_app/public/js/app.js b/eagle-webservice/src/main/webapp/_app/public/js/app.js
new file mode 100644
index 0000000..70b4afe
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/_app/public/js/app.js
@@ -0,0 +1,499 @@
+/*
+ * 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.
+ */
+
+var app = {};
+
+(function() {
+	'use strict';
+
+	/* App Module */
+	var eagleApp = angular.module('eagleApp', ['ngRoute', 'ngAnimate', 'ui.router', 'eagleControllers', 'featureControllers', 'eagle.service']);
+
+	// GRUNT REPLACEMENT: eagleApp.buildTimestamp = TIMESTAMP
+	eagleApp._TRS = function() {
+		return eagleApp.buildTimestamp || Math.random();
+	};
+
+	// ======================================================================================
+	// =                                   Feature Module                                   =
+	// ======================================================================================
+	var FN_ARGS = /^[^\(]*\(\s*([^\)]*)\)/m;
+	var FN_ARG_SPLIT = /,/;
+	var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/;
+	var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
+
+	var featureControllers = angular.module('featureControllers', ['ui.bootstrap', 'eagle.components']);
+	var featureControllerCustomizeHtmlTemplate = {};
+	var featureControllerProvider;
+	var featureProvider;
+
+	featureControllers.config(function ($controllerProvider, $provide) {
+		featureControllerProvider = $controllerProvider;
+		featureProvider = $provide;
+	});
+
+	featureControllers.service("Feature", function($wrapState, PageConfig, ConfigPageConfig, FeaturePageConfig) {
+		var _features = {};
+		var _services = {};
+
+		var Feature = function(name, config) {
+			this.name = name;
+			this.config = config || {};
+			this.features = {};
+		};
+
+		/***
+		 * Inner function. Replace the dependency of constructor.
+		 * @param constructor
+		 * @private
+		 */
+		Feature.prototype._replaceDependencies = function(constructor) {
+			var i, srvName;
+			var _constructor, _$inject;
+			var fnText, argDecl;
+
+			if($.isArray(constructor)) {
+				_constructor = constructor[constructor.length - 1];
+				_$inject = constructor.slice(0, -1);
+			} else if(constructor.$inject) {
+				_constructor = constructor;
+				_$inject = constructor.$inject;
+			} else {
+				_$inject = [];
+				_constructor = constructor;
+				fnText = constructor.toString().replace(STRIP_COMMENTS, '');
+				argDecl = fnText.match(FN_ARGS);
+				$.each(argDecl[1].split(FN_ARG_SPLIT), function(i, arg) {
+					arg.replace(FN_ARG, function(all, underscore, name) {
+						_$inject.push(name);
+					});
+				});
+			}
+			_constructor.$inject = _$inject;
+
+			for(i = 0 ; i < _$inject.length ; i += 1) {
+				srvName = _$inject[i];
+				_$inject[i] = this.features[srvName] || _$inject[i];
+			}
+
+			return _constructor;
+		};
+
+		/***
+		 * Register a common service for feature usage. Common service will share between the feature. If you are coding customize feature, use 'Feature.service' is the better way.
+		 * @param name
+		 * @param constructor
+		 */
+		Feature.prototype.commonService = function(name, constructor) {
+			if(!_services[name]) {
+				featureProvider.service(name, constructor);
+				_services[name] = this.name;
+			} else {
+				throw "Service '" + name + "' has already be registered by feature '" + _services[name] + "'";
+			}
+		};
+
+		/***
+		 * Register a service for feature usage.
+		 * @param name
+		 * @param constructor
+		 */
+		Feature.prototype.service = function(name, constructor) {
+			var _serviceName;
+			if(!this.features[name]) {
+				_serviceName = "__FEATURE_" + this.name + "_" + name;
+				featureProvider.service(_serviceName, this._replaceDependencies(constructor));
+				this.features[name] = _serviceName;
+			} else {
+				console.warn("Service '" + name + "' has already be registered.");
+			}
+		};
+
+		/***
+		 * Create an navigation item in left navigation bar
+		 * @param path
+		 * @param title
+		 * @param icon use Font Awesome. Needn't with 'fa fa-'.
+		 */
+		Feature.prototype.navItem = function(path, title, icon) {
+			title = title || path;
+			icon = icon || "question";
+
+			FeaturePageConfig.addNavItem(this.name, {
+				icon: icon,
+				title: title,
+				url: "#/" + this.name + "/" + path
+			});
+		};
+
+		/***
+		 * Register a controller.
+		 * @param name
+		 * @param constructor
+		 */
+		Feature.prototype.controller = function(name, constructor, htmlTemplatePath) {
+			var _name = this.name + "_" + name;
+
+			// Replace feature registered service
+			constructor = this._replaceDependencies(constructor);
+
+			// Register controller
+			featureControllerProvider.register(_name, constructor);
+			if(htmlTemplatePath) {
+				featureControllerCustomizeHtmlTemplate[_name] = htmlTemplatePath;
+			}
+
+			return _name;
+		};
+
+		/***
+		 * Register a configuration controller for admin usage.
+		 * @param name
+		 * @param constructor
+		 */
+		Feature.prototype.configController = function(name, constructor, htmlTemplatePath) {
+			var _name = "config_" + this.name + "_" + name;
+
+			// Replace feature registered service
+			constructor = this._replaceDependencies(constructor);
+
+			// Register controller
+			featureControllerProvider.register(_name, constructor);
+			if(htmlTemplatePath) {
+				featureControllerCustomizeHtmlTemplate[_name] = htmlTemplatePath;
+			}
+
+			return _name;
+		};
+
+		/***
+		 * Create an navigation item in left navigation bar for admin configuraion page
+		 * @param path
+		 * @param title
+		 * @param icon use Font Awesome. Needn't with 'fa fa-'.
+		 */
+		Feature.prototype.configNavItem = function(path, title, icon) {
+			title = title || path;
+			icon = icon || "question";
+
+			ConfigPageConfig.addNavItem(this.name, {
+				icon: icon,
+				title: title,
+				url: "#/config/" + this.name + "/" + path
+			});
+		};
+
+		// Register
+		featureControllers.register = Feature.register = function(featureName, config) {
+			_features[featureName] = _features[featureName] || new Feature(featureName, config);
+			return _features[featureName];
+		};
+
+		// Page go
+		Feature.go = function(feature, page, filter) {
+			if(!filter) {
+				$wrapState.go("page", {
+					feature: feature,
+					page: page
+				}, 2);
+			} else {
+				$wrapState.go("pageFilter", {
+					feature: feature,
+					page: page,
+					filter: filter
+				}, 2);
+			}
+		};
+
+		// Get feature by name
+		Feature.get = function (featureName) {
+			return _features[featureName];
+		};
+
+		return Feature;
+	});
+
+	// ======================================================================================
+	// =                                   Router config                                    =
+	// ======================================================================================
+	eagleApp.config(function ($stateProvider, $urlRouterProvider, $animateProvider) {
+		// Resolve
+		function _resolve(config) {
+			config = config || {};
+
+			var resolve = {
+				Site: function (Site) {
+					return Site._promise();
+				},
+				Authorization: function (Authorization) {
+					if(!config.roleType) {
+						return Authorization._promise();
+					} else {
+						return Authorization.rolePromise(config.roleType);
+					}
+				},
+				Application: function (Application) {
+					return Application._promise();
+				}
+			};
+
+			if(config.featureCheck) {
+				resolve._navigationCheck = function($q, $wrapState, Site, Application) {
+					var _deferred = $q.defer();
+
+					$q.all(Site._promise(), Application._promise()).then(function() {
+						var _match, i, tmpApp;
+						var _site = Site.current();
+						var _app = Application.current();
+
+						// Check application
+						if(_site && (
+							!_app ||
+							!_site.applicationList.set[_app.tags.application] ||
+							!_site.applicationList.set[_app.tags.application].enabled
+							)
+						) {
+							_match = false;
+
+							for(i = 0 ; i < _site.applicationGroupList.length ; i += 1) {
+								tmpApp = _site.applicationGroupList[i].enabledList[0];
+								if(tmpApp) {
+									_app = Application.current(tmpApp);
+									_match = true;
+									break;
+								}
+							}
+
+							if(!_match) {
+								_app = null;
+								Application.current(null);
+							}
+						}
+					}).finally(function() {
+						_deferred.resolve();
+					});
+
+					return _deferred.promise;
+				};
+			}
+
+			return resolve;
+		}
+
+		// Router
+		var _featureBase = {
+			templateUrl: function ($stateParams) {
+				var _htmlTemplate = featureControllerCustomizeHtmlTemplate[$stateParams.feature + "_" + $stateParams.page];
+				return  "public/feature/" + $stateParams.feature + "/page/" + (_htmlTemplate ||  $stateParams.page) + ".html?_=" + eagleApp._TRS();
+			},
+			controllerProvider: function ($stateParams) {
+				return $stateParams.feature + "_" + $stateParams.page;
+			},
+			resolve: _resolve({featureCheck: true}),
+			pageConfig: "FeaturePageConfig"
+		};
+
+		$urlRouterProvider.otherwise("/landing");
+		$stateProvider
+			// =================== Landing ===================
+			.state('landing', {
+				url: "/landing",
+				templateUrl: "partials/landing.html?_=" + eagleApp._TRS(),
+				controller: "landingCtrl",
+				resolve: _resolve({featureCheck: true})
+			})
+
+			// ================ Authorization ================
+			.state('login', {
+				url: "/login",
+				templateUrl: "partials/login.html?_=" + eagleApp._TRS(),
+				controller: "authLoginCtrl",
+				access: {skipCheck: true}
+			})
+
+			// ================ Configuration ================
+			// Site
+			.state('configSite', {
+				url: "/config/site",
+				templateUrl: "partials/config/site.html?_=" + eagleApp._TRS(),
+				controller: "configSiteCtrl",
+				pageConfig: "ConfigPageConfig",
+				resolve: _resolve({roleType: 'ROLE_ADMIN'})
+			})
+
+			// Application
+			.state('configApplication', {
+				url: "/config/application",
+				templateUrl: "partials/config/application.html?_=" + eagleApp._TRS(),
+				controller: "configApplicationCtrl",
+				pageConfig: "ConfigPageConfig",
+				resolve: _resolve({roleType: 'ROLE_ADMIN'})
+			})
+
+			// Feature
+			.state('configFeature', {
+				url: "/config/feature",
+				templateUrl: "partials/config/feature.html?_=" + eagleApp._TRS(),
+				controller: "configFeatureCtrl",
+				pageConfig: "ConfigPageConfig",
+				resolve: _resolve({roleType: 'ROLE_ADMIN'})
+			})
+
+			// Feature configuration page
+			.state('configFeatureDetail', $.extend({url: "/config/:feature/:page"}, {
+				templateUrl: function ($stateParams) {
+					var _htmlTemplate = featureControllerCustomizeHtmlTemplate[$stateParams.feature + "_" + $stateParams.page];
+					return  "public/feature/" + $stateParams.feature + "/page/" + (_htmlTemplate ||  $stateParams.page) + ".html?_=" + eagleApp._TRS();
+				},
+				controllerProvider: function ($stateParams) {
+					return "config_" + $stateParams.feature + "_" + $stateParams.page;
+				},
+				pageConfig: "ConfigPageConfig",
+				resolve: _resolve({roleType: 'ROLE_ADMIN'})
+			}))
+
+			// =================== Feature ===================
+			// Dynamic feature page
+			.state('page', $.extend({url: "/:feature/:page"}, _featureBase))
+			.state('pageFilter', $.extend({url: "/:feature/:page/:filter"}, _featureBase))
+		;
+
+		// Animation
+		$animateProvider.classNameFilter(/^((?!(fa-spin)).)*$/);
+		$animateProvider.classNameFilter(/^((?!(tab-pane)).)*$/);
+	});
+
+	eagleApp.filter('parseJSON', function () {
+		return function (input, defaultVal) {
+			return common.parseJSON(input, defaultVal);
+		};
+	});
+
+	eagleApp.filter('split', function () {
+		return function (input, regex) {
+			return input.split(regex);
+		};
+	});
+
+	eagleApp.filter('reverse', function () {
+		return function (items) {
+			return items.slice().reverse();
+		};
+	});
+
+	// ======================================================================================
+	// =                                   Main Controller                                  =
+	// ======================================================================================
+	eagleApp.controller('MainCtrl', function ($scope, $wrapState, $http, $injector, ServiceError, PageConfig, FeaturePageConfig, Site, Authorization, Entities, nvd3, Application, Feature, UI) {
+		window.serviceError = $scope.ServiceError = ServiceError;
+		window.site = $scope.Site = Site;
+		window.auth = $scope.Auth = Authorization;
+		window.entities = $scope.Entities = Entities;
+		window.application = $scope.Application = Application;
+		window.pageConfig = $scope.PageConfig = PageConfig;
+		window.featurePageConfig = $scope.FeaturePageConfig = FeaturePageConfig;
+		window.feature = $scope.Feature = Feature;
+		window.ui = $scope.UI = UI;
+		window.nvd3 = nvd3;
+		$scope.app = app;
+		$scope.common = common;
+
+		Object.defineProperty(window, "scope",{
+			get: function() {
+				return angular.element("[ui-view]").scope();
+			}
+		});
+
+		// Clean up
+		$scope.$on('$stateChangeStart', function (event, next, nextParam, current, currentParam) {
+			console.log("[Switch] current ->", current, currentParam);
+			console.log("[Switch] next ->", next, nextParam);
+			// Page initialization
+			PageConfig.reset();
+
+			// Dynamic navigation list
+			if(next.pageConfig) {
+				$scope.PageConfig.navConfig = $injector.get(next.pageConfig);
+			} else {
+				$scope.PageConfig.navConfig = {};
+			}
+
+			// Authorization
+			// > Login check
+			if (!common.getValueByPath(next, "access.skipCheck", false)) {
+				if (!Authorization.isLogin) {
+					console.log("[Authorization] Need access. Redirect...");
+					$wrapState.go("login");
+				}
+			}
+
+			// > Role control
+			/*var _roles = common.getValueByPath(next, "access.roles", []);
+			if (_roles.length && Authorization.userProfile.roles) {
+				var _roleMatch = false;
+				$.each(_roles, function (i, roleName) {
+					if (Authorization.isRole(roleName)) {
+						_roleMatch = true;
+						return false;
+					}
+				});
+
+				if (!_roleMatch) {
+					$wrapState.path("/dam");
+				}
+			}*/
+		});
+
+		$scope.$on('$stateChangeError', function (event, next, nextParam, current, currentParam, error) {
+			console.error("[Switch] Error", arguments);
+		});
+
+		// Get side bar navigation item class
+		$scope.getNavClass = function (page) {
+			var path = page.url.replace(/^#/, '');
+
+			if ($wrapState.path() === path) {
+				PageConfig.pageTitle = PageConfig.pageTitle || page.title;
+				return "active";
+			} else {
+				return "";
+			}
+		};
+
+		// Get side bar navigation item class visible
+		$scope.getNavVisible = function (page) {
+			if (!page.roles) return true;
+
+			for (var i = 0; i < page.roles.length; i += 1) {
+				var roleName = page.roles[i];
+				if (Authorization.isRole(roleName)) {
+					return true;
+				}
+			}
+
+			return false;
+		};
+
+		// Authorization
+		$scope.logout = function () {
+			console.log("[Authorization] Logout. Redirect...");
+			Authorization.logout();
+			$wrapState.go("login");
+		};
+	});
+})();
\ No newline at end of file


Mime
View raw message