You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@eagle.apache.org by ha...@apache.org on 2015/11/30 03:51:09 UTC

[35/44] incubator-eagle git commit: update pom using npm to install web deps

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/2c3005c9/eagle-webservice/src/main/webapp/app/public/js/ctrl/policyController.js
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/app/public/js/ctrl/policyController.js b/eagle-webservice/src/main/webapp/app/public/js/ctrl/policyController.js
new file mode 100644
index 0000000..a9af862
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/app/public/js/ctrl/policyController.js
@@ -0,0 +1,885 @@
+/*
+ * 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';
+
+
+// =============================================================
+// =                        Policy List                        =
+// =============================================================
+damControllers.controller('policyListCtrl', function(globalContent, Site, damContent, $scope, $routeParams, Entities) {
+	globalContent.setConfig(damContent.config);
+	globalContent.pageTitle = "Policy List";
+	globalContent.pageSubTitle = Site.current().name;
+	globalContent.navPath = ["Policy View", "Polict List"];
+
+	// Initial load
+	$scope.policyList = [];
+	if($routeParams.dataSource) {
+		$scope.dataSource = Site.current().dataSrcList.find($routeParams.dataSource);
+	}
+
+	// List policies
+	var _policyList = Entities.queryEntities("AlertDefinitionService", {site: Site.current().name, dataSource: $routeParams.dataSource});
+	_policyList._promise.then(function() {
+		$.each(_policyList, function(i, policy) {
+			if($.inArray(policy.tags.dataSource, app.config.dataSource.uiInvisibleList) === -1) {
+				policy.__mailStr = common.getValueByPath(common.parseJSON(policy.notificationDef, {}), "0.recipients", "");
+				policy.__mailList = policy.__mailStr.trim() === "" ? [] : policy.__mailStr.split(/[\,\;]/);
+				policy.__expression = common.parseJSON(policy.policyDef, {}).expression;
+
+				$scope.policyList.push(policy);
+			}
+		});
+	});
+	$scope.policyList._promise = _policyList._promise;
+
+	// Function
+	$scope.searchFunc = function(item) {
+		var key = $scope.search;
+		if(!key) return true;
+
+		var _key = key.toLowerCase();
+		function _hasKey(item, path) {
+			var _value = common.getValueByPath(item, path, "").toLowerCase();
+			return _value.indexOf(_key) !== -1;
+		}
+		return _hasKey(item, "tags.policyId") || _hasKey(item, "__expression") || _hasKey(item, "desc") || _hasKey(item, "owner") || _hasKey(item, "__mailStr");
+	};
+
+	$scope.updatePolicyStatus = damContent.updatePolicyStatus;
+	$scope.deletePolicy = function(policy) {
+		damContent.deletePolicy(policy, function(policy) {
+			var _index = $scope.policyList.indexOf(policy);
+			$scope.policyList.splice(_index, 1);
+		});
+	};
+});
+
+// =============================================================
+// =                       Policy Detail                       =
+// =============================================================
+damControllers.controller('policyDetailCtrl', function(globalContent, Site, damContent, charts, $scope, $routeParams, Entities) {
+	var MAX_PAGESIZE = 10000;
+
+	globalContent.setConfig(damContent.config);
+	globalContent.pageTitle = "Policy Detail";
+	globalContent.navPath = ["Policy View", "Polict List", "Polict Detail"];
+	globalContent.lockSite = true;
+
+	charts = charts($scope);
+
+	$scope.common = common;
+
+	// Query policy
+	if($routeParams.encodedRowkey) {
+		$scope.policyList = Entities.queryEntity("AlertDefinitionService", $routeParams.encodedRowkey);
+	} else {
+		$scope.policyList = Entities.queryEntities("AlertDefinitionService", {
+			policyId: $routeParams.policy,
+			site: $routeParams.site,
+			alertExecutorId: $routeParams.executor
+		});
+	}
+	$scope.policyList._promise.then(function() {
+		if($scope.policyList.length === 0) {
+			$.dialog({
+				title: "OPS!",
+				content: "Policy not found!",
+			}, function() {
+				location.href = "#/dam/policyList";
+			});
+			return;
+		} else {
+			var policy = $scope.policyList[0];
+
+			policy.__mailStr = common.getValueByPath(common.parseJSON(policy.notificationDef, {}), "0.recipients", "");
+			policy.__mailList = policy.__mailStr.trim() === "" ? [] : policy.__mailStr.split(/[\,\;]/);
+			policy.__expression = common.parseJSON(policy.policyDef, {}).expression;
+
+			$scope.policy = policy;
+			Site.current(Site.find($scope.policy.tags.site));
+			console.log($scope.policy);
+		}
+
+		// Visualization
+		var _cond = {
+			dataSource: policy.tags.dataSource,
+			policyId: policy.tags.policyId,
+			_duration: 1000 * 60 * 60 * 24 * 30,
+		};
+
+		// > eagle.policy.eval.count
+		$scope.policyEvalSeries = Entities.querySeries("GenericMetricService", $.extend({_metricName: "eagle.policy.eval.count"}, _cond), "@cluster", "sum(value)", 60 * 24);
+
+		// > eagle.policy.eval.fail.count
+		$scope.policyEvalFailSeries = Entities.querySeries("GenericMetricService", $.extend({_metricName: "eagle.policy.eval.fail.count"}, _cond), "@cluster", "sum(value)", 60 * 24);
+
+		// > eagle.alert.count
+		$scope.alertSeries = Entities.querySeries("GenericMetricService", $.extend({_metricName: "eagle.alert.count"}, _cond), "@cluster", "sum(value)", 60 * 24);
+
+		// > eagle.alert.fail.count
+		$scope.alertFailSeries = Entities.querySeries("GenericMetricService", $.extend({_metricName: "eagle.alert.fail.count"}, _cond), "@cluster", "sum(value)", 60 * 24);
+
+		// Alert list
+		$scope.alertList = Entities.queryEntities("AlertService", {
+			site: Site.current().name,
+			dataSource: policy.tags.dataSource,
+			policyId: policy.tags.policyId,
+			_pageSize: MAX_PAGESIZE,
+			_duration: 1000 * 60 * 60 * 24 * 30,
+			__ETD: 1000 * 60 * 60 * 24
+		});
+	});
+
+	// Function
+	$scope.updatePolicyStatus = damContent.updatePolicyStatus;
+	$scope.deletePolicy = function(policy) {
+		damContent.deletePolicy(policy, function(policy) {
+			location.href = "#/dam/policyList";
+		});
+	};
+});
+
+// =============================================================
+// =                        Policy Edit                        =
+// =============================================================
+(function() {
+	function policyCtrl(create, globalContent, Site, damContent, $scope, $routeParams, $location, $q, Entities) {
+		globalContent.setConfig(damContent.config);
+		globalContent.pageTitle = "Policy Edit";
+		globalContent.pageSubTitle = Site.current().name;
+		globalContent.navPath = ["Policy View", "Polict List", "Polict Edit"];
+
+		var _winTimeDesc = "Number unit[millisecond, sec, min, hour, day, month, year]. e.g. 23 sec";
+		var _winTimeRegex = /^\d+\s+(millisecond|milliseconds|second|seconds|sec|minute|minutes|min|hour|hours|day|days|week|weeks|month|months|year|years)$/;
+		var _winTimeDefaultValue = '10 min';
+		$scope.config = {
+			window: [
+				// Display name, window type, required columns[Title, Description || "LONG_FIELD", Regex check, default value]
+				{
+					title: "Message Time Slide",
+					description: "Using timestamp filed from input is used as event's timestamp",
+					type: "externalTime",
+					fields:[
+						{title: "Field", defaultValue: "timestamp", hide: true},
+						{title: "Time", description: _winTimeDesc, regex: _winTimeRegex, defaultValue: _winTimeDefaultValue},
+					]
+				},
+				{
+					title: "System Time Slide",
+					description: "Using System time is used as timestamp for event's timestamp",
+					type: "time",
+					fields:[
+						{title: "Time", description: _winTimeDesc, regex: _winTimeRegex, defaultValue: _winTimeDefaultValue},
+					]
+				},
+				{
+					title: "System Time Batch",
+					description: "Same as System Time Window except the policy is evaluated at fixed duration",
+					type: "timeBatch",
+					fields:[
+						{title: "Time", description: _winTimeDesc, regex: _winTimeRegex, defaultValue: _winTimeDefaultValue},
+					]
+				},
+				{
+					title: "Length Slide",
+					description: "The slide window has a fixed length",
+					type: "length",
+					fields:[
+						{title: "Number", description: "Number only. e.g. 1023", regex: /^\d+$/},
+					]
+				},
+				{
+					title: "Length Batch",
+					description: "Same as Length window except the policy is evaluated in batch mode when fixed event count reached",
+					type: "lengthBatch",
+					fields:[
+						{title: "Number", description: "Number only. e.g. 1023", regex: /^\d+$/},
+					]
+				},
+			],
+		};
+
+		$scope.create = create;
+		$scope.encodedRowkey = $routeParams.encodedRowkey;
+
+		$scope.step = 0;
+		$scope.dataSources = {};
+		$scope.policy = {};
+
+		// ==========================================
+		// =            Data Preparation            =
+		// ==========================================
+		// Steam list
+		var _streamList = Entities.queryEntities("AlertStreamService", '@streamName=~".*"');
+		var _executorList = Entities.queryEntities("AlertExecutorService", '@streamName=~".*"');
+		$scope.streamList = _streamList;
+		$scope.executorList = _executorList;
+		$scope.streamReady = false;
+
+		$q.all([_streamList._promise, _executorList._promise]).then(function() {
+			// Map executor with stream
+			$.each(_executorList, function(i, executor) {
+				$.each(_streamList, function(j, stream) {
+					if(stream.tags.dataSource === executor.tags.dataSource && stream.tags.streamName === executor.tags.streamName) {
+						stream.alertExecutor = executor;
+						return false;
+					}
+				});
+			});
+
+			// Fill stream list
+			$.each(_streamList, function(i, unit) {
+				var _srcStreamList = $scope.dataSources[unit.tags.dataSource] = $scope.dataSources[unit.tags.dataSource] || [];
+				_srcStreamList.push(unit);
+			});
+
+			$scope.streamReady = true;
+
+			// ==========================================
+			// =                Function                =
+			// ==========================================
+			function _findStream(dataSource, streamName) {
+				var _streamList = $scope.dataSources[dataSource];
+				if(!_streamList) return null;
+
+				for(var i = 0 ; i < _streamList.length ; i += 1) {
+					if(_streamList[i].tags.streamName === streamName) {
+						return _streamList[i];
+					}
+				}
+				return null;
+			};
+
+			// ==========================================
+			// =              Step control              =
+			// ==========================================
+			$scope.steps = [
+				// >> Select data source
+				{
+					title: "Select Data Source",
+					ready: function() {
+						return $scope.streamReady;
+					},
+					init: function() {
+						if(create) $scope.policy.tags.dataSource = $scope.policy.tags.dataSource || Site.current().dataSrcList[0].tags.dataSource;
+					},
+					nextable: function() {
+						return common.getValueByPath($scope.policy, "tags.dataSource");
+					},
+				},
+
+				// >> Select stream
+				{
+					title: "Select Stream",
+					ready: function() {
+						return $scope.streamReady;
+					},
+					init: function() {
+						$scope.policy.__.streamName = $scope.policy.__.streamName ||
+							common.array.find($scope.policy.tags.dataSource, _streamList, "tags.dataSource").tags.streamName;
+					},
+					nextable: function() {
+						var _streamName = common.getValueByPath($scope.policy, "__.streamName");
+						if(!_streamName) return false;
+
+						// Detect stream in current data source list
+						return !!common.array.find(_streamName, $scope.dataSources[$scope.policy.tags.dataSource], "tags.streamName");
+					},
+				},
+
+				// >> Define Alert Policy
+				{
+					title: "Define Alert Policy",
+					init: function() {
+						// Normal mode will fetch meta list
+						if(!$scope.policy.__.advanced) {
+							var _stream = _findStream($scope.policy.tags.dataSource, $scope.policy.__.streamName);
+							$scope._stream = _stream;
+
+							if(!_stream.metas) {
+								_stream.metas = Entities.queryEntities("AlertStreamSchemaService", {dataSource: $scope.policy.tags.dataSource, streamName: $scope.policy.__.streamName});
+								_stream.metas._promise.then(function() {
+									_stream.metas.sort(function(a, b) {
+										if(a.tags.attrName < b.tags.attrName) {
+											return -1;
+										} else if(a.tags.attrName > b.tags.attrName) {
+											return 1;
+										}
+										return 0;
+									});
+								});
+							}
+						}
+					},
+					ready: function() {
+						if(!$scope.policy.__.advanced) {
+							return $scope._stream.metas._promise.$$state.status === 1;
+						}
+						return true;
+					},
+					nextable: function() {
+						if($scope.policy.__.advanced) {
+							// Check stream source
+							$scope._stream = null;
+							$.each($scope.dataSources[$scope.policy.tags.dataSource], function(i, stream) {
+								if(($scope.policy.__._expression || "").indexOf(stream.tags.streamName) !== -1) {
+									$scope._stream = stream;
+									return false;
+								}
+							});
+							return $scope._stream;
+						} else {
+							// Window
+							if($scope.policy.__.windowConfig) {
+								var _winMatch = true;
+								var _winConds = $scope.getWindow().fields;
+								$.each(_winConds, function(i, cond) {
+									if(!(cond.val || "").match(cond.regex)) {
+										_winMatch = false;
+										return false;
+									}
+								});
+								if(!_winMatch) return false;
+
+								// Aggregation
+								if($scope.policy.__.groupAgg) {
+									if(!$scope.policy.__.groupAggPath ||
+										!$scope.policy.__.groupCondOp ||
+										!$scope.policy.__.groupCondVal) {
+										return false;
+									}
+								}
+							}
+						}
+						return true;
+					},
+				},
+
+				// >> Configuration & Notification
+				{
+					title: "Configuration & Notification",
+					nextable: function() {
+						return !!$scope.policy.tags.policyId;
+					},
+				}
+			];
+
+			// ==========================================
+			// =              Policy Logic              =
+			// ==========================================
+			_streamList._promise.then(function() {
+				// Initial policy entity
+				if(create) {
+					$scope.policy = {
+						__: {
+							toJSON: jQuery.noop,
+							conditions: {},
+							notification: [],
+							dedupe: {
+								alertDedupIntervalMin: 10,
+								emailDedupIntervalMin: 10,
+							},
+							policy: {},
+							window: "externalTime",
+							group: "",
+							groupAgg: "count",
+							groupAggPath: "timestamp",
+							groupCondOp: ">=",
+							groupCondVal: "2",
+						},
+						desc: "",
+						enabled: true,
+						prefix: "alertdef",
+						remediationDef: "",
+						tags: {
+							policyType: "siddhiCEPEngine",
+						}
+					};
+
+					// If configured data source
+					if($routeParams.dataSrc) {
+						$scope.policy.tags.dataSource = $routeParams.dataSrc;
+						if(common.array.find($routeParams.dataSrc, Site.current().dataSrcList, "tags.dataSource")) {
+							setTimeout(function() {
+								$scope.changeStep(0, 2, false);
+								$scope.$apply();
+							}, 1);
+						}
+					}
+				} else {
+					var _policy = Entities.queryEntity("AlertDefinitionService", $scope.encodedRowkey);
+					_policy._promise.then(function() {
+						if(_policy.length) {
+							$scope.policy = _policy[0];
+							$scope.policy.__ = {
+								toJSON: jQuery.noop,
+							};
+
+							Site.current(Site.find($scope.policy.tags.site));
+						} else {
+							$.dialog({
+								title: "OPS",
+								content: "Policy not found!",
+							}, function() {
+								$location.path("/dam/policyList");
+								$scope.$apply();
+							});
+							return;
+						}
+
+						// === Revert inner data ===
+						// >> De-dupe
+						$scope.policy.__.dedupe = common.parseJSON($scope.policy.dedupeDef, {});
+
+						// >> Notification
+						$scope.policy.__.notification = common.parseJSON($scope.policy.notificationDef, []);
+
+						// >> Policy
+						var _policyUnit = $scope.policy.__.policy = common.parseJSON($scope.policy.policyDef);
+
+						// >> Parse expression
+						$scope.policy.__.conditions = {};
+						var _condition = _policyUnit.expression.match(/from\s+(\w+)(\[(.*)\])?(#window[^\)]*\))?\s+(select (\w+\, )?(\w+)\((\w+)\) as aggValue (group by (\w+) )?having aggValue ([<>=]+) ([^\s]+))?/);
+						var _cond_stream = _condition[1];
+						var _cond_query = _condition[3] || "";
+						var _cond_window = _condition[4];
+						var _cond_group = _condition[5];
+						var _cond_groupUnit = _condition.slice(7,13);
+
+						if(!_condition) {
+							$scope.policy.__.advanced = true;
+						} else {
+							// > StreamName
+							var _streamName = _cond_stream;
+							var _cond = _cond_query;
+
+							$scope.policy.__.streamName = _streamName;
+
+							//  > Conditions
+							// Loop condition groups
+							if(_cond.trim() !== "" && /^\(.*\)$/.test(_cond)) {
+								var _condGrps = _cond.substring(1, _cond.length - 1).split(/\)\s+and\s+\(/);
+								$.each(_condGrps, function(i, line) {
+									// Loop condition cells
+									var _condCells = line.split(/\s+or\s+/);
+									$.each(_condCells, function(i, cell) {
+										var _opMatch = cell.match(/(\S*)\s*(==|!=|>|<|>=|<=|contains)\s*('(?:[^'\\]|\\.)*'|[\w\d]+)/);
+										if(!common.isEmpty(_opMatch)) {
+											var _key = _opMatch[1];
+											var _op = _opMatch[2];
+											var _val = _opMatch[3];
+											var _conds = $scope.policy.__.conditions[_key] = $scope.policy.__.conditions[_key] || [];
+											var _type = "";
+											if(_val.match(/\'.*\'/)) {
+												_val = _val.slice(1, -1);
+												_type = "string";
+											} else if(_val === "true" || _val === "false") {
+												var _regexMatch = _key.match(/^str:regexp\((\w+),'(.*)'\)/);
+												var _containsMatch = _key.match(/^str:contains\((\w+),'(.*)'\)/);
+												var _mathes = _regexMatch || _containsMatch;
+												if(_mathes) {
+													_key = _mathes[1];
+													_val = _mathes[2];
+													_type = "string";
+													_op = _regexMatch ? "regex" : "contains";
+													_conds = $scope.policy.__.conditions[_key] = $scope.policy.__.conditions[_key] || [];
+												} else {
+													_type = "bool";
+												}
+											} else {
+												_type = "number";
+											}
+											_conds.push($scope._CondUnit(_key, _op, _val, _type));
+										}
+									});
+								});
+							} else if(_cond_query !== "") {
+								$scope.policy.__.advanced = true;
+							}
+						}
+
+						if($scope.policy.__.advanced) {
+							$scope.policy.__._expression = _policyUnit.expression;
+						} else {
+							// > window
+							if(!_cond_window) {
+								$scope.policy.__.window = "externalTime";
+								$scope.policy.__.group = "";
+								$scope.policy.__.groupAgg = "count";
+								$scope.policy.__.groupAggPath = "timestamp";
+								$scope.policy.__.groupCondOp = ">=";
+								$scope.policy.__.groupCondVal = "2";
+							} else {
+								try {
+									$scope.policy.__.windowConfig = true;
+
+									var _winCells = _cond_window.match(/\.(\w+)\((.*)\)/);
+									$scope.policy.__.window = _winCells[1];
+									var _winConds = $scope.getWindow().fields;
+									$.each(_winCells[2].split(","), function(i, val) {
+										_winConds[i].val = val;
+									});
+
+									// Group
+									if(_cond_group) {
+										$scope.policy.__.group = _cond_groupUnit[3];
+										$scope.policy.__.groupAgg = _cond_groupUnit[0];
+										$scope.policy.__.groupAggPath = _cond_groupUnit[1];
+										$scope.policy.__.groupCondOp = _cond_groupUnit[4];
+										$scope.policy.__.groupCondVal = _cond_groupUnit[5];
+									} else {
+										$scope.policy.__.group = "";
+										$scope.policy.__.groupAgg = "count";
+										$scope.policy.__.groupAggPath = "timestamp";
+										$scope.policy.__.groupCondOp = ">=";
+										$scope.policy.__.groupCondVal = "2";
+									}
+								} catch(err) {
+									$scope.policy.__.window = "externalTime";
+								}
+							}
+						}
+
+						$scope.changeStep(0, 3, false);
+					});
+				}
+
+				// Start step
+				$scope.changeStep(0, 1, false);
+
+				console.log($scope.policy, $scope);
+			});
+
+			// ==========================================
+			// =                Function                =
+			// ==========================================
+			// UI: Highlight select step
+			$scope.stepSelect = function(step) {
+				return step === $scope.step ? "active" : "";
+			};
+
+			// UI: Collapse all
+			$scope.collapse = function(cntr) {
+				var _list = $(cntr).find(".collapse").css("height", "auto");
+				if(_list.hasClass("in")) {
+					_list.removeClass("in");
+				} else {
+					_list.addClass("in");
+				}
+			};
+
+			// Step process. Will fetch target step attribute and return boolean
+			function _check(key, step) {
+				var _step = $scope.steps[step - 1];
+				if(!_step) return;
+
+				var _value = _step[key];
+				if(typeof _value === "function") {
+					return _value();
+				} else if(typeof _value === "boolean") {
+					return _value;
+				}
+				return true;
+			}
+			// Check step is ready. Otherwise will display load animation
+			$scope.stepReady = function(step) {
+				return _check("ready", step);
+			};
+			// Check whether process next step. Otherwise will disable next button
+			$scope.checkNextable = function(step) {
+				return !_check("nextable", step);
+			};
+			// Switch step
+			$scope.changeStep = function(step, targetStep, check) {
+				if(check === false || _check("checkStep", step)) {
+					$scope.step =  targetStep;
+
+					_check("init", targetStep);
+				}
+			};
+
+			// Window
+			$scope.getWindow = function() {
+				if(!$scope.policy || !$scope.policy.__) return null;
+				var _window = common.array.find($scope.policy.__.window, $scope.config.window, "type");
+				return _window;
+			};
+
+			// Aggregation
+			$scope.groupAggPathList = function() {
+				return $.grep(common.getValueByPath($scope, "_stream.metas", []), function(meta) {
+					return $.inArray(meta.attrType, ['long','integer','number']) !== -1;
+				});
+			};
+
+			$scope.updateGroupAgg = function() {
+				$scope.policy.__.groupAggPath = $scope.policy.__.groupAggPath || common.getValueByPath($scope.groupAggPathList()[0], "tags.attrName");
+
+				if($scope.policy.__.groupAgg === 'count') {
+					$scope.policy.__.groupAggPath = 'timestamp';
+				}
+			};
+
+			// Resolver
+			$scope.resolverTypeahead = function(value, resolver) {
+				var _resolverList = Entities.query("stream/attributeresolve", {
+					site: Site.current().name,
+					resolver: resolver,
+					query: value
+				});
+				return _resolverList._promise.then(function() {
+					return _resolverList;
+				});
+			};
+
+			// Used for input box when pressing enter
+			$scope.conditionPress = function(event, key, operation, value, type) {
+				if(event.which == 13) {
+					setTimeout(function() {
+						$(event.currentTarget).closest(".input-group").find("button").click();
+					}, 1);
+				}
+			};
+			// Check whether has condition
+			$scope.hasCondition = function(key, type) {
+				var _list = common.getValueByPath($scope.policy.__.conditions, key, []);
+				if(_list.length === 0) return false;
+
+				if(type === "bool") {
+					return !_list[0].ignored();
+				}
+				return true;
+			};
+			// Condition unit definition
+			$scope._CondUnit = function(key, op, value, type) {
+				var _obj = {
+					key: key,
+					op: op,
+					val: value,
+					type: type,
+					ignored: function() {
+						if(this.type === "bool" && this.val === "none") {
+							return true;
+						}
+						return false;
+					},
+					getVal: function() {
+						return this.type === "string" ? "'" + this.val + "'" : this.val;
+					},
+					toString: function() {
+						return this.op + " " + this.getVal();
+					},
+					toCondString: function() {
+						var _op = this.op === "=" ? "==" : this.op;
+						if(_op === "regex") {
+							return "str:regexp(" + this.key + "," + this.getVal() + ")==true";
+						} else if(_op === "contains") {
+							return "str:contains(" + this.key + "," + this.getVal() + ")==true";
+						} else {
+							return this.key + " " + _op + " " + this.getVal();
+						}
+					},
+				};
+				return _obj;
+			};
+			// Add condition for policy
+			$scope.addCondition = function(key, op, value, type) {
+				if(value === "" || value === undefined) return false;
+
+				var _condList = $scope.policy.__.conditions[key] = $scope.policy.__.conditions[key] || [];
+				_condList.push($scope._CondUnit(key, op, value, type));
+				return true;
+			};
+			// Convert condition list to description string
+			$scope.parseConditionDesc = function(key) {
+				return $.map($scope.policy.__.conditions[key] || [], function(cond) {
+					if(!cond.ignored()) return "[" + cond.toString() + "]";
+				}).join(" or ");
+			};
+
+			// To query
+			$scope.toQuery = function() {
+				if(!$scope.policy.__) return "";
+
+				if($scope.policy.__.advanced) return $scope.policy.__._expression;
+
+				// > Query
+				var _query = $.map(common.getValueByPath($scope.policy, "__.conditions", {}), function(list, key) {
+					var _conds = $.map(list, function(cond) {
+						if(!cond.ignored()) return cond.toCondString();
+					});
+					if(_conds.length) {
+						return "(" + _conds.join(" or ") + ")";
+					}
+				}).join(" and ");
+				if(_query) {
+					_query = "[" + _query + "]";
+				}
+
+				// > Window
+				var _window = $scope.getWindow();
+				var _windowStr = "";
+				if($scope.policy.__.windowConfig) {
+					_windowStr = $.map(_window.fields, function(field) {
+						return field.val;
+					}).join(",");
+					_windowStr = "#window." + _window.type + "(" + _windowStr + ")";
+
+					// > Group
+					if($scope.policy.__.group) {
+						_windowStr += common.template(" select ${group}, ${groupAgg}(${groupAggPath}) as aggValue group by ${group} having aggValue ${groupCondOp} ${groupCondVal}", {
+							group: $scope.policy.__.group,
+							groupAgg: $scope.policy.__.groupAgg,
+							groupAggPath: $scope.policy.__.groupAggPath,
+							groupCondOp: $scope.policy.__.groupCondOp,
+							groupCondVal: $scope.policy.__.groupCondVal,
+						});
+					} else {
+						_windowStr += common.template(" select ${groupAgg}(${groupAggPath}) as aggValue having aggValue ${groupCondOp} ${groupCondVal}", {
+							groupAgg: $scope.policy.__.groupAgg,
+							groupAggPath: $scope.policy.__.groupAggPath,
+							groupCondOp: $scope.policy.__.groupCondOp,
+							groupCondVal: $scope.policy.__.groupCondVal,
+						});
+					}
+				} else {
+					_windowStr = " select *";
+				}
+
+				return common.template("from ${stream}${query}${window} insert into outputStream;", {
+					stream: $scope.policy.__.streamName,
+					query: _query,
+					window: _windowStr,
+				});
+			};
+
+			// ==========================================
+			// =             Update Policy              =
+			// ==========================================
+			// dedupeDef notificationDef policyDef
+			$scope.finishPolicy = function() {
+				$scope.lock = true;
+
+				// dedupeDef
+				$scope.policy.dedupeDef = JSON.stringify($scope.policy.__.dedupe);
+
+				// notificationDef
+				$scope.policy.__.notification = $scope.policy.__.notification || [];
+				var _notificationUnit = $scope.policy.__.notification[0];
+				if(_notificationUnit) {
+					_notificationUnit.flavor = "email";
+					_notificationUnit.id = "email_1";
+					_notificationUnit.tplFileName = "";
+				}
+				$scope.policy.notificationDef = JSON.stringify($scope.policy.__.notification);
+
+				// policyDef
+				$scope.policy.__._dedupTags = $scope.policy.__._dedupTags || {};
+				$scope.policy.__.policy = {
+					expression: $scope.toQuery(),
+					type: "siddhiCEPEngine"
+				};
+				$scope.policy.policyDef = JSON.stringify($scope.policy.__.policy);
+
+				// alertExecutorId
+				if($scope._stream.alertExecutor) {
+					$scope.policy.tags.alertExecutorId = $scope._stream.alertExecutor.tags.alertExecutorId;
+				} else {
+					$scope.lock = false;
+					$.dialog({
+						title: "OPS!",
+						content: "Alert Executor not defined! Please check 'AlertExecutorService'!"
+					});
+					return;
+				}
+
+				// site
+				$scope.policy.tags.site = $scope.policy.tags.site || Site.current().name;
+
+				// Update function
+				function _updatePolicy() {
+					Entities.updateEntity("AlertDefinitionService", $scope.policy)._promise.success(function(data) {
+						$scope.create = create = false;
+						$scope.encodedRowkey = data.obj[0];
+
+						$.dialog({
+							title: "Success",
+							content: (create ? "Create" : "Update") + " success!",
+						}, function() {
+							if(data.success) {
+								Site.url(Site.find($scope.policy.tags.site), "/dam/policyList");
+							} else {
+								$.dialog({
+									title: "OPS",
+									content: (create ? "Create" : "Update") + "failed!" + JSON.stringify(data),
+								});
+							}
+						});
+					}).error(function(data) {
+						$.dialog({
+							title: "OPS",
+							content: (create ? "Create" : "Update") + "failed!" + JSON.stringify(data),
+						});
+					}).then(function() {
+						$scope.lock = false;
+					});
+				}
+
+				// Check if already exist
+				if($scope.create) {
+					var _checkList = Entities.queryEntities("AlertDefinitionService", {
+						alertExecutorId: $scope.policy.tags.alertExecutorId,
+						policyId: $scope.policy.tags.policyId,
+						policyType: "siddhiCEPEngine",
+						dataSource: $scope.policy.tags.dataSource,
+					});
+					_checkList._promise.then(function() {
+						if(_checkList.length) {
+							$.dialog({
+								title: "Override Confirm",
+								content: "Already exists PolicyID '" + $scope.policy.tags.policyId + "'. Do you want to override?",
+								confirm: true
+							}, function(ret) {
+								if(ret) {
+									_updatePolicy();
+								} else {
+									$scope.lock = false;
+									$scope.$apply();
+								}
+							});
+						} else {
+							_updatePolicy();
+						}
+					});
+				} else {
+					_updatePolicy();
+				}
+			};
+		});
+	}
+
+	damControllers.controller('policyCreateCtrl', function(globalContent, Site, damContent, $scope, $routeParams, $location, $q, Entities) {
+		policyCtrl(true, globalContent, Site, damContent, $scope, $routeParams, $location, $q, Entities);
+	});
+	damControllers.controller('policyEditCtrl', function(globalContent, Site, damContent, $scope, $routeParams, $location, $q, Entities) {
+		globalContent.lockSite = true;
+		policyCtrl(false, globalContent, Site, damContent, $scope, $routeParams, $location, $q, Entities);
+	});
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/2c3005c9/eagle-webservice/src/main/webapp/app/public/js/ctrl/sensitivityController.js
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/app/public/js/ctrl/sensitivityController.js b/eagle-webservice/src/main/webapp/app/public/js/ctrl/sensitivityController.js
new file mode 100644
index 0000000..0dd5ea5
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/app/public/js/ctrl/sensitivityController.js
@@ -0,0 +1,389 @@
+/*
+ * 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';
+
+// =============================================================
+// =                    Sensitivity Summary                    =
+// =============================================================
+damControllers.controller('sensitivitySummaryCtrl', function(globalContent, Site, damContent, $scope, Entities) {
+	globalContent.setConfig(damContent.config);
+	globalContent.pageTitle = "Data Classification";
+	globalContent.pageSubTitle = Site.current().name;
+
+	$scope._sensitivitySource = null;
+	$scope._sensitivityLock = false;
+	$scope._sensitivityImportType = "By Text";
+	$scope._sensitivityFile = "";
+	$scope._sensitivityData = "";
+	$scope._sensitivityError = "";
+	$scope.sensitivityList = [];
+
+	// TODO: Only support Hive & HDFS
+	if(common.array.find("hdfsAuditLog", Site.current().dataSrcList, "tags.dataSource")) {
+		$scope.sensitivityList.push(
+			{name: "HDFS", service: "FileSensitivityService", keys: ["filedir", "sensitivityType"]}
+		);
+	}
+	if(common.array.find("hiveQueryLog", Site.current().dataSrcList, "tags.dataSource")) {
+		$scope.sensitivityList.push(
+			{name: "Hive", service: "HiveResourceSensitivityService", keys: ["hiveResource", "sensitivityType"]}
+		);
+	}
+
+	function _refreshStatistic(entity) {
+		if(entity) {
+				entity.statisitc = Entities.queryGroup(entity.service, {site: Site.current().name}, "@site", "count");
+		} else {
+			$.each($scope.sensitivityList, function(i, entity) {
+				entity.statisitc = Entities.queryGroup(entity.service, {site: Site.current().name}, "@site", "count");
+			});
+		}
+	}
+	_refreshStatistic();
+
+	// Import sensitivity data
+	$scope.showImportEditor = function(entity) {
+		$scope._sensitivitySource = entity;
+
+		$("#sensitivityMDL").modal('show');
+		setTimeout(function() {
+			$("#sensitivityData").focus();
+		}, 500);
+	};
+
+	$scope.confirmImport = function() {
+		if(!$scope._sensitivitySource) return;
+
+		$scope._sensitivityLock = true;
+
+		// Post data
+		switch($scope._sensitivityImportType) {
+		case "By Text":
+		// > By Text
+			Entities.updateEntity($scope._sensitivitySource.service, 
+				common.parseJSON($scope._sensitivityData, null),
+				 {timestamp: false})._promise.success(function(data) {
+				if(!Entities.dialog(data)) {
+					$("#sensitivityMDL").modal('hide');
+					$scope._sensitivityData = "";
+	
+					_refreshStatistic($scope._sensitivitySource);
+				}
+			}).finally(function() {
+				$scope._sensitivityLock = false;
+			});
+			break;
+
+		case "By File":
+		// > By File
+			var formData = new FormData();
+			formData.append("file", $("#sensitivityFile")[0].files[0]);
+			formData.append("site", Site.current().name);
+
+			$.ajax({
+				url : app.getURL("updateEntity", {serviceName: $scope._sensitivitySource.service}),
+				data : formData,
+				cache : false,
+				contentType : false,
+				processData : false,
+				type : 'POST',
+			}).done(function(data) {
+				if(!Entities.dialog(data)) {
+					$("#sensitivityMDL").modal('hide');
+					$scope._sensitivityFile = "";
+
+					_refreshStatistic($scope._sensitivitySource);
+				}
+			}).always(function() {
+				$scope._sensitivityLock = false;
+				$scope.$apply();
+			});
+		}
+	};
+
+	$scope.importCheck = function() {
+		if($scope._sensitivityLock) return false;
+		$scope._sensitivityError = "";
+
+		switch($scope._sensitivityImportType) {
+		case "By Text":
+			if(!$scope._sensitivityData) return false;
+
+			var _list = common.parseJSON($scope._sensitivityData, null);
+			if(!_list) {
+				$scope._sensitivityError = "Can't parse json";
+			} else if(!$.isArray(_list)) {
+				$scope._sensitivityError = "Must be array";
+			} else if(_list.length === 0) {
+				$scope._sensitivityError = "Please provide at least one sensitivity item";
+			}
+			break;
+		case "By File":
+			if(!$scope._sensitivityFile) return false;
+			break;
+		}
+		return !$scope._sensitivityError;
+	};
+
+	// Manage sensitivity data
+	$scope.showManagementEditor = function(entity) {
+		$scope._sensitivitySource = entity;
+		$("#sensitivityListMDL").modal('show');
+
+		entity.list = Entities.queryEntities(entity.service, {site: Site.current().name});
+	};
+
+	$scope.deleteItem = function(item) {
+		$.dialog({
+			title: "Delete Confirm",
+			content: "Do you want to delete '" + item.tags[$scope._sensitivitySource.keys[0]] + "'?",
+			buttons: [
+				{name: "Delete", class: "btn btn-danger", value: true},
+				{name: "Cancel", class: "btn btn-default", value: false},
+			]
+		}, function(ret) {
+			if(!ret) return;
+
+			common.array.remove(item, $scope._sensitivitySource.list);
+			Entities.deleteEntity($scope._sensitivitySource.service, item)._promise.then(function() {
+				_refreshStatistic($scope._sensitivitySource);
+			});
+
+
+			$scope.$apply();
+		});
+	};
+
+	$scope.deleteAll = function(entity) {
+		$.dialog({
+			title: "Delete Confirm",
+			content: "<span class='text-red fa fa-exclamation-triangle pull-left' style='font-size: 50px;'></span>" +
+					"<p>You are <strong class='text-red'>DELETING</strong> all the sensitivity data from '" + entity.name + "'!</p>" +
+					"<p>Proceed to delete?</p>",
+			buttons: [
+				{name: "Delete", class: "btn btn-danger", value: true},
+				{name: "Cancel", class: "btn btn-default", value: false},
+			]
+		}, function(ret) {
+			if(!ret) return;
+
+			Entities.deleteEntities(entity.service, {
+				site: Site.current().name
+			})._promise.then(function() {
+				_refreshStatistic(entity);
+			});
+
+			entity.list.splice(0);
+			$scope.$apply();
+		});
+	};
+});
+
+// =============================================================
+// =                        Sensitivity                        =
+// =============================================================
+damControllers.controller('sensitivityCtrl', function(globalContent, Site, damContent, $scope, $q, $routeParams, $location, Entities) {
+	globalContent.setConfig(damContent.config);
+	globalContent.pageTitle = "Data Classification";
+	globalContent.pageSubTitle = Site.current().name;
+
+	$scope.sensitivityList = [];
+	$scope.type = $routeParams.type;
+	$scope.ajaxId = Math.random();
+
+	// TODO: Only support Hive & HDFS
+	if(common.array.find("hdfsAuditLog", Site.current().dataSrcList, "tags.dataSource")) {
+		$scope.sensitivityList.push(
+			{name: "HDFS"}
+		);
+	}
+	if(common.array.find("hiveQueryLog", Site.current().dataSrcList, "tags.dataSource")) {
+		$scope.sensitivityList.push(
+			{name: "Hive"}
+		);
+	}
+
+	// Update search
+	$scope.$watch("type", function(value) {
+		$location.search("type", value);
+	});
+});
+
+// =============================================================
+// =                     Sensitivity: HDFS                     =
+// =============================================================
+damControllers.controller('sensitivityHDFSCtrl', function(globalContent, Site, damContent, $scope, $location, $q, Entities) {
+	$scope.path = $location.search().path || "/";
+	$scope.pathUnitList = [];
+
+	$scope.items = [];
+
+	/*$scope.$parent.$on("tab-change", function(event, pane) {
+		$location.search("path", pane.title === "HDFS" ? $scope.path : null);
+	});*/
+
+	// Mark sensitivity
+	$scope._oriItem = {};
+	$scope._markItem = {};
+
+	$scope.markSensitivity = function(item) {
+		$scope._oriItem = item;
+		$scope._markItem = {
+			prefix: "fileSensitivity",
+			tags: {
+				site: Site.current().name,
+				filedir: item.resource
+			},
+			sensitivityType: ""
+		};
+		$("#sensitivityHdfsMDL").modal();
+		setTimeout(function() {
+			$("#hdfsSensitiveType").focus();
+		}, 500);
+	};
+	$scope.confirmUpateSensitivity = function() {
+		$scope._oriItem.sensitiveType = $scope._markItem.sensitivityType;
+		var _promise = Entities.updateEntity("FileSensitivityService", $scope._markItem, {timestamp: false})._promise.success(function(data) {
+			Entities.dialog(data);
+		});
+		$("#sensitivityHdfsMDL").modal('hide');
+	};
+	$scope.unmarkSensitivity = function(item) {
+		$.dialog({
+			title: "Unmark Confirm",
+			content: "Do you want to remove the sensitivity mark on '" + item.resource + "'?",
+			confirm: true
+		}, function(ret) {
+			if(!ret) return;
+
+			Entities.deleteEntities("FileSensitivityService", {
+				site: Site.current().name,
+				filedir: item.resource
+			});
+
+			item.sensitiveType = null;
+			$scope.$apply();
+		});
+	};
+
+	// Path
+	function _refreshPathUnitList(_path) {
+		var _start,_current, _unitList = [];
+		var _path = _path + (_path.match(/\/$/) ? "" : "/");
+		for(_current = _start = 0 ; _current < _path.length ; _current += 1) {
+			if(_path[_current] === "/") {
+				_unitList.push({
+					name: _path.substring(_start, _current + (_current === 0 ? 1 : 0)),
+					path: _path.substring(0, _current === 0 ? 1 : _current),
+				});
+				_start = _current + 1;
+			}
+		}
+		$scope.pathUnitList = _unitList;
+	};
+
+	// Item
+	$scope.updateItems = function(path) {
+		if(path) $scope.path = path;
+
+		$scope.items = Entities.query("hdfsResource", {site: Site.current().name, path: $scope.path});
+		$scope.items._promise.success(function(data) {
+			var $dlg = Entities.dialog(data, function() {
+				if($scope.path !== "/") $scope.updateItems("/");
+			});
+		});
+		_refreshPathUnitList($scope.path);
+
+		//$location.search("path", $scope.path);
+	};
+
+	$scope.getFileName = function(item) {
+		return (item.resource + "").replace(/^.*\//, "");
+	};
+
+	$scope.updateItems($scope.path);
+});
+
+// =============================================================
+// =                     Sensitivity: Hive                     =
+// =============================================================
+damControllers.controller('sensitivityHiveCtrl', function(globalContent, Site, damContent, $scope, $q, Entities) {
+	$scope.table = null;
+
+	$scope.databases = Entities.query("hiveResource/databases", {site: Site.current().name});
+	$scope.loadTables = function(database) {
+		if(database.tables) return;
+		database.tables = Entities.query("hiveResource/tables", {site: Site.current().name, database: database.database});
+	};
+
+	$scope.loadColumns = function(database, table) {
+		$scope.table = table;
+
+		if(table.columns) return;
+		table.columns = Entities.query("hiveResource/columns", {site: Site.current().name, database: database.database, table: table.table});
+	};
+
+	// Mark sensitivity
+	$scope._oriItem = {};
+	$scope._markItem = {};
+
+	$scope.markSensitivity = function(item, event) {
+		if(event) event.stopPropagation();
+
+		$scope._oriItem = item;
+		$scope._markItem = {
+			prefix: "hiveResourceSensitivity",
+			tags: {
+				site: Site.current().name,
+				hiveResource: item.resource
+			},
+			sensitivityType: ""
+		};
+		$("#sensitivityHiveMDL").modal();
+		setTimeout(function() {
+			$("#hiveSensitiveType").focus();
+		}, 500);
+	};
+	$scope.confirmUpateSensitivity = function() {
+		$scope._oriItem.sensitiveType = $scope._markItem.sensitivityType;
+		var _promise = Entities.updateEntity("HiveResourceSensitivityService", $scope._markItem, {timestamp: false})._promise.success(function(data) {
+			Entities.dialog(data);
+		});
+		$("#sensitivityHiveMDL").modal('hide');
+	};
+	$scope.unmarkSensitivity = function(item, event) {
+		if(event) event.stopPropagation();
+
+		$.dialog({
+			title: "Unmark Confirm",
+			content: "Do you want to remove the sensitivity mark on '" + item.resource + "'?",
+			confirm: true
+		}, function(ret) {
+			if(!ret) return;
+
+			Entities.deleteEntities("HiveResourceSensitivityService", {
+				site: Site.current().name,
+				hiveResource: item.resource
+			});
+
+			item.sensitiveType = null;
+			$scope.$apply();
+		});
+	};
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/2c3005c9/eagle-webservice/src/main/webapp/app/public/js/ctrl/siteController.js
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/app/public/js/ctrl/siteController.js b/eagle-webservice/src/main/webapp/app/public/js/ctrl/siteController.js
new file mode 100644
index 0000000..a7228bd
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/app/public/js/ctrl/siteController.js
@@ -0,0 +1,209 @@
+/*
+ * 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';
+
+// =============================================================
+// =                         Site List                         =
+// =============================================================
+damControllers.controller('siteListCtrl', function(globalContent, Site, damContent, $scope, $q, Entities) {
+	globalContent.setConfig(damContent.config);
+	globalContent.hideSite = true;
+
+	$scope._siteEntity;
+	$scope._siteEntityLock;
+
+	$scope._dataSrcEntity;
+	$scope._dataSrcEntityLock;
+
+	$scope.dataSrcList = Entities.queryGroup("AlertStreamService", '@dataSource=~".*"', "@dataSource", "count");
+
+	// Policy statistic
+	$scope.policyStatistic = Entities.queryGroup("AlertDefinitionService", '@dataSource=~".*"', "@site,@dataSource", "count");
+	$scope.getPolicyCount = function(site, dataSource) {
+		for(var i = 0 ; i < $scope.policyStatistic.length ; i += 1) {
+			var _cur = $scope.policyStatistic[i];
+			if(_cur.key[0] === site && _cur.key[1] === dataSource) {
+				return _cur.value[0];
+			}
+		}
+		return 0;
+	};
+
+	// Alert statistic
+	$scope.alertStatistic = Entities.queryGroup("AlertService", {_duration: 1000 * 60 * 60 * 24 * 30}, "@site,@dataSource", "count");
+	$scope.getAlertCount = function(site, dataSource) {
+		for(var i = 0 ; i < $scope.alertStatistic.length ; i += 1) {
+			var _cur = $scope.alertStatistic[i];
+			if(_cur.key[0] === site && _cur.key[1] === dataSource) {
+				return _cur.value[0];
+			}
+		}
+		return 0;
+	};
+
+	// =========================================== Site ===========================================
+	$scope.showSiteEditor = function(site) {
+		$("#siteMDL").modal("show");
+		setTimeout(function() {
+			$("#siteName").focus();
+		}, 500);
+
+		$scope._siteEntity = {
+			dataSrcList: {}
+		};
+		$.each($scope.dataSrcList, function(i, item) {
+			$scope._siteEntity.dataSrcList[item.key[0]] = {
+				name: item.key[0],
+				enabled: false
+			};
+		});
+
+		if(site) {
+			$scope._siteEntity.srcSite = site;
+			$scope._siteEntity.name = site.name;
+
+			$.each(site.dataSrcList, function(i, dataSrc) {
+				$scope._siteEntity.dataSrcList[dataSrc.tags.dataSource].enabled = dataSrc.enabled === undefined ? true : dataSrc.enabled;
+			});
+		}
+	};
+	$scope.checkUpdateSite = function() {
+		if(!$scope._siteEntity || !$scope._siteEntity.dataSrcList) return false;
+
+		var _hasDataSrc = !!common.array.find(true, common.map.toArray($scope._siteEntity.dataSrcList), "enabled");
+		return $scope._siteEntity.name && _hasDataSrc && !$scope._siteEntityLock;
+	};
+	$scope.confirmUpateSite = function() {
+		var promiseList = [];
+		$scope._siteEntityLock = true;
+
+		if($scope._siteEntity.srcSite) {
+			var promiseList = [];
+			$.each($scope._siteEntity.dataSrcList, function(name, dataSrc) {
+				var _entity = {
+					enabled: dataSrc.enabled,
+					tags: {
+						site: $scope._siteEntity.name,
+						dataSource: name,
+					},
+				};
+
+				if(dataSrc.enabled) {
+					promiseList.push(Entities.updateEntity("AlertDataSourceService", _entity)._promise);
+				} else {
+					var _dataSrc = common.array.find(name, $scope._siteEntity.srcSite.dataSrcList, "tags.dataSource");
+					if(_dataSrc) {
+						_dataSrc.enabled = false;
+						promiseList.push(Entities.updateEntity("AlertDataSourceService", _entity)._promise);
+					}
+				}
+			});
+		} else {
+			$.each($scope._siteEntity.dataSrcList, function(name, dataSrc) {
+				if(!dataSrc.enabled) return;
+
+				var _entity = {
+					enabled: true,
+					tags: {
+						site: $scope._siteEntity.name,
+						dataSource: name,
+					},
+				};
+				promiseList.push(Entities.updateEntity("AlertDataSourceService", _entity)._promise);
+			});
+		}
+		
+		$q.all(promiseList).then(function() {
+			$("#siteMDL").modal("hide")
+			.on("hidden.bs.modal", function() {
+				$("#siteMDL").off("hidden.bs.modal");
+				Site.refresh();
+			});
+		}).finally(function() {
+			$scope._siteEntityLock = false;
+		});
+	};
+
+	$scope.deleteSite = function(site) {
+		$.dialog({
+			title: "Delete Confirm",
+			content: "<span class='text-red fa fa-exclamation-triangle pull-left' style='font-size: 50px;'></span>" +
+					"<p>You are <strong class='text-red'>DELETING</strong> the site '<strong>" + site.name + "</strong>'!</p>" +
+					"<p>Proceed to delete?</p>",
+			buttons: [
+				{name: "Delete", class: "btn btn-danger", value: true},
+				{name: "Cancel", class: "btn btn-default", value: false},
+			]
+		}, function(ret) {
+			if(!ret) return;
+
+			Entities.deleteEntities("AlertDataSourceService", {
+				site: site.name
+			})._promise.then(function() {
+				Site.refresh();
+			});
+		});
+	};
+
+	// ======================================= Data Source ========================================
+	$scope.showDataSourceEditor = function(dataSrc) {
+		$("#dataSrcMDL").modal("show");
+		setTimeout(function() {
+			$("#dataSrcConfig").focus();
+		}, 500);
+
+		$scope._dataSrcEntity = dataSrc;
+	};
+
+	$scope.confirmUpateDataSource = function() {
+		$scope._dataSrcEntityLock = true;
+		Entities.updateEntity("AlertDataSourceService", $scope._dataSrcEntity)._promise.then(function() {
+			$("#dataSrcMDL").modal("hide");
+		}).finally(function() {
+			$scope._dataSrcEntityLock = false;
+		});
+	};
+
+	$scope.confirmDeleteDataSource = function() {
+		console.log($scope._dataSrcEntity);
+		$("#dataSrcMDL").modal("hide")
+		.on('hidden.bs.modal', function (e) {
+			$("#dataSrcMDL").off('hidden.bs.modal');
+
+			var _additionalContent = Site.find($scope._dataSrcEntity.tags.site).dataSrcList.length > 1 ? "" : "<p class='text-muted' style='margin-left: 60px;'>(This site has only one source. Delete will remove site either.)</p>";
+
+			$.dialog({
+				title: "Delete Confirm",
+				content: "<span class='text-red fa fa-exclamation-triangle pull-left' style='font-size: 50px;'></span>" +
+						"<p>You are <strong class='text-red'>DELETING</strong> the data source '<strong>" + $scope._dataSrcEntity.tags.dataSource + "</strong>' of '" + $scope._dataSrcEntity.tags.site + "'!</p>" +
+						"<p>Proceed to delete?</p>" + _additionalContent,
+				buttons: [
+					{name: "Delete", class: "btn btn-danger", value: true},
+					{name: "Cancel", class: "btn btn-default", value: false},
+				]
+			}, function(ret) {
+				if(!ret) return;
+
+				Entities.deleteEntity("AlertDataSourceService", $scope._dataSrcEntity)._promise.then(function() {
+					Site.refresh();
+				});
+			});
+		});
+	};
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/2c3005c9/eagle-webservice/src/main/webapp/app/public/js/ctrl/streamController.js
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/app/public/js/ctrl/streamController.js b/eagle-webservice/src/main/webapp/app/public/js/ctrl/streamController.js
new file mode 100644
index 0000000..858a7a4
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/app/public/js/ctrl/streamController.js
@@ -0,0 +1,207 @@
+/*
+ * 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';
+
+// =============================================================
+// =                        Stream List                        =
+// =============================================================
+damControllers.controller('streamListCtrl', function(globalContent, damContent, $scope, $route, $routeParams, $q, Entities) {
+	globalContent.setConfig(damContent.config);
+	globalContent.hideSite = true;
+
+	$scope.streams = {};
+	$scope._streamEntity;
+	$scope._streamEntityLock = false;
+
+	// =========================================== List ===========================================
+	var _streamList = Entities.queryEntities("AlertStreamService", '@streamName=~".*"');
+	var _streamSchemaList = Entities.queryEntities("AlertStreamSchemaService", '@streamName=~".*"');
+	$scope.streamList = _streamList;
+	$scope.streamSchemaList = _streamSchemaList;
+
+	_streamList._promise.then(function() {
+		$.each(_streamList, function(i, stream) {
+			stream.metaList = [];
+			$scope.streams[stream.tags.dataSource + "_" + stream.tags.streamName] = stream;
+		});
+	});
+
+	$q.all([_streamList._promise, _streamSchemaList._promise]).then(function(res) {
+		$.each(_streamSchemaList, function(i, meta) {
+			var _stream = $scope.streams[meta.tags.dataSource + "_" + meta.tags.streamName];
+			if(_stream) {
+				_stream.metaList.push(meta);
+			} else {
+				console.warn("[Meta] Stream not match:", meta.tags.dataSource + "_" + meta.tags.streamName);
+			}
+		});
+	});
+
+	// =========================================== Edit ===========================================
+	$scope.showStreamEditor = function(stream) {
+		$("#streamMDL").modal("show");
+		setTimeout(function() {
+			$("#dataSource").focus();
+		}, 500);
+
+		$scope._streamEntity = {
+			dataSource: "",
+			streamName: "",
+			description: "",
+			metaList: [],
+		};
+		if(stream) {
+			$scope._streamEntity.srcStream = stream;
+			$scope._streamEntity.dataSource = stream.tags.dataSource;
+			$scope._streamEntity.streamName = stream.tags.streamName;
+			$scope._streamEntity.desc = stream.desc;
+
+			$scope._streamEntity.metaList = $.map(stream.metaList, function(meta) {
+				return {
+					srcMeta: meta,
+					attrName: meta.tags.attrName,
+					attrDisplayName: meta.attrDisplayName,
+					attrType: meta.attrType,
+					attrDescription: meta.attrDescription,
+				};
+			});
+		}
+	};
+
+	$scope.deleteMeta = function(meta) {
+		$.dialog({
+			title: "Delete confirm",
+			content: "<p>You are <strong class='text-red'>DELETING</strong> the meta '<strong>" + meta.attrName + "</strong>'!</p>" +
+					"<p>Proceed to delete?</p>",
+			buttons: [
+				{name: "Delete", class: "btn btn-danger", value: true},
+				{name: "Cancel", class: "btn btn-default", value: false},
+			]
+		}, function(ret) {
+			if(!ret) return;
+
+			common.array.remove(meta, $scope._streamEntity.metaList);
+			$scope.$apply();
+		});
+	};
+
+	$scope.checkUpdateStream = function() {
+		if(!$scope._streamEntity || $scope._streamEntityLock) return false;
+
+		var _pass = true;
+
+		if(!$scope._streamEntity.dataSource) _pass = false;
+		if(!$scope._streamEntity.streamName) _pass = false;
+
+		$.each($scope._streamEntity.metaList, function(i, meta) {
+			if(!meta.attrName || !meta.attrType) {
+				_pass = false;
+				return false;
+			}
+		});
+
+		return _pass;
+	};
+
+	$scope.confirmDeleteStream = function() {
+		$.dialog({
+			title: "Delete Confirm",
+			content: "<span class='text-red fa fa-exclamation-triangle pull-left' style='font-size: 50px;'></span>" +
+					"<p>You are <strong class='text-red'>DELETING</strong> the stream '<strong>" + $scope._streamEntity.streamName + "</strong>'!</p>" +
+					"<p>Proceed to delete?</p>",
+			buttons: [
+				{name: "Delete", class: "btn btn-danger", value: true},
+				{name: "Cancel", class: "btn btn-default", value: false},
+			]
+		}, function(ret) {
+			if(!ret) return;
+
+			var _promiseStream = Entities.deleteEntities("AlertStreamService", {
+				dataSource: $scope._streamEntity.dataSource,
+				streamName: $scope._streamEntity.streamName,
+			})._promise;
+			var _promiseStreamSchema = Entities.deleteEntities("AlertStreamSchemaService", {
+				dataSource: $scope._streamEntity.dataSource,
+				streamName: $scope._streamEntity.streamName,
+			})._promise;
+
+			$q.all(_promiseStream, _promiseStreamSchema).then(function() {
+				$("#streamMDL").modal("hide");
+
+				setTimeout(function() {
+					$route.reload();
+				}, 500);
+			});
+		});
+	};
+
+	$scope.confirmUpateStream = function() {
+		$scope._streamEntityLock = true;
+
+		// Stream entity
+		var _entity = {
+			prefix: "alertStream",
+			tags: {
+				dataSource: $scope._streamEntity.dataSource,
+				streamName: $scope._streamEntity.streamName,
+			},
+			desc: $scope._streamEntity.desc
+		};
+
+		// Merge original stream
+		if($scope._streamEntity.srcStream) {
+			$.extend(_entity, $scope._streamEntity.srcStream);
+		}
+
+		// Meta entities
+		var _metaList = $.map($scope._streamEntity.metaList, function(meta) {
+			return {
+				prefix: "alertStreamSchema",
+				attrType: meta.attrType,
+				attrDisplayName: meta.attrDisplayName,
+				attrDescription: meta.attrDescription,
+				tags: {
+					dataSource: _entity.tags.dataSource,
+					streamName: _entity.tags.streamName,
+					attrName: meta.attrName,
+				},
+			};
+		});
+
+		Entities.updateEntity("AlertStreamService", _entity)._promise.then(function() {
+			Entities.deleteEntities("AlertStreamSchemaService", {
+				dataSource: _entity.tags.dataSource,
+				streamName: _entity.tags.streamName,
+			})._promise.then(function() {
+				Entities.updateEntity("AlertStreamSchemaService", _metaList)._promise.finally(function() {
+					Site.refresh();
+				});
+			}).finally(function() {
+				$("#streamMDL").modal("hide");
+				$scope._streamEntityLock = false;
+
+				setTimeout(function() {
+					$route.reload();
+				}, 500);
+			});
+		}, function() {
+			$scope._streamEntityLock = false;
+		});
+	};
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/2c3005c9/eagle-webservice/src/main/webapp/app/public/js/ctrl/userProfileController.js
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/app/public/js/ctrl/userProfileController.js b/eagle-webservice/src/main/webapp/app/public/js/ctrl/userProfileController.js
new file mode 100644
index 0000000..73bec7b
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/app/public/js/ctrl/userProfileController.js
@@ -0,0 +1,258 @@
+/*
+ * 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';
+
+// =============================================================
+// =                     User Profile List                     =
+// =============================================================
+damControllers.controller('userProfileListCtrl', function(globalContent, Site, damContent, $scope, $interval, Entities) {
+	globalContent.setConfig(damContent.config);
+	globalContent.pageSubTitle = Site.current().name;
+
+	$scope.common = common;
+	$scope.algorithms = [];
+
+	// ======================================== Algorithms ========================================
+	$scope.algorithmEntity = {};
+	Entities.queryEntities("AlertDefinitionService", {site: Site.current().name, dataSource: "userProfile"})._promise.then(function(data) {
+		$scope.algorithmEntity = common.getValueByPath(data, "data.obj[0]");
+		$scope.algorithmEntity.policy = common.parseJSON($scope.algorithmEntity.policyDef);
+	});
+
+	// ======================================= User profile =======================================
+	$scope.profileList = Entities.queryEntities("MLModelService", {site: Site.current().name}, ["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().name,
+			_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().name,
+					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
+		});
+
+		// TODO: Remove task
+		_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(data) {
+					_loadTasks();
+				});
+			});
+		});
+	};
+
+	_loadTasks();
+	var _loadInterval = $interval(_loadTasks, app.time.refreshInterval);
+	$scope.$on('$destroy',function(){
+		$interval.cancel(_loadInterval);
+	});
+});
+
+// =============================================================
+// =                    User Profile Detail                    =
+// =============================================================
+damControllers.controller('userProfileDetailCtrl', function(globalContent, Site, damContent, $scope, $routeParams, Entities) {
+	globalContent.setConfig(damContent.config);
+	globalContent.pageTitle = "User Profile";
+	globalContent.pageSubTitle = Site.current().name;
+	globalContent.navPath = ["User Profile", "Detail"];
+
+	$scope.user = $routeParams.user;
+
+	// User profile
+	$scope.profiles = {};
+	$scope.profileList = Entities.queryEntities("MLModelService", {site: Site.current().name, 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) {
+			$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 = [];
+			var _categoryList = [];
+
+			$.each($scope.profiles.DE._content.statistics, function(i, unit) {
+				_meanList[i] = unit.mean;
+				_stddevList[i] = unit.stddev;
+
+				_categoryList[i] = unit.commandName;
+			});
+			$scope.profiles.DE._chart.series = [
+				{
+					name: "mean",
+					data: _meanList
+				},
+				{
+					name: "stddev",
+					data: _stddevList
+				},
+				{
+					type: "category",
+					data: _categoryList
+				},
+			];
+
+			// 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/2c3005c9/eagle-webservice/src/main/webapp/grunt.json
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/grunt.json b/eagle-webservice/src/main/webapp/grunt.json
new file mode 100644
index 0000000..78f4c30
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/grunt.json
@@ -0,0 +1,46 @@
+{
+	"concat": {
+		"js": {
+			"options": {
+				"separator": "\n"
+			},
+			"src": [
+				"node_modules/jquery/dist/jquery.min.js",
+				"node_modules/jquery-slimscroll/jquery.slimscroll.min.js",
+				"node_modules/bootstrap/dist/js/bootstrap.min.js",
+				"app/public/assets/bootstrap-components/js/bootstrap-components.min.js",
+				"node_modules/moment/min/moment-with-locales.min.js",
+				"node_modules/moment-timezone/builds/moment-timezone-with-data.min.js",
+				"node_modules/admin-lte/dist/js/app.min.js",
+				"node_modules/angular/angular.min.js",
+				"node_modules/angular-resource/angular-resource.min.js",
+				"node_modules/angular-route/angular-route.min.js",
+				"node_modules/angular-cookies/angular-cookies.min.js",
+				"node_modules/angular-ui-bootstrap/ui-bootstrap-tpls.min.js",
+				"node_modules/Flot/jquery.flot.js",
+				"node_modules/Flot/jquery.flot.stack.js",
+				"node_modules/Flot/jquery.flot.pie.js",
+				"node_modules/Flot/jquery.flot.time.js",
+				"node_modules/Flot/jquery.flot.crosshair.js",
+				"app/public/assets/flot/jquery.flot.tooltip.js",
+				"app/public/assets/flot/jquery.flot.legend.js",
+				"node_modules/d3/d3.min.js",
+
+				"tmp/public/js/scripts.min.js"
+			],
+			"dest": "tmp/public/js/doc.js"
+		},
+		"css": {
+			"src": [
+				"node_modules/bootstrap/dist/css/bootstrap.min.css",
+				"app/public/assets/bootstrap-components/css/bootstrap-components.min.css",
+				"node_modules/font-awesome/css/font-awesome.min.css",
+				"node_modules/admin-lte/dist/css/AdminLTE.min.css",
+				"node_modules/admin-lte/dist/css/skins/skin-blue.min.css",
+
+				"tmp/public/css/styles.css"
+			],
+			"dest": "tmp/public/css/styles.min.css"
+		}
+	}
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/2c3005c9/eagle-webservice/src/main/webapp/index.html
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/index.html b/eagle-webservice/src/main/webapp/index.html
index d5a4ade..a67650b 100755
--- a/eagle-webservice/src/main/webapp/index.html
+++ b/eagle-webservice/src/main/webapp/index.html
@@ -17,217 +17,12 @@
   limitations under the License.
   -->
 
-<html ng-app="eagleApp" ng-controller="MainCtrl">
+<html>
 	<head>
-		<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
-		<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
-		<meta charset="UTF-8">
-		<meta content='width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no' name='viewport'>
-		<link rel="shortcut icon" href="public/images/favicon.png">
-
-		<title>Eagle</title>
-
-		<!-- main -->
-		<link rel="shortcut icon" type="image/png" href="public/images/favicon.png">
-		<link rel="stylesheet" type="text/css" media="screen" href="public/css/main.css">
-
-		<!-- jQuery -->
-		<script src="public/assets/jquery/jquery-1.11.3.min.js" type="text/javascript" charset="utf-8"></script>
-		<script src="public/assets/jquery/jquery.slimscroll.min.js" type="text/javascript" charset="utf-8"></script>
-
-		<!-- Bootstrap -->
-		<link rel="stylesheet" type="text/css" media="screen" href="public/assets/bootstrap/css/bootstrap.min.css">
-		<script src="public/assets/bootstrap/js/bootstrap.min.js" type="text/javascript" charset="utf-8"></script>
-		<link rel="stylesheet" type="text/css" media="screen" href="public/assets/bootstrap-components/css/bootstrap-components.min.css">
-		<script src="public/assets/bootstrap-components/js/bootstrap-components.min.js"></script>
-
-		<!-- Moment -->
-		<script src="public/assets/moment/moment-with-locales.min.js"></script>
-		<script src="public/assets/moment/moment-timezone-with-data.min.js"></script>
-
-		<!-- FastClick -->
-		<script src="public/assets/fastclick/fastclick.min.js"></script>
-
-		<!-- Font Awesome Icons -->
-		<link href="public/assets/font-awesome/css/font-awesome.min.css" rel="stylesheet" type="text/css" />
-
-		<!-- AdminLTE -->
-		<!-- Theme style -->
-		<link href="public/assets/adminLTE/dist/css/AdminLTE.min.css" rel="stylesheet" type="text/css" />
-		<link href="public/assets/adminLTE/dist/css/skins/skin-blue.min.css" rel="stylesheet" type="text/css" />
-		<script src="public/assets/adminLTE/dist/js/app.min.js"></script>
-
-		<!-- Angular -->
-		<script src="public/assets/angular/angular.min.js"></script>
-		<script src="public/assets/angular/angular-resource.min.js"></script>
-		<script src="public/assets/angular/angular-route.min.js"></script>
-		<script src="public/assets/angular/angular-cookies.min.js"></script>
-		<script src="public/assets/angular/angular-animate.min.js"></script>
-		<script src="public/assets/angular/ui-bootstrap-tpls-0.13.4.min.js"></script>
-
-		<!-- Flot -->
-		<script src="public/assets/flot/excanvas.min.js"></script>
-		<script src="public/assets/flot/jquery.flot.min.js"></script>
-		<script src="public/assets/flot/jquery.flot.stack.min.js"></script>
-		<script src="public/assets/flot/jquery.flot.pie.min.js"></script>
-		<script src="public/assets/flot/jquery.flot.time.min.js"></script>
-		<script src="public/assets/flot/jquery.flot.crosshair.min.js"></script>
-		<script src="public/assets/flot/jquery.flot.tooltip.js"></script>
-		<script src="public/assets/flot/jquery.flot.legend.js"></script>
-
-		<!-- d3 -->
-		<script src="public/assets/d3/d3.min.js"></script>
-
-		<!-- Customized -->
-		<script src="public/js/app.ui.js" type="text/javascript" charset="utf-8"></script>
-		<script src="public/js/components/main.js" type="text/javascript" charset="utf-8"></script>
-		<script src="public/js/components/sortTable.js" type="text/javascript" charset="utf-8"></script>
-		<script src="public/js/components/tabs.js" type="text/javascript" charset="utf-8"></script>
-		<script src="public/js/components/file.js" type="text/javascript" charset="utf-8"></script>
-		<script src="public/js/components/charts.js" type="text/javascript" charset="utf-8"></script>
-		<script src="public/js/components/charts/bar.js" type="text/javascript" charset="utf-8"></script>
-		<script src="public/js/components/charts/line3d.js" type="text/javascript" charset="utf-8"></script>
-
-		<script src="public/js/common.js" type="text/javascript" charset="utf-8"></script>
-		<script src="public/js/config.js" type="text/javascript" charset="utf-8"></script>
-
-		<script src="public/js/app.time.js" type="text/javascript" charset="utf-8"></script>
-
-		<script src="public/js/app.js" type="text/javascript" charset="utf-8"></script>
-		<script src="public/js/ctrl/damController.js" type="text/javascript" charset="utf-8"></script>
-		<script src="public/js/ctrl/policyController.js" type="text/javascript" charset="utf-8"></script>
-		<script src="public/js/ctrl/siteController.js" type="text/javascript" charset="utf-8"></script>
-		<script src="public/js/ctrl/streamController.js" type="text/javascript" charset="utf-8"></script>
-		<script src="public/js/ctrl/alertController.js" type="text/javascript" charset="utf-8"></script>
-		<script src="public/js/ctrl/sensitivityController.js" type="text/javascript" charset="utf-8"></script>
-		<script src="public/js/ctrl/userProfileController.js" type="text/javascript" charset="utf-8"></script>
-		<script src="public/js/ctrl/authController.js" type="text/javascript" charset="utf-8"></script>
+		<script>
+			window.location.href = "ui";
+		</script>
 	</head>
-	<body class="skin-blue sidebar-mini" ng-class="{'no-sidebar' : globalContent.hideSidebar}">
-		<!-- Site wrapper -->
-		<div class="wrapper">
-			<header class="main-header">
-				<a href="#/" class="logo">
-					<span class="logo-mini">DAM</span>
-					<span class="logo-lg">Eagle <small>Data Activity Monitoring</small></span>
-				</a>
-				<!-- Header Navbar: style can be found in header.less -->
-				<nav class="navbar navbar-static-top" role="navigation">
-					<!-- Sidebar toggle button-->
-					<a href="#" ng-hide="globalContent.hideSidebar" class="sidebar-toggle" data-toggle="offcanvas" role="button">
-						<span class="sr-only">Toggle navigation</span>
-						<span class="icon-bar"></span>
-						<span class="icon-bar"></span>
-						<span class="icon-bar"></span>
-					</a>
-					<div class="navbar-custom-menu">
-						<ul class="nav navbar-nav">
-							<!-- Site -->
-							<li class="dropdown" ng-show="!globalContent.hideSite && !globalContent.lockSite">
-								<a class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
-									<i class="fa fa-server"></i>
-									{{site.current().name}}
-									<i class="fa fa-caret-down"></i>
-								</a>
-								<ul class="dropdown-menu">
-									<li ng-repeat="sites in site.list">
-										<a ng-click="site.current(sites);">
-											<span class="fa fa-database"></span> {{sites.name}}
-										</a>
-									</li>
-									<!--li role="separator" class="divider"></li>
-									<li>
-										<a href="#/dam/siteList"><span class="fa fa-cog"></span>Config Sites</a>
-									</li-->
-								</ul>
-							</li>
-							<li class="dropdown" ng-show="globalContent.lockSite">
-								<a>
-									<i class="fa fa-server"></i>
-									{{site.current().name}}
-								</a>
-							</li>
-
-							<!-- User -->
-							<li class="dropdown user user-menu" ng-hide="globalContent.hideUser">
-								<a class="dropdown-toggle" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
-									<i class="fa fa-user"></i>
-									{{auth.userProfile.username}}
-								</a>
-								<!--ul class="dropdown-menu">
-									<li>
-										<a ng-click="logout();">
-											<span class="fa fa-sign-out"></span> Logout
-										</a>
-									</li>
-								</ul-->
-								<ul class="dropdown-menu">
-									<!-- User image -->
-									<li class="user-header">
-										<span class="img-circle">
-											<span class="fa fa-user" alt="User Image"></span>
-										</span>
-										<p>
-											{{auth.userProfile.username}}
-											<small>
-												<span ng-repeat="role in auth.userProfile.authorities">{{role.authority}} </span>
-											</small>
-										</p>
-									</li>
-									<!-- Menu Footer-->
-									<li class="user-footer">
-										<div class="pull-right">
-											<a ng-click="logout();" class="btn btn-default btn-flat">Sign out</a>
-										</div>
-									</li>
-								</ul>
-							</li>
-						</ul>
-					</div>
-				</nav>
-			</header>
-
-			<!-- =============================================== -->
-			<!-- Left side column. contains the side bar -->
-			<aside class="main-sidebar" ng-hide="globalContent.hideSidebar">
-				<!-- side bar: style can be found in sidebar.less -->
-				<section class="sidebar">
-					<ul class="sidebar-menu">
-						<li ng-repeat="page in globalContent.pageList" ng-class="getNavClass(page)" ng-show="getNavVisible(page)">
-							<a href="{{page.url}}">
-								<i class="fa fa-{{page.icon}}"></i> <span>{{page.title}}</span> 
-							</a>
-						</li>
-					</ul>
-				</section>
-				<!-- /.sidebar -->
-			</aside>
-
-			<!-- =============================================== -->
-			<!-- Right side column. Contains the navbar and content of the page -->
-			<div class="content-wrapper">
-				<!-- Content Header (Page header) -->
-				<section class="content-header" ng-hide="globalContent.hideSidebar">
-					<h1>
-						<span class="pageTitle">{{globalContent.pageTitle}}</span>
-						<small class="pageSubTitle">{{globalContent.pageSubTitle}}</small>
-					</h1>
-
-					<ol class="breadcrumb">
-						<li ng-repeat="nav in globalContent.navPath">
-							<a ng-href="{{$last ? '' : globalContent.navMapping[nav]}}">
-								<span class="fa fa-home" ng-show="$first"></span>
-								{{nav}}
-							</a>
-						</li>
-					</ol>
-				</section>
-
-				<!-- Main content -->
-				<section class="content">
-					<div id="content" ng-view></div>
-				</section><!-- /.content -->
-			</div><!-- /.content-wrapper -->
-		</div><!-- ./wrapper -->
+	<body>
 	</body>
 </html>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/2c3005c9/eagle-webservice/src/main/webapp/package.json
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/package.json b/eagle-webservice/src/main/webapp/package.json
new file mode 100644
index 0000000..f07a6d3
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/package.json
@@ -0,0 +1,47 @@
+{
+	"name": "ApacheEagleWebApp",
+	"description": "Apache Eagle Web Application",
+	"author": "ApacheEagle",
+	"repository": {
+		"type:": "git",
+		"url": "https://github.com/apache/incubator-kylin.git"
+	},
+	"license": "Apache-2.0",
+	"dependencies": {
+		"jquery"				: "1.11.3",
+		"bootstrap"				: "3.3.5",
+		"moment"				: "2.10.6",
+		"moment-timezone"		: "0.4.1",
+		"font-awesome"			: "4.4.0",
+		"admin-lte"				: "2.3.2",
+		"angular"				: "1.4.7",
+		"angular-resource"		: "1.4.7",
+		"angular-route"			: "1.4.7",
+		"angular-cookies"		: "1.4.7",
+		"angular-animate"		: "1.4.7",
+		"angular-ui-bootstrap"	: "0.14.3",
+		"Flot": "git+https://github.com/flot/flot.git#v0.8.3",
+		"d3"					: "3.5.9",
+		"jquery-slimscroll": "1.3.6"
+	},
+
+	"devDependencies": {
+		"grunt": "~0.4.5",
+		"grunt-cli": "~0.1.13",
+
+		"grunt-contrib-jshint": "~0.10.0",
+		"grunt-contrib-nodeunit": "~0.4.1",
+
+		"grunt-regex-replace": "~0.2.6",
+		"grunt-contrib-clean": "~0.7.0",
+		"grunt-contrib-uglify": "~0.5.0",
+		"grunt-contrib-concat": "~0.5.1",
+		"grunt-contrib-cssmin": "~0.14.0",
+		"grunt-contrib-copy": "~0.8.2",
+		"grunt-htmlrefs": "~0.5.0"
+	},
+
+	"scripts": {
+		"grunt": "grunt"
+	}
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/2c3005c9/eagle-webservice/src/main/webapp/partials/dam/alertDetail.html
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/partials/dam/alertDetail.html b/eagle-webservice/src/main/webapp/partials/dam/alertDetail.html
deleted file mode 100755
index 9b0ec01..0000000
--- a/eagle-webservice/src/main/webapp/partials/dam/alertDetail.html
+++ /dev/null
@@ -1,61 +0,0 @@
-<!--
-  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-info">
-	<div class="box-header with-border">
-		<h3 class="box-title" id="policyId">
-			{{alert.tags.policyId}}
-			<small>{{common.format.date(alert.timestamp)}}</small>
-		</h3>
-	</div><!-- /.box-header -->
-
-	<div class="box-body">
-		<a class="btn btn-primary pull-right" href="#/dam/policyDetail/?policy={{alert.tags.policyId}}&site={{alert.tags.site}}&executor={{alert.tags.alertExecutorId}}">View Policy</a>
-
-		<div class="inline-group">
-			<dl><dt>Site</dt><dd>{{alert.tags.site}}</dd></dl>
-			<dl><dt>Data Source</dt><dd>{{alert.tags.dataSource}}</dd></dl>
-		</div>
-		<div class="inline-group">
-			<dl><dt>Alert Time</dt><dd>{{common.format.date(alert.timestamp)}}</dd></dl>
-			<dl><dt>Message Time</dt><dd>{{common.format.date(alert.alertContext.properties.timestamp)}}</dd></dl>
-		</div>
-		<!--div class="inline-group">
-			<dl><dt>Severity</dt><dd>{{alert.alertContext.properties.severity}}</dd></dl>
-		</div-->
-		<div class="inline-group">
-			<dl><dt>Stream Name</dt><dd>{{alert.tags.sourceStreams}}</dd></dl>
-		</div>
-		<div class="inline-group">
-			<dl><dt>Alert Source</dt><dd>{{alert.tags.alertSource}}</dd></dl>
-		</div>
-		<div class="inline-group">
-			<dl><dt>User</dt><dd>{{alert.alertContext.properties.user}}</dd></dl>
-			<dl><dt>Host</dt><dd>{{alert.alertContext.properties.host}}</dd></dl>
-		</div>
-		<div class="inline-group">
-			<dl><dt>Event</dt><dd>{{alert.alertContext.properties.alertEvent}}</dd></dl>
-		</div>
-		<div class="inline-group">
-			<dl><dt>Message</dt><dd>{{alert.alertContext.properties.alertMessage}}</dd></dl>
-		</div>
-	</div><!-- /.box-body -->
-
-	<div class="overlay" ng-hide="alertList._promise.$$state.status === 1;">
-		<i class="fa fa-refresh fa-spin"></i>
-	</div>
-</div>

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/2c3005c9/eagle-webservice/src/main/webapp/partials/dam/alertList.html
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/partials/dam/alertList.html b/eagle-webservice/src/main/webapp/partials/dam/alertList.html
deleted file mode 100755
index d5492ca..0000000
--- a/eagle-webservice/src/main/webapp/partials/dam/alertList.html
+++ /dev/null
@@ -1,83 +0,0 @@
-<!--
-  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">
-			{{dataSource || "All Alerts"}}
-			<div class="btn-group">
-				<button class="btn btn-box-tool dropdown-toggle" data-toggle="dropdown" aria-expanded="false">
-					<i class="fa fa-caret-down"></i>
-				</button>
-				<ul id="programList" class="dropdown-menu" role="menu">
-					<li ng-repeat="dataSrc in site.current().dataSrcList">
-						<a href="#/dam/alertList/{{dataSrc.tags.dataSource}}">{{dataSrc.tags.dataSource}}</a>
-					</li>
-					<li class="divider"></li>
-					<li>
-						<a href="#/dam/alertList">All Policy List</a>
-					</li>
-				</ul>
-			</div>
-		</h3>
-	</div>
-	<div class="box-body">
-		<p ng-show="!alertList.ready">
-			<span class="fa fa-refresh fa-spin"> </span>
-			Loading...
-		</p>
-
-		<div sorttable source="alertList" sort="-timestamp">
-			<table class="table table-bordered" ng-non-bindable>
-				<thead>
-					<tr>
-						<th width="170" sortpath="timestamp">Alert Time</th>
-						<th width="170" sortpath="alertContext.properties.timestamp">Message Time</th>
-						<th width="105" sortpath="tags.dataSource">Data Source</th>
-						<!--th width="70" sortpath="severity">Type</th-->
-						<th width="150" sortpath="tags.policyId">Policy Name</th>
-						<th width="60" sortpath="alertContext.properties.user">User</th>
-						<th width="150" sortpath="alertContext.properties.host">Host</th>
-						<th sortpath="alertContext.properties.emailMessage">Description</th>
-						<th width="50"> </th>
-					</tr>
-				</thead>
-				<tbody>
-					<tr ng-class="{info : item.__new}">
-						<td>{{common.format.date(item.timestamp)}}</td>
-						<td>{{common.format.date(item.alertContext.properties.timestamp)}}</td>
-						<td>{{item.tags.dataSource}}</td>
-						<!--td>{{item.severity}}</td-->
-						<td class="text-nowrap">
-							<a class="fa fa-share-square-o" ng-show="item.tags.policyId"
-							href="#/dam/policyDetail/?policy={{item.tags.policyId}}&site={{item.tags.site}}&executor={{item.tags.alertExecutorId}}"> </a>
-							{{item.tags.policyId}}
-						</td>
-						<td>{{item.alertContext.properties.user}}</td>
-						<td>{{item.alertContext.properties.host}}</td>
-						<td>{{item.alertContext.properties.alertMessage}}</td>
-						<td><a href="#/dam/alertDetail/{{item.encodedRowkey}}">Detail</a></td>
-					</tr>
-				</tbody>
-			</table>
-		</div>
-
-	</div>
-	<!--div class="box-footer clearfix">
-	</div-->
-</div>