eagle-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From hdenduk...@apache.org
Subject [5/9] incubator-eagle git commit: EAGLE-139 EAGLE-163 Eagle UI Modularization and fix bugs in policy extensions
Date Fri, 26 Feb 2016 21:26:23 GMT
http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/d37643b0/eagle-webservice/src/main/webapp/app/public/feature/common/controller.js
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/app/public/feature/common/controller.js b/eagle-webservice/src/main/webapp/app/public/feature/common/controller.js
new file mode 100644
index 0000000..50f377c
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/app/public/feature/common/controller.js
@@ -0,0 +1,1023 @@
+/*
+ * 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("common");
+
+	// ==============================================================
+	// =                          Function                          =
+	// ==============================================================
+	feature.service("Policy", function(Entities) {
+		var Policy = function () {};
+
+		Policy.updatePolicyStatus = function(policy, status) {
+			$.dialog({
+				title: "Confirm",
+				content: "Do you want to " + (status ? "enable" : "disable") + " policy[" + policy.tags.policyId + "]?",
+				confirm: true
+			}, function(ret) {
+				if(ret) {
+					policy.enabled = status;
+					Entities.updateEntity("AlertDefinitionService", policy);
+				}
+			});
+		};
+		Policy.deletePolicy = function(policy, callback) {
+			$.dialog({
+				title: "Confirm",
+				content: "Do you want to delete policy[" + policy.tags.policyId + "]?",
+				confirm: true
+			}, function(ret) {
+				if(ret) {
+					policy.enabled = status;
+					Entities.deleteEntity("AlertDefinitionService", policy)._promise.finally(function() {
+						if(callback) {
+							callback(policy);
+						}
+					});
+				}
+			});
+		};
+		return Policy;
+	});
+
+	// ==============================================================
+	// =                          Policies                          =
+	// ==============================================================
+
+	// ========================= Policy List ========================
+	feature.navItem("policyList", "Policies", "list");
+	feature.controller('policyList', function(PageConfig, Site, $scope, Application, Entities, Policy) {
+		PageConfig.pageTitle = "Policy List";
+		PageConfig.pageSubTitle = Site.current().tags.site;
+
+		// Initial load
+		$scope.policyList = [];
+		$scope.application = Application.current();
+
+		// List policies
+		var _policyList = Entities.queryEntities("AlertDefinitionService", {site: Site.current().tags.site, dataSource: $scope.application.tags.application});
+		_policyList._promise.then(function() {
+			$.each(_policyList, function(i, policy) {
+				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 = Policy.updatePolicyStatus;
+		$scope.deletePolicy = function(policy) {
+			Policy.deletePolicy(policy, function(policy) {
+				var _index = $scope.policyList.indexOf(policy);
+				$scope.policyList.splice(_index, 1);
+			});
+		};
+	});
+
+	// ======================= Policy Detail ========================
+	feature.controller('policyDetail', function(PageConfig, Site, $scope, $wrapState, Entities, Policy, nvd3) {
+		var MAX_PAGESIZE = 10000;
+
+		PageConfig.pageTitle = "Policy Detail";
+		PageConfig.lockSite = true;
+		PageConfig
+			.addNavPath("Policy List", "/common/policyList")
+			.addNavPath("Policy Detail");
+
+		$scope.chartConfig = {
+			chart: "line",
+			xType: "time"
+		};
+
+		// Query policy
+		if($wrapState.param.filter) {
+			$scope.policyList = Entities.queryEntity("AlertDefinitionService", $wrapState.param.filter);
+		} else {
+			$scope.policyList = Entities.queryEntities("AlertDefinitionService", {
+				policyId: $wrapState.param.policy,
+				site: $wrapState.param.site,
+				alertExecutorId: $wrapState.param.executor
+			});
+		}
+
+		$scope.policyList._promise.then(function() {
+			var policy = null;
+
+			if($scope.policyList.length === 0) {
+				$.dialog({
+					title: "OPS!",
+					content: "Policy not found!"
+				}, function() {
+					location.href = "#/common/policyList";
+				});
+				return;
+			} else {
+				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 _endTime = app.time.now().hour(23).minute(59).second(59).millisecond(0);
+			var _startTime = _endTime.clone().subtract(1, "month").hour(0).minute(0).second(0).millisecond(0);
+			var _cond = {
+				dataSource: policy.tags.dataSource,
+				policyId: policy.tags.policyId,
+				_startTime: _startTime,
+				_endTime: _endTime
+			};
+
+			// > eagle.policy.eval.count
+			$scope.policyEvalSeries = nvd3.convert.eagle([Entities.querySeries("GenericMetricService", $.extend({_metricName: "eagle.policy.eval.count"}, _cond), "@cluster", "sum(value)", 60 * 24)]);
+
+			// > eagle.policy.eval.fail.count
+			$scope.policyEvalFailSeries = nvd3.convert.eagle([Entities.querySeries("GenericMetricService", $.extend({_metricName: "eagle.policy.eval.fail.count"}, _cond), "@cluster", "sum(value)", 60 * 24)]);
+
+			// > eagle.alert.count
+			$scope.alertSeries = nvd3.convert.eagle([Entities.querySeries("GenericMetricService", $.extend({_metricName: "eagle.alert.count"}, _cond), "@cluster", "sum(value)", 60 * 24)]);
+
+			// > eagle.alert.fail.count
+			$scope.alertFailSeries = nvd3.convert.eagle([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().tags.site,
+				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 = Policy.updatePolicyStatus;
+		$scope.deletePolicy = function(policy) {
+			Policy.deletePolicy(policy, function() {
+				location.href = "#/common/policyList";
+			});
+		};
+	});
+
+	// ======================== Policy Edit =========================
+	function policyCtrl(create, PageConfig, Site, Policy, $scope, $wrapState, $q, Entities, Application, Authorization) {
+		PageConfig.pageTitle = create ? "Policy Create" : "Policy Edit";
+		PageConfig.pageSubTitle = Site.current().tags.site;
+		PageConfig
+			.addNavPath("Policy List", "/common/policyList")
+			.addNavPath("Policy 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 = $wrapState.param.filter;
+
+		$scope.step = 0;
+		$scope.dataSources = {};
+		$scope.policy = {};
+
+		// ==========================================
+		// =            Data Preparation            =
+		// ==========================================
+		// Steam list
+		var _streamList = Entities.queryEntities("AlertStreamService", {dataSource: Application.current().tags.application});
+		var _executorList = Entities.queryEntities("AlertExecutorService", {dataSource: Application.current().tags.application});
+		$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 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: {
+							dataSource: Application.current().tags.application,
+							policyType: "siddhiCEPEngine"
+						}
+					};
+
+					// If configured data source
+					if($wrapState.param.dataSrc) {
+						$scope.policy.tags.dataSource = $wrapState.param.dataSrc;
+						if(common.array.find($wrapState.param.dataSrc, Site.current().dataSrcList, "tags.dataSource")) {
+							setTimeout(function() {
+								$scope.changeStep(0, 2, false);
+								$scope.$apply();
+							}, 1);
+						}
+					}
+
+					// Start step
+					$scope.changeStep(0, 1, false);
+					console.log($scope.policy);
+				} 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("/common/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 [\w\d_]+ (group by (\w+) )?having ([\w\d_]+) ([<>=]+) ([^\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,14);
+
+						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.__.groupAggAlias = _cond_groupUnit[4] === "aggValue" ? "" : _cond_groupUnit[4];
+										$scope.policy.__.groupCondOp = _cond_groupUnit[5];
+										$scope.policy.__.groupCondVal = _cond_groupUnit[6];
+									} 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, 2, false);
+						console.log($scope.policy);
+					});
+				}
+			});
+
+			// ==========================================
+			// =                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;
+				return common.array.find($scope.policy.__.window, $scope.config.window, "type");
+			};
+
+			// Aggregation
+			$scope.groupAggPathList = function() {
+				return $.grep(common.getValueByPath($scope, "_stream.metas", []), function(meta) {
+					return $.inArray(meta.attrType, ['long','integer','number', 'double', 'float']) !== -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().tags.site,
+					resolver: resolver,
+					query: value
+				});
+				return _resolverList._promise.then(function() {
+					return _resolverList;
+				});
+			};
+
+			// Used for input box when pressing enter
+			$scope.conditionPress = function(event) {
+				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) {
+				return {
+					key: key,
+					op: op,
+					val: value,
+					type: type,
+					ignored: function() {
+						return this.type === "bool" && this.val === "none";
+					},
+					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();
+						}
+					}
+				};
+			};
+			// 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) {
+					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 ${groupAggAlias} group by ${group} having ${groupAggAlias} ${groupCondOp} ${groupCondVal}", {
+							group: $scope.policy.__.group,
+							groupAgg: $scope.policy.__.groupAgg,
+							groupAggPath: $scope.policy.__.groupAggPath,
+							groupCondOp: $scope.policy.__.groupCondOp,
+							groupCondVal: $scope.policy.__.groupCondVal,
+							groupAggAlias: $scope.policy.__.groupAggAlias || "aggValue"
+						});
+					} else {
+						_windowStr += common.template(" select ${groupAgg}(${groupAggPath}) as ${groupAggAlias} having ${groupAggAlias} ${groupCondOp} ${groupCondVal}", {
+							groupAgg: $scope.policy.__.groupAgg,
+							groupAggPath: $scope.policy.__.groupAggPath,
+							groupCondOp: $scope.policy.__.groupCondOp,
+							groupCondVal: $scope.policy.__.groupCondVal,
+							groupAggAlias: $scope.policy.__.groupAggAlias || "aggValue"
+						});
+					}
+				} 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().tags.site;
+
+				// owner
+				$scope.policy.owner = Authorization.userProfile.username;
+
+				// Update function
+				function _updatePolicy() {
+					Entities.updateEntity("AlertDefinitionService", $scope.policy)._promise.success(function(data) {
+						$.dialog({
+							title: "Success",
+							content: (create ? "Create" : "Update") + " success!"
+						}, function() {
+							if(data.success) {
+								location.href = "#/common/policyList";
+							} else {
+								$.dialog({
+									title: "OPS",
+									content: (create ? "Create" : "Update") + "failed!" + JSON.stringify(data)
+								});
+							}
+						});
+
+						$scope.create = create = false;
+						$scope.encodedRowkey = data.obj[0];
+					}).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();
+				}
+			};
+		});
+	}
+
+	feature.controller('policyCreate', function(PageConfig, Site, Policy, $scope, $wrapState, $q, Entities, Application, Authorization) {
+		var _args = [true];
+		_args.push.apply(_args, arguments);
+		policyCtrl.apply(this, _args);
+	}, "policyEdit");
+	feature.controller('policyEdit', function(PageConfig, Site, Policy, $scope, $wrapState, $q, Entities, Application, Authorization) {
+		PageConfig.lockSite = true;
+		var _args = [false];
+		_args.push.apply(_args, arguments);
+		policyCtrl.apply(this, _args);
+	});
+
+	// ==============================================================
+	// =                           Alerts                           =
+	// ==============================================================
+
+	// ========================= Alert List =========================
+	feature.navItem("alertList", "Alerts", "exclamation-triangle");
+	feature.controller('alertList', function(PageConfig, Site, $scope, $wrapState, $interval, $timeout, Entities, Application) {
+		PageConfig.pageSubTitle = Site.current().tags.site;
+
+		var MAX_PAGESIZE = 10000;
+
+		// Initial load
+		$scope.application = Application.current();
+
+		$scope.alertList = [];
+		$scope.alertList.ready = false;
+
+		// Load data
+		function _loadAlerts() {
+			if($scope.alertList._promise) {
+				$scope.alertList._promise.abort();
+			}
+
+			var _list = Entities.queryEntities("AlertService", {
+				site: Site.current().tags.site,
+				dataSource: $scope.application.tags.application,
+				hostname: null,
+				_pageSize: MAX_PAGESIZE,
+				_duration: 1000 * 60 * 60 * 24 * 30,
+				__ETD: 1000 * 60 * 60 * 24
+			});
+			$scope.alertList._promise = _list._promise;
+			_list._promise.then(function() {
+				var index;
+
+				if($scope.alertList[0]) {
+					// List new alerts
+					for(index = 0 ; index < _list.length ; index += 1) {
+						var _alert = _list[index];
+						_alert.__new = true;
+						if(_alert.encodedRowkey === $scope.alertList[0].encodedRowkey) {
+							break;
+						}
+					}
+
+					if(index > 0) {
+						$scope.alertList.unshift.apply($scope.alertList, _list.slice(0, index));
+
+						// Clean up UI highlight
+						$timeout(function() {
+							$.each(_list, function(i, alert) {
+								delete alert.__new;
+							});
+						}, 100);
+					}
+				} else {
+					// List all alerts
+					$scope.alertList.push.apply($scope.alertList, _list);
+				}
+
+				$scope.alertList.ready = true;
+			});
+		}
+
+		_loadAlerts();
+		var _loadInterval = $interval(_loadAlerts, app.time.refreshInterval);
+		$scope.$on('$destroy',function(){
+			$interval.cancel(_loadInterval);
+		});
+	});
+
+	// ========================= Alert List =========================
+	feature.controller('alertDetail', function(PageConfig, Site, $scope, $wrapState, Entities) {
+		PageConfig.pageTitle = "Alert Detail";
+		PageConfig.lockSite = true;
+		PageConfig
+			.addNavPath("Alert List", "/common/alertList")
+			.addNavPath("Alert Detail");
+
+		$scope.common = common;
+
+		// Query policy
+		$scope.alertList = Entities.queryEntity("AlertService", $wrapState.param.filter);
+		$scope.alertList._promise.then(function() {
+			if($scope.alertList.length === 0) {
+				$.dialog({
+					title: "OPS!",
+					content: "Alert not found!"
+				}, function() {
+					location.href = "#/common/alertList";
+				});
+			} else {
+				$scope.alert = $scope.alertList[0];
+				Site.current(Site.find($scope.alert.tags.site));
+				console.log($scope.alert);
+			}
+		});
+
+		// UI
+		$scope.getMessageTime = function(alert) {
+			var _time = common.getValueByPath(alert, "alertContext.properties.timestamp");
+			return Number(_time);
+		};
+	});
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/d37643b0/eagle-webservice/src/main/webapp/app/public/feature/common/page/alertDetail.html
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/app/public/feature/common/page/alertDetail.html b/eagle-webservice/src/main/webapp/app/public/feature/common/page/alertDetail.html
new file mode 100644
index 0000000..fee5ab1
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/app/public/feature/common/page/alertDetail.html
@@ -0,0 +1,61 @@
+<!--
+  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="#/common/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/d37643b0/eagle-webservice/src/main/webapp/app/public/feature/common/page/alertList.html
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/app/public/feature/common/page/alertList.html b/eagle-webservice/src/main/webapp/app/public/feature/common/page/alertList.html
new file mode 100644
index 0000000..44f5a32
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/app/public/feature/common/page/alertList.html
@@ -0,0 +1,69 @@
+<!--
+  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">
+			{{application.displayName}}
+		</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="#/common/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="#/common/alertDetail/{{item.encodedRowkey}}">Detail</a></td>
+					</tr>
+				</tbody>
+			</table>
+		</div>
+
+	</div>
+	<!--div class="box-footer clearfix">
+	</div-->
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/d37643b0/eagle-webservice/src/main/webapp/app/public/feature/common/page/dataSrcConfig.html
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/app/public/feature/common/page/dataSrcConfig.html b/eagle-webservice/src/main/webapp/app/public/feature/common/page/dataSrcConfig.html
new file mode 100644
index 0000000..ba4f728
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/app/public/feature/common/page/dataSrcConfig.html
@@ -0,0 +1,125 @@
+<div class="box box-primary" ng-repeat="_site in Site.list">
+	<div class="box-header">
+		<h3 class="box-title">{{_site.name}}</h3>
+		<div class="box-tools pull-right">
+			<button class="btn btn-box-tool" data-widget="collapse">
+				<i class="fa fa-minus"></i>
+			</button>
+			<button class="btn btn-box-tool" ng-click="showSiteEditor(_site)">
+				<i class="fa fa-wrench"></i>
+			</button>
+		</div>
+	</div><!-- /.box-header -->
+	<div class="box-body">
+		<div class="row">
+			<div class="col-md-4" ng-repeat="dataSrc in _site.dataSrcList">
+				<div class="info-box" ng-class="dataSrc.enabled !== false ? 'bg-aqua' : 'bg-gray'">
+					<span class="info-box-icon"><i class="fa fa-suitcase"></i></span>
+					<div class="info-box-content">
+						<a class="fa fa-cog config pull-right" ng-click="showDataSourceEditor(dataSrc)"></a>
+						<span class="info-box-text">{{dataSrc.desc || dataSrc.tags.dataSource}}</span>
+
+						<span class="info-box-number" ng-show="dataSrc.hide"><small>-</small></span>
+						<span class="info-box-number" ng-show="!dataSrc.hide">
+							<span class="fa fa-refresh fa-spin" ng-hide="policyStatistic._promise.$$state.status === 1"></span>
+							<span ng-show="policyStatistic._promise.$$state.status === 1">{{getPolicyCount(_site.name, dataSrc.tags.dataSource)}}</span>
+							<small>Policies</small>
+						</span>
+						<div class="progress">
+							<div class="progress-bar"></div>
+						</div>
+						<span class="progress-description" ng-show="dataSrc.hide"><small>-</small></span>
+						<span class="progress-description" ng-show="!dataSrc.hide">
+							<span class="fa fa-refresh fa-spin" ng-hide="alertStatistic._promise.$$state.status === 1"></span>
+							<span ng-show="alertStatistic._promise.$$state.status === 1">{{getAlertCount(_site.name, dataSrc.tags.dataSource)}}</span>
+							alerts in 30 Days
+						</span>
+					</div><!-- /.info-box-content -->
+				</div>
+			</div>
+		</div>
+	</div>
+</div>
+
+<!-- Modal: Create / Edit site -->
+<div class="modal fade" id="siteMDL" tabindex="-1" role="dialog">
+	<div class="modal-dialog" 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">{{!_siteEntity.srcSite ? 'Create' : 'Config'}} Site</h4>
+			</div>
+			<div class="modal-body">
+				<div class="form-group">
+					<label>* Site Name</label>
+					<input type="text" class="form-control" placeholder="Site name..." ng-model="_siteEntity.name" ng-disabled="!!_siteEntity.srcSite" id="siteName">
+				</div>
+
+				<label>
+					* Data Source
+					<small class="text-muted">at least select 1 source</small>
+				</label>
+				<div class="checkbox" ng-repeat="item in _siteEntity.dataSrcList">
+					<label>
+						<input type="checkbox" value="{{item.name}}"
+							   ng-checked="item.enabled"
+							   ng-click="item.enabled = !item.enabled">
+						{{item.name}}
+					</label>
+				</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="confirmUpateSite()"
+						ng-disabled="!checkUpdateSite()">
+					{{!_siteEntity.srcSite ? 'Create' : 'Update'}}
+				</button>
+			</div>
+		</div>
+	</div>
+</div>
+
+<!-- Modal: Edit data source -->
+<div class="modal fade" id="dataSrcMDL" tabindex="-1" role="dialog">
+	<div class="modal-dialog" 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">{{_dataSrcEntity.tags.dataSource}}</h4>
+			</div>
+			<div class="modal-body">
+				<div class="checkbox">
+					<label>
+						<input type="checkbox"
+							   ng-checked="_dataSrcEntity.enabled"
+							   ng-click="_dataSrcEntity.enabled = !_dataSrcEntity.enabled">
+						Enabled
+					</label>
+				</div>
+
+				<div class="form-group">
+					<label>Configuration</label>
+					<textarea type="text" class="form-control" placeholder="Data source configuration..." ng-model="_dataSrcEntity.config" id="dataSrcConfig" rows="10"></textarea>
+				</div>
+			</div>
+			<div class="modal-footer">
+				<button type="button" class="btn btn-danger pull-left" ng-click="confirmDeleteDataSource()">
+					Delete Source
+				</button>
+
+				<button type="button" class="btn btn-default" data-dismiss="modal">
+					Close
+				</button>
+				<button type="button" class="btn btn-primary" ng-click="confirmUpateDataSource()" ng-disabled="_dataSrcEntityLock">
+					Update
+				</button>
+			</div>
+		</div>
+	</div>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/d37643b0/eagle-webservice/src/main/webapp/app/public/feature/common/page/policyDetail.html
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/app/public/feature/common/page/policyDetail.html b/eagle-webservice/src/main/webapp/app/public/feature/common/page/policyDetail.html
new file mode 100644
index 0000000..8670839
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/app/public/feature/common/page/policyDetail.html
@@ -0,0 +1,172 @@
+<!--
+  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">
+			{{policy.tags.policyId}}
+			<small>{{policy.tags.site}}</small>
+		</h3>
+	</div>
+
+	<div class="box-body">
+		<div class="row">
+			<div class="col-xs-8">
+				<div class="inline-group">
+					<dl><dt>Data Source</dt><dd>{{policy.tags.dataSource}}</dd></dl>
+					<dl><dt>Status</dt><dd>
+						<span ng-show="policy.enabled" class="text-muted"><i class="fa fa-square text-green"></i> Enabled</span>
+						<span ng-show="!policy.enabled" class="text-muted"><i class="fa fa-square text-muted"></i> Disabled</span>
+					</dd></dl>
+				</div>
+				<div class="inline-group">
+					<dl><dt>Description</dt><dd>{{policy.desc}}</dd></dl>
+				</div>
+				<div class="inline-group">
+					<dl><dt>Alert</dt><dd>
+						<a href="mailto:{{mail}}" ng-repeat="mail in policy.__mailList track by $index" style="margin-right: 10px;">{{mail}}</a>
+					</dd></dl>
+				</div>
+			</div>
+			<div class="col-xs-4 text-right" ng-show="Auth.isRole('ROLE_ADMIN')">
+				<a class="btn btn-primary" href="#/common/policyEdit/{{policy.encodedRowkey}}">Edit</a>
+				<button class="btn btn-warning" ng-show="!policy.enabled" ng-click="updatePolicyStatus(policy, true)">Enable</button>
+				<button class="btn btn-warning" ng-show="policy.enabled" ng-click="updatePolicyStatus(policy, false)">Disable</button>
+				<button class="btn btn-danger" ng-click="deletePolicy(policy)">Delete</button>
+			</div>
+		</div>
+	</div>
+
+	<div class="overlay" ng-hide="policyList._promise.$$state.status === 1;">
+		<i class="fa fa-refresh fa-spin"></i>
+	</div>
+
+	<div class="box-footer clearfix">
+		<a data-toggle="collapse" href="[data-id='query']">
+			View Query
+		</a>
+		<div data-id="query" class="collapse in">
+			<pre>{{policy.__expression}}</pre>
+		</div>
+	</div>
+</div>
+
+<div class="nav-tabs-custom">
+	<ul class="nav nav-tabs">
+		<li class="active">
+			<a href="[data-id='visualization']" data-toggle="tab">Visualization</a>
+		</li>
+		<li>
+			<a href="[data-id='statistics']" data-toggle="tab">Statistics</a>
+		</li>
+		<li>
+			<a href="[data-id='alerts']" data-toggle="tab">Alerts</a>
+		</li>
+	</ul>
+	<div class="tab-content">
+		<div class="tab-pane active" data-id="visualization">
+			<div class="row">
+				<div class="col-xs-6">
+					<div nvd3="policyEvalSeries" data-title="Policy Eval Count" data-config="chartConfig" class="nvd3-chart-cntr"></div>
+				</div>
+				<div class="col-xs-6">
+					<div nvd3="policyEvalFailSeries" data-title="Policy Eval Fail Count" data-config="chartConfig" class="nvd3-chart-cntr"></div>
+				</div>
+				<div class="col-xs-6">
+					<div nvd3="alertSeries" data-title="Alert Count" data-config="chartConfig" class="nvd3-chart-cntr"></div>
+				</div>
+				<div class="col-xs-6">
+					<div nvd3="alertFailSeries" data-title="Alert Fail Count" data-config="chartConfig" class="nvd3-chart-cntr"></div>
+				</div>
+			</div>
+		</div>
+
+		<div class="tab-pane" data-id="statistics">
+			<div class="row">
+				<div class="col-xs-3">
+					<div class="info-box bg-aqua">
+						<span class="info-box-icon"><i class="fa fa-bookmark-o"></i></span>
+						<div class="info-box-content">
+							<span class="info-box-text">Policy Eval Count</span>
+							<span class="info-box-number">{{common.array.sum(policyEvalSeries, "1")}} <small>(Monthly)</small></span>
+							<span class="info-box-number">{{policyEvalSeries[policyEvalSeries.length - 1][1]}} <small>(Daily)</small></span>
+						</div>
+					</div>
+				</div>
+				<div class="col-xs-3">
+					<div class="info-box bg-red">
+						<span class="info-box-icon"><i class="fa fa-bookmark-o"></i></span>
+						<div class="info-box-content">
+							<span class="info-box-text">Policy Eval Fail Count</span>
+							<span class="info-box-number">{{common.array.sum(policyEvalFailSeries, "1")}} <small>(Monthly)</small></span>
+							<span class="info-box-number">{{policyEvalFailSeries[policyEvalFailSeries.length - 1][1]}} <small>(Daily)</small></span>
+						</div>
+					</div>
+				</div>
+				<div class="col-xs-3">
+					<div class="info-box bg-aqua">
+						<span class="info-box-icon"><i class="fa fa-bookmark-o"></i></span>
+						<div class="info-box-content">
+							<span class="info-box-text">Alert Count</span>
+							<span class="info-box-number">{{common.array.sum(alertSeries, "1")}} <small>(Monthly)</small></span>
+							<span class="info-box-number">{{alertSeries[alertSeries.length - 1][1]}} <small>(Daily)</small></span>
+						</div>
+					</div>
+				</div>
+				<div class="col-xs-3">
+					<div class="info-box bg-red">
+						<span class="info-box-icon"><i class="fa fa-bookmark-o"></i></span>
+						<div class="info-box-content">
+							<span class="info-box-text">Alert Fail Count</span>
+							<span class="info-box-number">{{common.array.sum(alertFailSeries, "1")}} <small>(Monthly)</small></span>
+							<span class="info-box-number">{{alertFailSeries[alertFailSeries.length - 1][1]}} <small>(Daily)</small></span>
+						</div>
+					</div>
+				</div>
+			</div>
+		</div>
+
+		<div class="tab-pane" data-id="alerts">
+			<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="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.alertContext.properties.user}}</td>
+							<td>{{item.alertContext.properties.host}}</td>
+							<td>{{item.alertContext.properties.alertMessage}}</td>
+							<td><a href="#/common/alertDetail/{{item.encodedRowkey}}">Detail</a></td>
+						</tr>
+					</tbody>
+				</table>
+			</div>
+		</div>
+
+	</div>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/d37643b0/eagle-webservice/src/main/webapp/app/public/feature/common/page/policyEdit.html
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/app/public/feature/common/page/policyEdit.html b/eagle-webservice/src/main/webapp/app/public/feature/common/page/policyEdit.html
new file mode 100644
index 0000000..e07cb0c
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/app/public/feature/common/page/policyEdit.html
@@ -0,0 +1,359 @@
+<!--
+  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="progress active" ng-show="!streamReady">
+	<div class="progress-bar progress-bar-primary progress-bar-striped" style="width: 100%">
+	</div>
+</div>
+
+<!-- Step navigation -->
+<div ng-show="streamReady">
+	<div class="row step-cntr">
+		<div class="col-sm-4" ng-repeat="step in steps">
+			<div class="step" ng-class="stepSelect($index + 1)">
+				<h1>{{$index + 1}}</h1>
+				<h2>Step {{$index + 1}}</h2>
+				<p title="{{step.title}}">{{step.title}}</p>
+			</div>
+		</div>
+	</div>
+
+	<!-- Step container -->
+	<div class="box box-info">
+		<div class="box-header with-border">
+			<h3 class="box-title">Step {{step}} - {{steps[step - 1].title}}</h3>
+		</div><!-- /.box-header -->
+
+		<div class="box-body">
+			<!-- ---------------------- Step Body Start ---------------------- -->
+
+			<!-- Step 1: Data Source -->
+			<!--div ng-show="step === 1">
+				<div class="form-group">
+					<label>Select Data Source</label>
+					<select class="form-control" ng-model="policy.tags.dataSource">
+						<option ng-repeat="dataSource in Site.current().dataSrcList" ng-hide="dataSource.hide" value="{{dataSource.tags.dataSource}}">{{dataSource.desc || dataSource.tags.dataSource}}</option>
+					</select>
+				</div>
+			</div-->
+
+			<!-- Step 1: Stream -->
+			<div ng-show="step === 1">
+				<div class="pull-right" ng-show="policy.__.advanced === undefined">
+					<span class="text-muted">or</span>
+					<a ng-click="policy.__.advanced = true;">Advanced</a>
+				</div>
+
+				<div class="form-group">
+					<label>Select Stream</label>
+					<select class="form-control" ng-model="policy.__.streamName" ng-show="!policy.__.advanced">
+						<option ng-repeat="stream in dataSources[policy.tags.dataSource]">{{stream.tags.streamName}}</option>
+					</select>
+					<select class="form-control" ng-show="policy.__.advanced" disabled="disabled">
+						<option>[Advanced Mode]</option>
+					</select>
+				</div>
+
+				<div class="checkbox" ng-show="policy.__.advanced !== undefined">
+					<label>
+						<input type="checkbox" ng-model="policy.__.advanced">
+						Advanced Mode
+					</label>
+				</div>
+			</div>
+
+			<!-- Step 2: Define Alert Policy -->
+			<div ng-show="step === 2 && !policy.__.advanced">
+				<!-- Criteria -->
+				<div>
+					<label>Match Criteria</label>
+					<a ng-click="collapse('.panel-group')">expand / collapse all</a>
+
+					<div class="panel-group panel-group-sm" role="tablist">
+						<div class="panel panel-default"
+							ng-repeat="meta in _stream.metas"
+							ng-init="op = '=='; val = null; type = (meta.attrType || '').toLowerCase();">
+							<div class="panel-heading" role="tab">
+								<h4 class="panel-title">
+									<span class="bg-navy disabled color-palette pull-right">
+										{{parseConditionDesc(meta.tags.attrName)}}
+									</span>
+
+									<a role="button" data-toggle="collapse" href="[data-name='{{meta.tags.attrName}}']" class="collapsed">
+										<span class="fa fa-square" ng-class="hasCondition(meta.tags.attrName, type) ? 'text-green' : 'text-muted'"> </span>
+										{{meta.attrDisplayName || meta.tags.attrName}}
+										<span class="fa fa-question-circle" ng-show="meta.attrDescription"
+										uib-tooltip="{{meta.attrDescription}}" tooltip-placement="right" tooltip-animation="false"> </span>
+									</a>
+								</h4>
+							</div>
+							<div data-name="{{meta.tags.attrName}}" data-type="{{meta.attrType}}" role="tabpanel" class="collapse">
+								<div class="panel-body">
+									<ul ng-show="type !== 'bool'">
+										<li ng-repeat="cond in policy.__.conditions[meta.tags.attrName]">
+											[<a ng-click="policy.__.conditions[meta.tags.attrName].splice($index, 1)">X</a>]
+											{{cond.toString()}}
+										</li>
+									</ul>
+
+									<!-- String -->
+									<div ng-if="type == 'string'">
+										<div class="input-group" style="max-width: 450px;">
+											<div class="input-group-btn">
+												<select class="form-control" ng-model="op">
+													<option ng-repeat="mark in ['==','!=','contains','regex']">{{mark}}</option>
+												</select>
+											</div>
+
+											<!-- With resolver -->
+											<input type="text" class="form-control" autocomplete="off" ng-model="val" ng-show="meta.attrValueResolver"
+												ng-keypress="conditionPress($event, meta.tags.attrName, op, val, type)"
+												uib-typeahead="item for item in resolverTypeahead($viewValue, meta.attrValueResolver)">
+											<!-- Without resolver -->
+											<input type="text" class="form-control" autocomplete="off" ng-model="val" ng-show="!meta.attrValueResolver"
+												ng-keypress="conditionPress($event, meta.tags.attrName, op, val, type)">
+
+											<span class="input-group-btn">
+												<button class="btn btn-info btn-flat" type="button" ng-click="addCondition(meta.tags.attrName, op, val, type);val=null;">Add</button>
+											</span>
+										</div>
+									</div>
+
+									<!-- Number -->
+									<div ng-if="type == 'long' || type == 'integer' || type == 'number' || type == 'double' || type == 'float'">
+										<div class="input-group" style="max-width: 450px;">
+											<div class="input-group-btn">
+												<select class="form-control" ng-model="op">
+													<option ng-repeat="mark in ['==','!=','>','>=','<','<=']">{{mark}}</option>
+												</select>
+											</div>
+
+											<input type="number" class="form-control" autocomplete="off" placeholder="Number Only..." ng-model="val" ng-keypress="conditionPress($event, meta.tags.attrName, op, val, type)">
+											<span class="input-group-btn">
+												<button class="btn btn-info btn-flat" type="button" ng-click="addCondition(meta.tags.attrName, op, val, type) ? val=null : void(0);">Add</button>
+											</span>
+										</div>
+									</div>
+
+									<!-- Boolean -->
+									<div ng-if="type == 'bool'" ng-init="policy.__.conditions[meta.tags.attrName] = policy.__.conditions[meta.tags.attrName] || [_CondUnit(meta.tags.attrName, '==', 'none', 'bool')]">
+										<select class="form-control" ng-model="policy.__.conditions[meta.tags.attrName][0].val" style="max-width: 100px;">
+											<option ng-repeat="bool in ['none','true','false']">{{bool}}</option>
+										</select>
+									</div>
+								</div>
+							</div>
+						</div>
+					</div>
+				</div>
+
+				<!-- Window -->
+				<div class="checkbox">
+					<label>
+						<input type="checkbox" ng-checked="policy.__.windowConfig" ng-click="policy.__.windowConfig = !policy.__.windowConfig"> Slide Window
+					</label>
+				</div>
+				<div ng-show="policy.__.windowConfig">
+					<div class="row">
+						<div class="col-md-4">
+							<div class="form-group">
+								<label>Window</label>
+								<select class="form-control" ng-model="policy.__.window"
+								uib-tooltip="{{getWindow().description}}" tooltip-animation="false">
+									<option ng-repeat="item in config.window" value="{{item.type}}">{{item.title}}</option>
+								</select>
+							</div>
+						</div>
+
+						<!-- fields -->
+						<div class="col-md-4" ng-repeat="field in getWindow().fields" ng-init="field.val = field.val || (field.defaultValue || '');" ng-hide="field.hide">
+							<div class="form-group" ng-class="{'has-warning' : !field.val || !field.val.match(field.regex)}">
+								<label>Window - {{field.title}}</label>
+								<input type="text" class="form-control" autocomplete="off" placeholder="{{field.description}}" ng-model="field.val" title="{{field.description}}">
+							</div>
+						</div>
+					</div>
+
+					<!-- Aggregation -->
+					<div class="row">
+						<div class="col-md-4">
+							<div class="form-group" ng-class="{'text-yellow' : (policy.__.groupAgg && !policy.__.groupAggPath)}">
+								<label>Aggregation</label>
+								<div class="input-group">
+									<div class="input-group-btn">
+										<select class="form-control" ng-model="policy.__.groupAgg" ng-change="updateGroupAgg()">
+											<option ng-repeat="op in ['max','min','avg','count', 'sum']">{{op}}</option>
+										</select>
+									</div>
+									<select class="form-control" ng-model="policy.__.groupAggPath" ng-class="{'has-warning' : !policy.__.groupAggPath}" id="groupAggPath"
+											ng-show="policy.__.groupAgg" ng-disabled="policy.__.groupAgg === 'count'">
+										<option ng-repeat="meta in groupAggPathList()">{{meta.tags.attrName}}</option>
+									</select>
+								</div>
+							</div>
+						</div>
+
+						<div class="col-md-4">
+							<div class="form-group" ng-class="{'text-yellow' : (!policy.__.groupCondOp || !policy.__.groupCondVal)}">
+								<label>Condition</label>
+								<div class="input-group">
+									<div class="input-group-btn">
+										<select class="form-control" ng-model="policy.__.groupCondOp" ng-class="{'has-warning' : !policy.__.groupCondOp}">
+											<option ng-repeat="op in ['>','<','>=','<=','==']">{{op}}</option>
+										</select>
+									</div>
+									<input type="text" class="form-control" ng-model="policy.__.groupCondVal" ng-class="{'has-warning' : !policy.__.groupCondVal}" />
+								</div>
+							</div>
+						</div>
+
+						<div class="col-md-4">
+							<div class="form-group">
+								<label>Alias (Optional)</label>
+								<input type="text" class="form-control" ng-model="policy.__.groupAggAlias" placeholder="Default: aggValue" />
+							</div>
+						</div>
+					</div>
+
+					<!-- Group -->
+					<div class="row">
+						<div class="col-md-4">
+							<div class="form-group">
+								<label>Group By</label>
+								<select class="form-control" ng-model="policy.__.group">
+									<option value="">None</option>
+									<option ng-repeat="meta in _stream.metas">{{meta.tags.attrName}}</option>
+								</select>
+							</div>
+						</div>
+					</div>
+				</div>
+			</div>
+
+			<!-- Step 2: Define Alert Policy -->
+			<div ng-show="step === 2 && policy.__.advanced">
+				<div class="form-group">
+					<label>Query Expression</label>
+					<textarea class="form-control" ng-model="policy.__._expression"
+					placeholder="Query expression. e.g. from hdfsAuditLogEventStream[(cmd=='open') and (host=='localhost' or host=='127.0.0.1')]#window.time(2 sec) select * insert into outputStream;" rows="5"></textarea>
+				</div>
+			</div>
+
+			<!-- Step 3: Email Notification -->
+			<div ng-show="step === 3">
+				<div class="row">
+					<div class="col-xs-4">
+						<div class="form-group" ng-class="{'has-warning' : !policy.tags.policyId}">
+							<label>Policy Name</label>
+							<input type="text" class="form-control" placeholder="" ng-model="policy.tags.policyId" ng-disabled="!create">
+						</div>
+					</div>
+					<div class="col-xs-3">
+						<div class="form-group">
+							<label>
+								Alert De-Dup Interval(min)
+								<span class="fa fa-question-circle" uib-tooltip="Same type alert will be De-dup in configured interval"> </span>
+							</label>
+							<input type="number" class="form-control" ng-model="policy.__.dedupe.alertDedupIntervalMin" placeholder="[Minute] Number only. Suggestion: 720">
+						</div>
+					</div>
+					<div class="col-xs-3">
+						<div class="form-group">
+							<label>
+								Email De-Dup Interval(min)
+								<span class="fa fa-question-circle" uib-tooltip="The minimun time interval of email"> </span>
+							</label>
+							<input type="number" class="form-control" ng-model="policy.__.dedupe.emailDedupIntervalMin" placeholder="[Minute] Number only. Suggestion: 1440">
+						</div>
+					</div>
+					<div class="col-xs-2">
+						<div class="form-group">
+							<label>
+								Enabled
+							</label>
+							<p>
+								<input type="checkbox" checked="checked" ng-model="policy.enabled">
+							</p>
+						</div>
+					</div>
+				</div>
+
+				<div>
+					<a data-toggle="collapse" href="[data-id='advancedDeDup']">Advanced De-Dup</a>
+					<div data-id='advancedDeDup' class="collapse">
+						<label>
+							De-Dup Key
+							<span class="fa fa-question-circle" uib-tooltip="Type of grouping alerts. If you don't know how to config, leave it default."> </span>
+						</label>
+						<div class="form-group">
+							<div class="checkbox" ng-repeat="meta in _stream.metas" ng-init="create ? policy.__._dedupTags[meta.tags.attrName] = !!meta.usedAsTag : void(0);">
+								<label>
+									<input type="checkbox" ng-model="policy.__._dedupTags[meta.tags.attrName]">
+									{{meta.tags.attrName}}
+								</label>
+							</div>
+						</div>
+					</div>
+				</div>
+
+				<hr/>
+
+				<div class="form-group">
+					<label>Sender</label>
+					<input type="text" class="form-control" value="noreply-hadoop-eagle@company1.com" placeholder="Enter Sender. e.g. sender@eaxmple.com" ng-model="policy.__.notification[0].sender">
+				</div>
+				<div class="form-group">
+					<label>Recipients</label>
+					<input type="text" class="form-control" placeholder="Enter Recipients. Split with ','. e.g. usera@example.com, userb@example.com" ng-model="policy.__.notification[0].recipients">
+				</div>
+				<div class="form-group">
+					<label>Subject</label>
+					<input type="text" class="form-control" placeholder="Enter Subject" ng-model="policy.__.notification[0].subject">
+				</div>
+				<div class="form-group">
+					<label>Description</label>
+					<textarea class="form-control" placeholder="Policy description" ng-model="policy.desc"></textarea>
+				</div>
+
+				<a data-toggle="collapse" href="[data-id='policyQuery']">
+					View Query
+				</a>
+				<div class="collapse in" data-id="policyQuery">
+					<pre>{{toQuery()}}</pre>
+				</div>
+			</div>
+
+			<!-- ----------------------- Step Body End ----------------------- -->
+		</div><!-- /.box-body -->
+
+		<div class="overlay" ng-hide="stepReady(step)">
+			<span class="fa fa-refresh fa-spin"> </span>
+		</div>
+
+		<div class="box-footer text-right">
+			<button class="btn btn-info" ng-show="step > 1" ng-click="changeStep(step, step - 1, false)" ng-disabled="lock">
+				Prev <span class="fa fa-arrow-circle-o-left"> </span>
+			</button>
+			<button class="btn btn-info" ng-show="step < steps.length" ng-click="changeStep(step, step + 1)" ng-disabled="checkNextable(step) || lock">
+				Next <span class="fa fa-arrow-circle-o-right"> </span>
+			</button>
+			<button class="btn btn-info" ng-show="step === steps.length" ng-click="finishPolicy()" ng-disabled="checkNextable(step) || lock">
+				Done <span class="fa fa-check-circle-o"> </span>
+			</button>
+		</div>
+	</div>
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/d37643b0/eagle-webservice/src/main/webapp/app/public/feature/common/page/policyList.html
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/app/public/feature/common/page/policyList.html b/eagle-webservice/src/main/webapp/app/public/feature/common/page/policyList.html
new file mode 100644
index 0000000..d7141a6
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/app/public/feature/common/page/policyList.html
@@ -0,0 +1,84 @@
+<!--
+  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">
+			{{application.displayName}}
+		</h3>
+	</div>
+	<div class="box-body">
+		<div class="row">
+			<div class="col-xs-3">
+				<div class="search-box">
+					<input type="search" class="form-control input-sm" placeholder="Search" ng-model="search" />
+					<span class="fa fa-search"> </span>
+				</div>
+			</div>
+			<div class="col-xs-6">
+				<div class="inline-group form-inline text-muted">
+					<dl><dt><i class="fa fa-square text-green"> </i></dt><dd>Enabled</dd></dl>
+					<dl><dt><i class="fa fa-square text-muted"> </i></dt><dd>Disabled</dd></dl>
+				</div>
+			</div>
+			<div class="col-xs-3 text-right">
+				<a class="btn btn-primary" href="#/common/policyCreate/{{!dataSource ? '' : '?dataSrc=' + dataSource.tags.dataSource}}" ng-show="Auth.isRole('ROLE_ADMIN')">
+					New Policy
+					<i class="fa fa-plus-circle"> </i>
+				</a>
+			</div>
+		</div>
+
+		<p ng-show="policyList._promise.$$state.status !== 1">
+			<span class="fa fa-refresh fa-spin"> </span>
+			Loading...
+		</p>
+
+		<div sorttable source="policyList" ng-show="policyList._promise.$$state.status === 1" search="false" searchfunc="searchFunc">
+			<table class="table table-bordered" ng-non-bindable>
+				<thead>
+					<tr>
+						<th width="30" sortpath="enabled"> </th>
+						<th width="200" sortpath="tags.policyId">Policy Name</th>
+						<th sortpath="desc">Description</th>
+						<th width="150" sortpath="owner">Owner</th>
+						<th width="170" sortpath="lastModifiedDate">Last Modified</th>
+						<th width="95" ng-show="_parent.Auth.isRole('ROLE_ADMIN')">Action</th>
+					</tr>
+				</thead>
+				<tbody>
+					<tr>
+						<td><span class='fa fa-square' ng-class="item.enabled ? 'text-green' : 'text-muted'"> </span></td>
+						<td><a href="#/common/policyDetail/{{item.encodedRowkey}}">{{item.tags.policyId}}</a></td>
+						<td>{{item.desc}}</td>
+						<td>{{item.owner}}</td>
+						<td>{{common.format.date(item.lastModifiedDate) || "-"}}</td>
+						<td ng-show="_parent.Auth.isRole('ROLE_ADMIN')">
+							<a class="fa fa-pencil btn btn-default btn-xs" uib-tooltip="Edit" tooltip-animation="false" href="#/common/policyEdit/{{item.encodedRowkey}}"> </a>
+							<button class="fa fa-play sm btn btn-default btn-xs" uib-tooltip="Enable" tooltip-animation="false" ng-show="!item.enabled" ng-click="_parent.updatePolicyStatus(item, true)"> </button>
+							<button class="fa fa-pause sm btn btn-default btn-xs" uib-tooltip="Disable" tooltip-animation="false" ng-show="item.enabled" ng-click="_parent.updatePolicyStatus(item, false)"> </button>
+							<button class="rm fa fa-trash-o btn btn-danger btn-xs" uib-tooltip="Delete" tooltip-animation="false" ng-click="_parent.deletePolicy(item)"> </button>
+						</td>
+					</tr>
+				</tbody>
+			</table>
+		</div>
+	</div>
+	<!--div class="box-footer clearfix">
+	</div-->
+</div>

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/d37643b0/eagle-webservice/src/main/webapp/app/public/feature/metadata/controller.js
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/app/public/feature/metadata/controller.js b/eagle-webservice/src/main/webapp/app/public/feature/metadata/controller.js
new file mode 100644
index 0000000..2797223
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/app/public/feature/metadata/controller.js
@@ -0,0 +1,66 @@
+/*
+ * 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("metadata");
+
+	// ==============================================================
+	// =                          Function                          =
+	// ==============================================================
+
+	// ==============================================================
+	// =                          Metadata                          =
+	// ==============================================================
+
+	// ======================= Policy Summary =======================
+	feature.navItem("streamList", "Metadata", "bullseye");
+	feature.controller('streamList', function(PageConfig, Site, $scope, $q, Application, Entities) {
+		PageConfig.hideSite = true;
+
+		$scope.streams = {};
+		$scope._streamEntity = null;
+		$scope._streamEntityLock = false;
+
+		// =========================================== List ===========================================
+		var _streamList = Entities.queryEntities("AlertStreamService", {dataSource: Application.current().tags.application});
+		var _streamSchemaList = Entities.queryEntities("AlertStreamSchemaService", {dataSource: Application.current().tags.application});
+		$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() {
+			$.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);
+				}
+			});
+		});
+	});
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/d37643b0/eagle-webservice/src/main/webapp/app/public/feature/metadata/page/streamList.html
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/app/public/feature/metadata/page/streamList.html b/eagle-webservice/src/main/webapp/app/public/feature/metadata/page/streamList.html
new file mode 100644
index 0000000..9218d8d
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/app/public/feature/metadata/page/streamList.html
@@ -0,0 +1,84 @@
+<!--
+  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.
+  -->
+<p ng-show="streamList._promise.$$state.status !== 1">
+	<span class="fa fa-refresh fa-spin"> </span>
+	Loading...
+</p>
+
+<div class="box box-primary" ng-repeat="stream in streams">
+	<div class="box-header with-border">
+		<h3 class="box-title">{{stream.tags.streamName}}</h3>
+	</div>
+	<div class="box-body">
+		<div class="inline-group">
+			<dl>
+				<dt>
+					Data Source
+				</dt>
+				<dd>
+					{{stream.tags.dataSource}}
+				</dd>
+			</dl>
+			<dl>
+				<dt>
+					Stream Name
+				</dt>
+				<dd>
+					{{stream.tags.streamName}}
+				</dd>
+			</dl>
+		</div>
+		<div>
+			<dl>
+				<dt>
+					Description
+				</dt>
+				<dd>
+					{{stream.desc}}
+				</dd>
+			</dl>
+		</div>
+
+		<p ng-show="streamSchemaList._promise.$$state.status !== 1">
+			<span class="fa fa-refresh fa-spin"> </span>
+			Loading...
+		</p>
+
+		<div class="list" ng-show="streamSchemaList._promise.$$state.status === 1">
+			<table class="table table-bordered">
+				<thead>
+					<tr>
+						<th width="10%">Attribute Name</th>
+						<th width="12%">Display Name</th>
+						<th width="8%">Type</th>
+						<th>Description</th>
+					</tr>
+				</thead>
+				<tbody>
+					<tr ng-repeat="meta in stream.metaList">
+						<td>{{meta.tags.attrName}}</td>
+						<td>{{meta.attrDisplayName}}</td>
+						<td><span class="label label-warning">{{meta.attrType}}</span></td>
+						<td>{{meta.attrDescription}}</td>
+					</tr>
+				</tbody>
+			</table>
+		</div>
+	</div><!-- /.box-body -->
+	<!-- Loading (remove the following to stop the loading)-->
+</div>
\ No newline at end of file


Mime
View raw message