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

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

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/js/app.time.js
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/_app/public/js/app.time.js b/eagle-webservice/src/main/webapp/_app/public/js/app.time.js
new file mode 100644
index 0000000..f5f41c1
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/_app/public/js/app.time.js
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+
+// Time Zone
+(function() {
+	"use strict";
+
+	app.time = {
+		UTC_OFFSET: 0,
+		now: function() {
+			return app.time.offset();
+		},
+		offset: function(time) {
+			// Parse string number
+			if(typeof time === "string" && !isNaN(+time)) {
+				time = +time;
+			}
+
+			var _mom = new moment(time);
+			_mom.utcOffset(app.time.UTC_OFFSET);
+			return _mom;
+		},
+		/*
+		 * Return the moment object which use server time zone and keep the time.
+		 */
+		srvZone: function(time) {
+			var _timezone = time._isAMomentObject ? time.utcOffset() : new moment().utcOffset();
+			var _mom = app.time.offset(time);
+			_mom.subtract(app.time.UTC_OFFSET, "m").add(_timezone, "m");
+			return _mom;
+		},
+
+		refreshInterval: 1000 * 10
+	};
+
+	// Moment update
+	moment.fn.toISO = function() {
+		return this.format("YYYY-MM-DDTHH:mm:ss.000Z");
+	};
+
+	// Force convert date
+	var _toDate = moment.fn.toDate;
+	moment.fn.toDate = function(ignoreTimeZone) {
+		if(!ignoreTimeZone) return _toDate.bind(this)();
+		return new Date(
+			this.year(),
+			this.month(),
+			this.date(),
+			this.hour(),
+			this.minute(),
+			this.second(),
+			this.millisecond()
+		);
+	};
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/js/app.ui.js
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/_app/public/js/app.ui.js b/eagle-webservice/src/main/webapp/_app/public/js/app.ui.js
new file mode 100644
index 0000000..ca494d3
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/_app/public/js/app.ui.js
@@ -0,0 +1,76 @@
+/*
+ * 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() {
+	// ================== AdminLTE Update ==================
+	var _boxSelect = $.AdminLTE.options.boxWidgetOptions.boxWidgetSelectors;
+
+	// Box collapse
+	$(document).on("click", _boxSelect.collapse, function(e) {
+		if(common.getValueByPath($._data(this), "events.click")) return;
+
+		e.preventDefault();
+		$.AdminLTE.boxWidget.collapse($(this));
+	});
+
+	// Box remove
+	$(document).on("click", _boxSelect.remove, function(e) {
+		if(common.getValueByPath($._data(this), "events.click")) return;
+
+		e.preventDefault();
+		$.AdminLTE.boxWidget.remove($(this));
+	});
+
+	// =================== jQuery Update ===================
+	// Slide Toggle
+	var _slideToggle = $.fn.slideToggle;
+	$.fn.slideToggle = function(showOrHide) {
+		if(arguments.length === 1 && typeof showOrHide === "boolean") {
+			if(showOrHide) {
+				this.slideDown();
+			} else {
+				this.slideUp();
+			}
+		} else {
+			_slideToggle.apply(this, arguments);
+		}
+	};
+
+	// Fade Toggle
+	var _fadeToggle = $.fn.fadeToggle;
+	$.fn.fadeToggle = function(showOrHide) {
+		if(arguments.length === 1 && typeof showOrHide === "boolean") {
+			if(showOrHide) {
+				this.fadeIn();
+			} else {
+				this.fadeOut();
+			}
+		} else {
+			_fadeToggle.apply(this, arguments);
+		}
+	};
+
+	// Modal
+	var _modal = $.fn.modal;
+	$.fn.modal = function() {
+		setTimeout(function() {
+			$(this).find("input[type='text'], textarea").filter(':not([readonly]):enabled:visible:first').focus();
+		}.bind(this), 500);
+		_modal.apply(this, arguments);
+		return this;
+	};
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/js/common.js
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/_app/public/js/common.js b/eagle-webservice/src/main/webapp/_app/public/js/common.js
new file mode 100644
index 0000000..4c5e82f
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/_app/public/js/common.js
@@ -0,0 +1,304 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var common = {};
+
+common.template = function (str, list) {
+	$.each(list, function(key, value) {
+		var _regex = new RegExp("\\$\\{" + key + "\\}", "g");
+		str = str.replace(_regex, value);
+	});
+	return str;
+};
+
+common.getValueByPath = function (unit, path, defaultValue) {
+	if(unit === null || unit === undefined) throw "Unit or path can't be empty!";
+	if(path === "" || path === null || path === undefined) return unit;
+
+	path = path.replace(/\[(\d+)\]/g, ".$1").replace(/^\./, "").split(/\./);
+	$.each(path, function(i, path) {
+		unit = unit[path];
+		if(unit === null || unit === undefined) {
+			unit = null;
+			return false;
+		}
+	});
+	if(unit === null && defaultValue !== undefined) {
+		unit = defaultValue;
+	}
+	return unit;
+};
+
+common.setValueByPath = function(unit, path, value) {
+	if(!unit || typeof path !== "string" || path === "") throw "Unit or path can't be empty!";
+
+	var _inArray = false;
+	var _end = 0;
+	var _start = 0;
+	var _unit = unit;
+
+	function _nextPath(array) {
+		var _key = path.slice(_start, _end);
+		if(_inArray) {
+			_key = _key.slice(0, -1);
+		}
+		if(!_unit[_key]) {
+			if(array) {
+				_unit[_key] = [];
+			} else {
+				_unit[_key] = {};
+			}
+		}
+		_unit = _unit[_key];
+	}
+
+	for(; _end < path.length ; _end += 1) {
+		if(path[_end] === ".") {
+			_nextPath(false);
+			_start = _end + 1;
+			_inArray = false;
+		} else if(path[_end] === "[") {
+			_nextPath(true);
+			_start = _end + 1;
+			_inArray = true;
+		}
+	}
+
+	_unit[path.slice(_start, _inArray ? -1 : _end)] = value;
+
+	return unit;
+};
+
+common.parseJSON = function (str, defaultVal) {
+	try {
+		str = (str + "").trim();
+		if(Number(str).toString() === str) throw "Number format";
+		return JSON.parse(str);
+	} catch(err) {
+		if(defaultVal === undefined) {
+			console.warn("Can't parse JSON: " + str);
+		}
+	}
+	return defaultVal === undefined ? null : defaultVal;
+};
+
+common.stringify = function(json) {
+	return JSON.stringify(json, function(key, value) {
+		if(/^(_|\$)/.test(key)) return undefined;
+		return value;
+	});
+};
+
+common.isEmpty = function(val) {
+	if($.isArray(val)) {
+		return val.length === 0;
+	} else {
+		return val === null || val === undefined;
+	}
+};
+
+common.extend = function(target, origin) {
+	$.each(origin, function(key, value) {
+		if(/^(_|\$)/.test(key)) return;
+
+		target[key] = value;
+	});
+	return target;
+};
+
+// ====================== Format ======================
+common.format = {};
+
+/*
+ * Format date to string. Support number, string, Date instance. Will auto convert time zone offset(Moment instance will keep time zone).
+ */
+common.format.date = function(val, type) {
+	if(val === undefined || val === null) return "";
+
+	if(typeof val === "number" || typeof val === "string" || val instanceof Date) {
+		val = app.time.offset(val);
+	}
+	switch(type) {
+	case 'date':
+		return val.format("YYYY-MM-DD");
+	case 'time':
+		return val.format("HH:mm:ss");
+	case 'datetime':
+		return val.format("YYYY-MM-DD HH:mm:ss");
+	case 'mixed':
+		return val.format("YYYY-MM-DD HH:mm");
+	default:
+		return val.format("YYYY-MM-DD HH:mm:ss") + (val.utcOffset() === 0 ? '[UTC]' : '');
+	}
+};
+
+// ===================== Property =====================
+common.properties = {};
+
+common.properties.parse = function (str, defaultValue) {
+	var regex = /\s*([\w\.]+)\s*=\s*(.*?)\s*([\r\n]+|$)/g;
+	var match, props = {};
+	var hasValue = false;
+	while((match = regex.exec(str)) !== null) {
+		props[match[1]] = match[2];
+		hasValue = true;
+	}
+	props = hasValue ? props : defaultValue;
+	props.getValueByPath = function (path) {
+		if(props[path] !== undefined) return props[path];
+		var subProps = {};
+		var prefixPath = path + ".";
+		$.each(props, function (key, value) {
+			if(typeof value === "string" && key.indexOf(prefixPath) === 0) {
+				subProps[key.replace(prefixPath, "")] = value;
+			}
+		});
+		return subProps;
+	};
+
+	return props;
+};
+
+common.properties.check = function (str) {
+	var pass = true;
+	var regex = /^\s*[\w\.]+\s*=(.*)$/;
+	$.each((str || "").trim().split(/[\r\n\s]+/g), function (i, line) {
+		if(!regex.test(line)) {
+			pass = false;
+			return false;
+		}
+	});
+	return pass;
+};
+
+// ====================== Array =======================
+common.array = {};
+
+common.array.sum = function(list, path) {
+	var _sum = 0;
+	if(list) {
+		$.each(list, function(i, unit) {
+			var _val = common.getValueByPath(unit, path);
+			if(typeof _val === "number") {
+				_sum += _val;
+			}
+		});
+	}
+	return _sum;
+};
+
+common.array.max = function(list, path) {
+	var _max = null;
+	if(list) {
+		$.each(list, function(i, unit) {
+			var _val = common.getValueByPath(unit, path);
+			if(typeof _val === "number" && (_max === null || _max < _val)) {
+				_max = _val;
+			}
+		});
+	}
+	return _max;
+};
+
+common.array.bottom = function(list, path, count) {
+	var _list = list.slice();
+
+	_list.sort(function(a, b) {
+		var _a = common.getValueByPath(a, path, null);
+		var _b = common.getValueByPath(b, path, null);
+
+		if(_a === _b) return 0;
+		if(_a < _b || _a === null) {
+			return -1;
+		} else {
+			return 1;
+		}
+	});
+	return !count ? _list : _list.slice(0, count);
+};
+common.array.top = function(list, path, count) {
+	var _list = common.array.bottom(list, path);
+	_list.reverse();
+	return !count ? _list : _list.slice(0, count);
+};
+
+common.array.find = function(val, list, path, findAll, caseSensitive) {
+	path = path || "";
+	val = caseSensitive === false ? (val || "").toUpperCase() : val;
+
+	var _list = $.grep(list, function(unit) {
+		if(caseSensitive === false) {
+			return val === (common.getValueByPath(unit, path) || "").toUpperCase();
+		} else {
+			return val === common.getValueByPath(unit, path);
+		}
+	});
+	return findAll ? _list : (_list.length === 0 ? null : _list[0]);
+};
+
+common.array.filter = function(val, list, path) {
+	return common.array.find(val, list, path, true);
+};
+
+common.array.count = function(list, val, path) {
+	if(arguments.length === 1) {
+		return list.length;
+	} else {
+		return common.array.find(val, list, path, true).length;
+	}
+};
+
+common.array.remove = function(val, list) {
+	for(var i = 0 ; i < list.length ; i += 1) {
+		if(list[i] === val) {
+			list.splice(i, 1);
+			i -= 1;
+		}
+	}
+};
+
+common.array.insert = function(val, list, index) {
+	list.splice(index, 0, val);
+};
+
+common.array.moveOffset = function(item, list, offset) {
+	var _index = $.inArray(item, list);
+	var _tgtPos = _index + offset;
+	if(_tgtPos < 0 || _tgtPos >= list.length) return;
+
+	common.array.remove(item, list);
+	common.array.insert(item, list, _index + offset);
+};
+
+// ======================= Map ========================
+common.map = {};
+
+common.map.toArray = function(map) {
+	return $.map(map, function(unit) {
+		return unit;
+	});
+};
+
+// ======================= Math =======================
+common.math = {};
+
+common.math.distance = function(x1,y1,x2,y2) {
+	var a = x1 - x2;
+	var b = y1 - y2;
+	return Math.sqrt(a * a + b * b);
+};

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/js/components/charts/line3d.js
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/_app/public/js/components/charts/line3d.js b/eagle-webservice/src/main/webapp/_app/public/js/components/charts/line3d.js
new file mode 100644
index 0000000..c2bf23b
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/_app/public/js/components/charts/line3d.js
@@ -0,0 +1,348 @@
+/*
+ * 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.
+ */
+
+eagleComponents.directive('line3dChart', function($compile) {
+	'use strict';
+
+	return {
+		restrict : 'AE',
+		scope : {
+			title : "@",
+			data : "=",
+
+			height : "=?height"
+		},
+		controller : function(line3dCharts, $scope, $element, $attrs) {
+			$scope.height = $scope.height || 200;
+
+			var _chart = line3dCharts($scope);
+
+			var _chartBody = $element.find(".chart-body");
+			_chartBody.height($scope.height);
+
+			_chart.gen($element.find(".chart-body"), $attrs.data, {
+				height : $scope.height,
+			});
+		},
+		template : '<div class="chart">' + '<div class="chart-header">' + '<h3>{{title}}</h3>' + '</div>' + '<div class="chart-body">' + '</div>' + '</div>',
+		replace : true
+	};
+});
+
+eagleComponents.service('line3dCharts', function() {
+	'use strict';
+
+	var charts = function($scope) {
+		return {
+			gen : function(ele, series, config) {
+				// ======================= Initialization =======================
+				ele = $(ele);
+
+				series = series || [];
+				config = config || {};
+
+				var _bounds = [{min:-10, max: 10},{min:-10, max: 10},{min:-10, max: 10}];
+				var _scale = 1;
+
+				// ======================= Set Up D3 View =======================
+				var width = ele.innerWidth(), height = config.height;
+
+				var color = ["#7cb5ec", "#f7a35c", "#90ee7e", "#7798BF", "#aaeeee"];
+
+				var svg = d3.select(ele[0]).append("svg").attr("width", width).attr("height", height);
+
+				// ========================== Function ==========================
+				var yaw=0.5,pitch=0.5,drag;
+				var transformPrecalc = [];
+
+				var offsetPoint = function(point) {
+					var _point = [
+						+point[0] - (_bounds[0].max + _bounds[0].min) / 2,
+						-point[1] + (_bounds[1].max + _bounds[1].min) / 2,
+						-point[2] + (_bounds[2].max + _bounds[2].min) / 2
+					];
+					return [_point[0] * _scale, _point[1] * _scale, _point[2] * _scale];
+				};
+
+				var transfromPointX = function(point) {
+					point = offsetPoint(point);
+					return transformPrecalc[0] * point[0] + transformPrecalc[1] * point[1] + transformPrecalc[2] * point[2] + width / 2;
+				};
+				var transfromPointY = function(point) {
+					point = offsetPoint(point);
+					return transformPrecalc[3] * point[0] + transformPrecalc[4] * point[1] + transformPrecalc[5] * point[2] + height / 2;
+				};
+				var transfromPointZ = function(point) {
+					point = offsetPoint(point);
+					return transformPrecalc[6] * point[0] + transformPrecalc[7] * point[1] + transformPrecalc[8] * point[2];
+				};
+				var transformPoint2D = function(point) {
+					var _point = [point[0], point[1], point[2]];
+					return transfromPointX(_point).toFixed(10) + "," + transfromPointY(_point).toFixed(10);
+				};
+
+				var setTurtable = function(yaw, pitch, update) {
+					var cosA = Math.cos(pitch);
+					var sinA = Math.sin(pitch);
+					var cosB = Math.cos(yaw);
+					var sinB = Math.sin(yaw);
+					transformPrecalc[0] = cosB;
+					transformPrecalc[1] = 0;
+					transformPrecalc[2] = sinB;
+					transformPrecalc[3] = sinA * sinB;
+					transformPrecalc[4] = cosA;
+					transformPrecalc[5] = -sinA * cosB;
+					transformPrecalc[6] = -sinB * cosA;
+					transformPrecalc[7] = sinA;
+					transformPrecalc[8] = cosA * cosB;
+
+					if(update) _update();
+				};
+				setTurtable(0.4,0.4);
+
+				// =========================== Redraw ===========================
+				var _coordinateList = [];
+				var _axisText = [];
+				var coordinate = svg.selectAll(".axis");
+				var axisText = svg.selectAll(".axis-text");
+				var lineList = svg.selectAll(".line");
+
+				function _redraw(series) {
+					var _desX, _desY, _desZ, _step;
+
+					// Bounds
+					if(series) {
+						_bounds = [{},{},{}];
+						$.each(series, function(j, series) {
+							// Points
+							$.each(series.data, function(k, point) {
+								for(var i = 0 ; i < 3 ; i += 1) {
+									// Minimum
+									if(_bounds[i].min === undefined || point[i] < _bounds[i].min) {
+										_bounds[i].min = point[i];
+									}
+									// Maximum
+									if(_bounds[i].max === undefined || _bounds[i].max < point[i]) {
+										_bounds[i].max = point[i];
+									}
+								}
+							});
+						});
+					}
+
+					_desX = _bounds[0].max - _bounds[0].min;
+					_desY = _bounds[1].max - _bounds[1].min;
+					_desZ = _bounds[2].max - _bounds[2].min;
+
+					// Step
+					(function() {
+						var _stepX = _desX / 10;
+						var _stepY = _desY / 10;
+						var _stepZ = _desZ / 10;
+
+						_step = Math.min(_stepX, _stepY, _stepZ);
+						_step = Math.max(_step, 0.5);
+						_step = 0.5;
+					})();
+
+					// Scale
+					(function() {
+						var _scaleX = width / _desX;
+						var _scaleY = height / _desY / 2;
+						var _scaleZ = width / _desZ;
+						_scale = Math.min(_scaleX, _scaleY, _scaleZ) / 2;
+					})();
+
+					// Coordinate
+					// > Basic
+					_coordinateList = [
+						{color: "rgba(0,0,0,0.1)", data: [[0,0,-100],[0,0,100]]},
+						{color: "rgba(0,0,0,0.1)", data: [[0,-100,0],[0,100,0]]},
+						{color: "rgba(0,0,0,0.1)", data: [[-100,0,0],[100,0,0]]},
+
+						{color: "rgba(0,0,255,0.3)", data: [[_bounds[0].min,_bounds[1].min,_bounds[2].min],[_bounds[0].max,_bounds[1].min,_bounds[2].min]]},
+						{color: "rgba(0,0,255,0.3)", data: [[_bounds[0].min,_bounds[1].min,_bounds[2].min],[_bounds[0].min,_bounds[1].min,_bounds[2].max]]},
+
+						{color: "rgba(0,0,255,0.3)", data: [[_bounds[0].min,_bounds[1].min,_bounds[2].max],[_bounds[0].max,_bounds[1].min,_bounds[2].max]]},
+						{color: "rgba(0,0,255,0.3)", data: [[_bounds[0].max,_bounds[1].min,_bounds[2].min],[_bounds[0].max,_bounds[1].min,_bounds[2].max]]},
+
+						{color: "rgba(0,0,255,0.3)", data: [[_bounds[0].min,_bounds[1].max,_bounds[2].min],[_bounds[0].max,_bounds[1].max,_bounds[2].min]]},
+						{color: "rgba(0,0,255,0.3)", data: [[_bounds[0].min,_bounds[1].max,_bounds[2].min],[_bounds[0].min,_bounds[1].max,_bounds[2].max]]},
+
+						{color: "rgba(0,0,255,0.3)", data: [[_bounds[0].min,_bounds[1].min,_bounds[2].min],[_bounds[0].min,_bounds[1].max,_bounds[2].min]]},
+						{color: "rgba(0,0,255,0.3)", data: [[_bounds[0].max,_bounds[1].min,_bounds[2].min],[_bounds[0].max,_bounds[1].max,_bounds[2].min]]},
+						{color: "rgba(0,0,255,0.3)", data: [[_bounds[0].min,_bounds[1].min,_bounds[2].max],[_bounds[0].min,_bounds[1].max,_bounds[2].max]]},
+					];
+
+					_axisText = [];
+					function _axisPoint(point, dimension, number) {
+						// Coordinate
+						if(dimension === 1) {
+							_coordinateList.push({
+								color: "rgba(0,0,0,0.2)",
+								data:[[_step/5,point[1],0],[0,point[1],0],[0,point[1],_step/5]]
+							});
+						} else {
+							_coordinateList.push({
+								color: "rgba(0,0,0,0.2)",
+								data:[[point[0],-_step/8,point[2]], [point[0],_step/8,point[2]]]
+							});
+						}
+
+						// Axis Text
+						if(number.toFixed(0) == number + "") {
+							point.text = number;
+							point.dimension = dimension;
+							_axisText.push(point);
+						}
+					}
+					function _axisPoints(dimension, bound) {
+						var i, _unit;
+						for(i = _step ; i < bound.max + _step ; i += _step) {
+							_unit = [0,0,0];
+							_unit[dimension] = i;
+							_axisPoint(_unit, dimension, i);
+						}
+						for(i = -_step ; i > bound.min - _step ; i -= _step) {
+							_unit = [0,0,0];
+							_unit[dimension] = i;
+							_axisPoint(_unit, dimension, i);
+						}
+					}
+					// > Steps
+					_axisPoint([0,0,0],1,0);
+					_axisPoints(0, _bounds[0]);
+					_axisPoints(1, _bounds[1]);
+					_axisPoints(2, _bounds[2]);
+
+					// > Draw
+					coordinate = coordinate.data(_coordinateList);
+					coordinate.enter()
+					.append("path")
+						.attr("fill", "none")
+						.attr("stroke", function(d) {return d.color;});
+					coordinate.exit().remove();
+
+					// Axis Text
+					axisText = axisText.data(_axisText);
+					axisText.enter()
+						.append("text")
+						.classed("noSelect", true)
+						.attr("fill", "rgba(0,0,0,0.5)")
+						.attr("text-anchor", function(d) {return d.dimension === 1 ? "start" : "middle";})
+						.text(function(d) {return d.text;});
+					axisText.transition()
+						.attr("text-anchor", function(d) {return d.dimension === 1 ? "end" : "middle";})
+						.text(function(d) {return d.text;});
+					axisText.exit().remove();
+
+					// Lines
+					lineList = lineList.data(series || []);
+					lineList.enter()
+						.append("path")
+							.attr("fill", "none")
+							.attr("stroke", function(d) {return d.color;});
+					lineList.exit().remove();
+
+					_update();
+				}
+
+				function _update() {
+					coordinate
+						.attr("d", function(d) {
+						var path = "";
+						$.each(d.data, function(i, point) {
+							path += (i === 0 ? "M" : "L") + transformPoint2D(point);
+						});
+						return path;
+						});
+
+					axisText
+						.attr("x", function(d) {return transfromPointX(d) + (d.dimension === 1 ? -3 : 0);})
+						.attr("y", function(d) {return transfromPointY(d) + (d.dimension === 1 ? 0 : -5);});
+
+					lineList
+						.attr("d", function(d, index) {
+							var path = "";
+							$.each(d.data, function(i, point) {
+								path += (i === 0 ? "M" : "L") + transformPoint2D(point);
+							});
+							return path;
+						});
+				}
+
+
+				svg.on("mousedown", function() {
+					drag = [d3.mouse(this), yaw, pitch];
+				}).on("mouseup", function() {
+					drag = false;
+				}).on("mousemove", function() {
+					if (drag) {
+						var mouse = d3.mouse(this);
+						yaw = drag[1] - (mouse[0] - drag[0][0]) / 50;
+						pitch = drag[2] + (mouse[1] - drag[0][1]) / 50;
+						pitch = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, pitch));
+						setTurtable(yaw, pitch, true);
+					}
+				});
+
+				// =========================== Render ===========================
+				_redraw();
+
+				function _render() {
+					// ======== Parse Data ========
+					var _series = typeof series === "string" ? $scope.data : series;
+					if(!_series) return;
+
+					// Clone
+					_series = $.map(_series, function(series) {
+						return {
+							name: series.name,
+							color: series.color,
+							data: series.data
+						};
+					});
+
+					// Colors
+					$.each(_series, function(i, series) {
+						series.color = series.color || color[i % color.length];
+					});
+
+					// Render
+					_redraw(_series);
+				}
+
+				// ======================= Dynamic Detect =======================
+				if(typeof series === "string") {
+					$scope.$parent.$watch(series, function() {
+						_render();
+					}, true);
+				} else {
+					_render();
+				}
+
+
+				// ========================== Clean Up ==========================
+				$scope.$on('$destroy', function() {
+					svg.remove();
+				});
+			},
+		};
+	};
+	return charts;
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/js/components/file.js
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/_app/public/js/components/file.js b/eagle-webservice/src/main/webapp/_app/public/js/components/file.js
new file mode 100644
index 0000000..a8d78db
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/_app/public/js/components/file.js
@@ -0,0 +1,50 @@
+/*
+ * 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.
+ */
+
+eagleComponents.directive('file', function($compile) {
+	'use strict';
+
+	return {
+		restrict : 'A',
+		scope: {
+			filepath: "=?filepath",
+		},
+		controller: function($scope, $element, $attrs) {
+			// Watch change(Only support clean the data)
+			if($attrs.filepath) {
+				$scope.$parent.$watch($attrs.filepath, function(value) {
+					if(!value) $element.val(value);
+				});
+			}
+
+			// Bind changed value
+			$element.on("change", function() {
+				var _path = $(this).val();
+				if($attrs.filepath) {
+					common.setValueByPath($scope.$parent, $attrs.filepath, _path);
+					$scope.$parent.$apply();
+				}
+			});
+
+			$scope.$on('$destroy',function(){
+				$element.off("change");
+			});
+		},
+		replace: false
+	};
+});

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/js/components/main.js
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/_app/public/js/components/main.js b/eagle-webservice/src/main/webapp/_app/public/js/components/main.js
new file mode 100644
index 0000000..a0d9f9f
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/_app/public/js/components/main.js
@@ -0,0 +1,19 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+var eagleComponents = angular.module('eagle.components', []);

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/js/components/nvd3.js
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/_app/public/js/components/nvd3.js b/eagle-webservice/src/main/webapp/_app/public/js/components/nvd3.js
new file mode 100644
index 0000000..8687c78
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/_app/public/js/components/nvd3.js
@@ -0,0 +1,418 @@
+/*
+ * 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.
+ */
+
+eagleComponents.service('nvd3', function() {
+	var nvd3 = {
+		charts: [],
+		colors: [
+			"#7CB5EC", "#F7A35C", "#90EE7E", "#7798BF", "#AAEEEE"
+		]
+	};
+
+	// ============================================
+	// =              Format Convert              =
+	// ============================================
+
+	/***
+	 * Format: [series:{key:name, value: [{x, y}]}]
+	 */
+
+	nvd3.convert = {};
+	nvd3.convert.eagle = function(seriesList) {
+		return $.map(seriesList, function(series) {
+			var seriesObj = $.isArray(series) ? {values: series} : series;
+			if(!seriesObj.key) seriesObj.key = "value";
+			return seriesObj;
+		});
+	};
+
+	nvd3.convert.druid = function(seriesList) {
+		var _seriesList = [];
+
+		$.each(seriesList, function(i, series) {
+			if(!series.length) return;
+
+			// Fetch keys
+			var _measure = series[0];
+			var _keys = $.map(_measure.event, function(value, key) {
+				return key !== "metric" ? key : null;
+			});
+
+			// Parse series
+			_seriesList.push.apply(_seriesList, $.map(_keys, function(key) {
+				return {
+					key: key,
+					values: $.map(series, function(unit) {
+						return {
+							x: new moment(unit.timestamp).valueOf(),
+							y: unit.event[key]
+						};
+					})
+				};
+			}));
+		});
+
+		return _seriesList;
+	};
+
+	// ============================================
+	// =                    UI                    =
+	// ============================================
+	// Resize with refresh
+	function chartResize() {
+		$.each(nvd3.charts, function(i, chart) {
+			if(chart) chart.nvd3Update();
+		});
+	}
+	$(window).on("resize.components.nvd3", chartResize);
+	$("body").on("collapsed.pushMenu expanded.pushMenu", function() {
+		setTimeout(chartResize, 300);
+	});
+
+	return nvd3;
+});
+
+/**
+ * config:
+ * 		chart:			Defined chart type: line, column, area
+ * 		xTitle:			Defined x axis title.
+ * 		yTitle:			Defined y axis title.
+ * 		xType:			Defined x axis label type: number, decimal, time
+ * 		yType:			Defined y axis label type
+ * 		yMin:			Defined minimum of y axis
+ * 		yMax:			Defined maximum of y axis
+ * 		displayType:	Defined the chart display type. Each chart has own type.
+ */
+eagleComponents.directive('nvd3', function(nvd3) {
+	'use strict';
+
+	return {
+		restrict: 'AE',
+		scope: {
+			nvd3: "=",
+			title: "@?title",				// title
+			chart: "@?chart",				// Same as config.chart
+			config: "=?config",
+			watching: "@?watching",			// Default watching data(nvd3) only. true will also watching chart & config. false do not watching.
+
+			holder: "=?holder"				// Container for holder to call the chart function
+		},
+		controller: function($scope, $element, $attrs, $timeout) {
+			var _config, _chartType;
+			var _chart;
+			var _chartCntr;
+			var _holder, _holder_updateTimes;
+
+			// Destroy
+			function destroy() {
+				var _index = $.inArray(_chart, nvd3.charts);
+				if(!_chartCntr) return _index;
+
+				// Clean events
+				d3.select(_chartCntr)
+					.on("touchmove",null)
+					.on("mousemove",null, true)
+					.on("mouseout" ,null,true)
+					.on("dblclick" ,null)
+					.on("click", null);
+
+				// Clean elements
+				d3.select(_chartCntr).selectAll("*").remove();
+				$element.find(".nvtooltip").remove();
+				$(_chartCntr).remove();
+
+				// Clean chart in nvd3 pool
+				nvd3.charts[_index] = null;
+				_chart = null;
+
+				return _index;
+			}
+
+			// Setup chart environment. Will clean old chart and build new chart if recall.
+			function initChart() {
+				// Clean up if already have chart
+				var _preIndex = destroy();
+
+				// Initialize
+				_config = $.extend({}, $scope.config);
+				_chartType = $scope.chart || _config.chart;
+				_chartCntr = $(document.createElementNS("http://www.w3.org/2000/svg", "svg"))
+					.css("min-height", 50)
+					.attr("_rnd", Math.random())
+					.appendTo($element)[0];
+
+				// Size
+				if(_config.height) {
+					$(_chartCntr).css("height", _config.height);
+				}
+
+				switch(_chartType) {
+					case "line":
+						_chart = nv.models.lineChart()
+							.useInteractiveGuideline(true)
+							.showLegend(true)
+							.showYAxis(true)
+							.showXAxis(true)
+							.options({
+								duration: 350
+							});
+						break;
+					case "column":
+						_chart = nv.models.multiBarChart()
+							.groupSpacing(0.1)
+							.options({
+								duration: 350
+							});
+						break;
+					case "area":
+						_chart = nv.models.stackedAreaChart()
+							.useInteractiveGuideline(true)
+							.showLegend(true)
+							.showYAxis(true)
+							.showXAxis(true)
+							.options({
+								duration: 350
+							});
+						break;
+					case "pie":
+						_chart = nv.models.dimensionalPieChart()
+							.x(function(d) { return d.key; })
+							.y(function(d) { return d.values[d.values.length - 1].y; });
+						break;
+					default :
+						throw "Type not defined: " + _chartType;
+				}
+
+				// nvd3 display Type
+				// TODO: support type define
+
+				// Define title
+				if(_chartType !== 'pie') {
+					if(_config.xTitle) _chart.xAxis.axisLabel(_config.xTitle);
+					if(_config.yTitle) _chart.yAxis.axisLabel(_config.yTitle);
+				}
+
+				// Define label type
+				var _tickMultiFormat = d3.time.format.multi([
+					["%-I:%M%p", function(d) { return d.getMinutes(); }],
+					["%-I%p", function(d) { return d.getHours(); }],
+					["%b %-d", function(d) { return d.getDate() != 1; }],
+					["%b %-d", function(d) { return d.getMonth(); }],
+					["%Y", function() { return true; }]
+				]);
+
+				function _defineLabelType(axis, type) {
+					if(!_chart) return;
+
+					var _axis = _chart[axis + "Axis"];
+					if(!_axis) return;
+
+					switch(type) {
+						case "decimal":
+						case "decimals":
+							_axis.tickFormat(d3.format('.02f'));
+							break;
+						case "text":
+							if(axis === "x") {
+								_chart.rotateLabels(10);
+								_chart.reduceXTicks(false).staggerLabels(true);
+							}
+							_axis.tickFormat(function(d) {
+								return d;
+							});
+							break;
+						case "time":
+							if(_chartType !== 'column') {
+								_chart[axis + "Scale"](d3.time.scale());
+								(function () {
+									var measureSeries = null;
+									$.each($scope.nvd3 || [], function(i, series) {
+										var _len = (series.values || []).length;
+										if(_len === 0) return;
+										if(measureSeries === null || measureSeries.values.length < _len) measureSeries = series;
+									});
+
+									var width = $element.width() - 35;// Use default nvd3 margin. Hard code.
+									if(!measureSeries || width <= 0) return;
+									var count = Math.floor(width / 80);
+									var countDes = Math.floor(measureSeries.values.length / count);
+									var tickValues = [];
+									for(var loc = 0 ; loc < measureSeries.values.length ; loc += countDes) {
+										tickValues.push(measureSeries.values[loc].x);
+									}
+									_chart[axis + "Axis"].tickValues(tickValues);
+								})();
+							}
+							_axis.tickFormat(function(d) {
+								return _tickMultiFormat(app.time.offset(d).toDate(true));
+							});
+							break;
+						case "number":
+						/* falls through */
+						default:
+							_axis.tickFormat(d3.format(',r'));
+					}
+				}
+
+				if(_chartType !== 'pie') {
+					_defineLabelType("x", _config.xType || "number");
+					_defineLabelType("y", _config.yType || "decimal");
+				}
+
+				// Global chart list update
+				if(_preIndex === -1) {
+					nvd3.charts.push(_chart);
+				} else {
+					nvd3.charts[_preIndex] = _chart;
+				}
+
+				// Use customize update function to update the view
+				_chart.nvd3Update = function() {
+					if(_config.xType === "time") _defineLabelType("x", _config.xType);
+					_chart.update();
+				};
+
+				updateData();
+			}
+
+			// Update chart data
+			function updateData() {
+				var _min = null, _max = null;
+
+				// Copy series to prevent Angular loop watching
+				var _data = $.map($scope.nvd3 || [], function(series, i) {
+					var _series = $.extend(true, {}, series);
+					_series.color = _series.color || nvd3.colors[i % nvd3.colors.length];
+					return _series;
+				});
+
+				// Chart Y value
+				if(($scope.chart || _config.chart) !== "pie") {
+					$.each(_data, function(i, series) {
+						$.each(series.values, function(j, unit) {
+							if(_min === null || unit.y < _min) _min = unit.y;
+							if(_max === null || unit.y > _max) _max = unit.y;
+						});
+					});
+
+					if(_min === 0 && _max === 0) {
+						_chart.forceY([0, 10]);
+					} else if(_config.yMin !== undefined || _config.yMax !== undefined) {
+						_chart.forceY([_config.yMin, _config.yMax]);
+					} else {
+						_chart.forceY([]);
+					}
+				}
+
+				// Update data
+				d3.select(_chartCntr)						//Select the <svg> element you want to render the chart in.
+					.datum(_data)							//Populate the <svg> element with chart data...
+					.call(_chart);							//Finally, render the chart!
+
+				setTimeout(_chart.nvd3Update, 10);
+			}
+
+			// ================================================================
+			// =                           Watching                           =
+			// ================================================================
+			// Ignore initial checking
+			$timeout(function() {
+				if ($scope.watching !== "false") {
+					$scope.$watch(function() {
+						if(!$scope.nvd3) return [];
+
+						var _hashList = $.map($scope.nvd3, function(series) {
+							if(!series.values) return 0;
+							var unit = {
+								x: 0,
+								y: 0
+							};
+
+							$.each(series.values, function(j, item) {
+								unit.x += item.x;
+								unit.y += item.y;
+							});
+
+							return unit;
+						});
+
+						return _hashList;
+					}, function() {
+						updateData();
+					}, true);
+
+					// All watching mode
+					if ($scope.watching === "true") {
+						$scope.$watch("[chart, config]", function(newValue, oldValue) {
+							if(angular.equals(newValue, oldValue)) return;
+							initChart();
+						}, true);
+					}
+				}
+			});
+
+			// Holder inject
+			_holder_updateTimes = 0;
+			_holder = {
+				element: $element,
+				refresh: function() {
+					setTimeout(function() {
+						updateData();
+					}, 0);
+				},
+				refreshAll: function() {
+					setTimeout(function() {
+						initChart();
+					}, 0);
+				}
+			};
+
+			Object.defineProperty(_holder, 'chart', {
+				get: function() {return _chart;}
+			});
+
+			$scope.$watch("holder", function() {
+				// Holder times update
+				setTimeout(function() {
+					_holder_updateTimes = 0;
+				}, 0);
+				_holder_updateTimes += 1;
+				if(_holder_updateTimes > 100) throw "Holder conflict";
+
+				$scope.holder = _holder;
+			});
+
+			// ================================================================
+			// =                           Start Up                           =
+			// ================================================================
+			initChart();
+
+			// ================================================================
+			// =                           Clean Up                           =
+			// ================================================================
+			$scope.$on('$destroy', function() {
+				destroy();
+			});
+		},
+		template :
+		'<div>' +
+			'<h3 title="{{title || config.title}}">{{title || config.title}}</h3>' +
+		'</div>',
+		replace: true
+	};
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/js/components/sortTable.js
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/_app/public/js/components/sortTable.js b/eagle-webservice/src/main/webapp/_app/public/js/components/sortTable.js
new file mode 100644
index 0000000..bdcbca4
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/_app/public/js/components/sortTable.js
@@ -0,0 +1,113 @@
+/*
+ * 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.
+ */
+
+eagleComponents.directive('sorttable', function($compile) {
+	'use strict';
+
+	return {
+		restrict : 'AE',
+		scope: {
+			source: '=',
+			search: '=?search',
+			searchfunc: "=?searchfunc",
+
+			orderKey: "@?sort",
+
+			maxSize: "=?maxSize",
+		},
+		controller: function($scope, $element, $attrs) {
+			// Initialization
+			$scope.app = app;
+			$scope.common = common;
+			$scope._parent = $scope.$parent;
+
+			$scope.pageNumber = $scope.pageNumber || 1;
+			$scope.pageSize = $scope.pageSize || 10;
+
+			$scope.maxSize = $scope.maxSize || 10;
+
+			// Search box
+			if($scope.search !== false) {
+				var $search = $(
+					'<div style="overflow:hidden;">' +
+						'<div class="row">' +
+							'<div class="col-xs-4">' +
+								'<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>' +
+					'</div>'
+				).prependTo($element);
+				$compile($search)($scope);
+			}
+
+			// List head
+			$scope.doSort = function(path) {
+				if($scope.orderKey === path) {
+					$scope.orderKey = "-" + path;
+				} else {
+					$scope.orderKey = path;
+				}
+			};
+			$scope.checkSortClass = function(key) {
+				if($scope.orderKey === key) {
+					return "fa fa-sort-asc";
+				} else if($scope.orderKey === "-" + key) {
+					return "fa fa-sort-desc";
+				}
+				return "fa fa-sort";
+			};
+
+			var $listHead = $element.find("thead > tr");
+			$listHead.find("> th").each(function() {
+				var $th = $(this);
+				$th.addClass("noSelect");
+
+				var _sortpath = $th.attr("sortpath");
+				if(_sortpath) {
+					$th.attr("ng-click", "doSort('" + _sortpath + "')");
+					$th.prepend('<span ng-class="checkSortClass(\'' + _sortpath + '\')"></span>');
+				}
+			});
+			$compile($listHead)($scope);
+
+			// List body
+			var $listBody = $element.find("tbody > tr");
+			$listBody.attr("ng-repeat", 'item in (filteredList = (source | filter: ' + ($scope.searchfunc ? 'searchfunc' : 'search') + ' | orderBy: orderKey)).slice((pageNumber - 1) * pageSize, pageNumber * pageSize)');
+			$compile($listBody)($scope);
+
+			// Navigation
+			var $navigation = $(
+				'<div style="overflow:hidden;">' +
+					'<div class="row">' +
+						'<div class="col-xs-5">' +
+							'show {{(pageNumber - 1) * pageSize + 1}} to {{pageNumber * pageSize}} of {{filteredList.length}} items' +
+						'</div>' +
+						'<div class="col-xs-7 text-right">' +
+							'<uib-pagination total-items="filteredList.length" ng-model="pageNumber" boundary-links="true" items-per-page="pageSize" num-pages="numPages" max-size="maxSize"></uib-pagination>' +
+						'</div>' +
+					'</div>' +
+				'</div>'
+			).appendTo($element);
+			$compile($navigation)($scope);
+		},
+		replace: false
+	};
+});

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/js/components/sortable.js
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/_app/public/js/components/sortable.js b/eagle-webservice/src/main/webapp/_app/public/js/components/sortable.js
new file mode 100644
index 0000000..c98c732
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/_app/public/js/components/sortable.js
@@ -0,0 +1,166 @@
+/*
+ * 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.
+ */
+
+eagleComponents.directive('uieSortable', function($rootScope) {
+	'use strict';
+
+	var COLLECTION_MATCH = /^\s*([\s\S]+?)\s+in\s+([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+track\s+by\s+([\s\S]+?))?\s*$/;
+
+	var _move = false;
+	var _selectElement;
+	var _overElement;
+	var _mockElement;
+	var _offsetX, _offsetY;
+	var _mouseDownPageX, _mouseDownPageY;
+
+	function doMock(element, event) {
+		var _offset = element.offset();
+		if(_mockElement) _mockElement.remove();
+
+		// Create mock element
+		_mockElement = element.clone(false).appendTo("body");
+		_mockElement.addClass("sortable-mock-element");
+		_mockElement.css({
+			display: "block",
+			position: "absolute",
+			"pointer-events": "none",
+			"z-index": 10001,
+			padding: element.css("padding"),
+			margin: element.css("margin")
+		});
+		_mockElement.width(element.width());
+		_mockElement.height(element.height());
+
+		_mockElement.offset(_offset);
+		_offsetX = event.pageX - _offset.left;
+		_offsetY = event.pageY - _offset.top;
+	}
+
+	$(window).on("mousemove", function(event) {
+		if(!_move) return;
+		event.preventDefault();
+
+		_mockElement.offset({
+			left: event.pageX - _offsetX,
+			top: event.pageY - _offsetY
+		});
+	});
+
+	$(window).on("mouseup", function() {
+		if(!_move) {
+			_overElement = null;
+			_selectElement = null;
+			_mockElement = null;
+			return;
+		}
+		_move = false;
+
+		if(_overElement) {
+			_overElement.removeClass("sortable-enter");
+
+			if(_overElement[0] !== _selectElement[0]) {
+				// Process switch
+				var _oriHolder = _selectElement.holder;
+				var _tgtHolder = _overElement.holder;
+				var _oriSortableScope = _oriHolder.scope;
+				var _tgtSortableScope = _tgtHolder.scope;
+				var _oriScope = angular.element(_selectElement).scope();
+				var _tgtScope = angular.element(_overElement).scope();
+
+				var _oriRepeat = _selectElement.closest("[ng-repeat]").attr("ng-repeat");
+				var _tgtRepeat = _overElement.closest("[ng-repeat]").attr("ng-repeat");
+				var _oriMatch = _oriRepeat.match(COLLECTION_MATCH)[2];
+				var _tgtMatch = _tgtRepeat.match(COLLECTION_MATCH)[2];
+				var _oriCollection = _oriScope.$parent.$eval(_oriMatch);
+				var _tgtCollection = _tgtScope.$parent.$eval(_tgtMatch);
+				var _oriIndex = $.inArray(_oriCollection[_oriScope.$index], _oriSortableScope.ngModel);
+				var _tgtIndex = $.inArray(_tgtCollection[_tgtScope.$index], _tgtSortableScope.ngModel);
+
+				var _oriUnit = _oriSortableScope.ngModel[_oriIndex];
+				var _tgtUnit = _tgtSortableScope.ngModel[_tgtIndex];
+				_oriSortableScope.ngModel[_oriIndex] = _tgtUnit;
+				_tgtSortableScope.ngModel[_tgtIndex] = _oriUnit;
+
+				// Trigger event
+				_oriHolder.change(_oriUnit, _tgtUnit);
+				if (_oriHolder !== _tgtHolder) _tgtHolder.change(_oriUnit, _tgtUnit);
+
+				$rootScope.$apply();
+			}
+		}
+
+		if(_mockElement) _mockElement.remove();
+
+		_overElement = null;
+		_selectElement = null;
+		_mockElement = null;
+	});
+
+	return {
+		require: 'ngModel',
+		restrict : 'AE',
+		scope: {
+			ngModel: "=",
+			sortableEnabled: "=?sortableEnabled",
+			sortableUpdateFunc: "=?sortableUpdateFunc"
+		},
+		link: function($scope, $element, $attrs, $ctrl) {
+			var _holder = {
+				scope: $scope,
+				change: function(source, target) {
+					if($scope.sortableUpdateFunc) $scope.sortableUpdateFunc(source, target);
+				}
+			};
+
+			$element.on("mousedown", ">", function(event) {
+				if($scope.sortableEnabled === false) return;
+
+				_selectElement = $(this);
+				_selectElement.holder = _holder;
+
+				_mouseDownPageX = event.pageX;
+				_mouseDownPageY = event.pageY;
+
+				event.preventDefault();
+			});
+
+			$element.on("mousemove", ">", function(event) {
+				if(_selectElement && !_move && common.math.distance(_mouseDownPageX, _mouseDownPageY, event.pageX, event.pageY) > 10) {
+					_move = true;
+					_overElement = _selectElement;
+					_overElement.addClass("sortable-enter");
+
+					doMock(_selectElement, event);
+				}
+			});
+
+			$element.on("mouseenter", ">", function() {
+				if(!_move) return;
+				_overElement = $(this);
+				_overElement.holder = _holder;
+				_overElement.addClass("sortable-enter");
+			});
+			$element.on("mouseleave", ">", function() {
+				if(!_move) return;
+				$(this).removeClass("sortable-enter");
+				_overElement = null;
+			});
+		},
+		replace: false
+	};
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/js/components/tabs.js
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/_app/public/js/components/tabs.js b/eagle-webservice/src/main/webapp/_app/public/js/components/tabs.js
new file mode 100644
index 0000000..21c4a4a
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/_app/public/js/components/tabs.js
@@ -0,0 +1,247 @@
+/*
+ * 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.
+ */
+
+eagleComponents.directive('tabs', function() {
+	'use strict';
+
+	return {
+		restrict: 'AE',
+		transclude: {
+			'header': '?header',
+			'pane': 'pane',
+			'footer': '?footer'
+		},
+		scope : {
+			title: "@?title",
+			icon: "@",
+			selected: "@?selected",
+			holder: "=?holder",
+			sortableModel: "=?sortableModel",
+
+			menuList: "=?menu"
+		},
+
+		controller: function($scope, $element, $attrs, $timeout) {
+			var transDuration = $.fn.tab.Constructor.TRANSITION_DURATION;
+			var transTimer = null;
+			var _holder, _holder_updateTimes;
+
+			var $header, $footer;
+
+			$scope.paneList = [];
+			$scope.selectedPane = null;
+			$scope.inPane = null;
+			$scope.activePane = null;
+
+			// ================== Function ==================
+			$scope.getPaneList = function() {
+				return !$scope.title ? $scope.paneList : $scope.paneList.slice().reverse();
+			};
+
+			$scope.setSelect = function(pane) {
+				if(typeof pane === "string") {
+					pane = common.array.find(pane, $scope.paneList, "title");
+				} else if(typeof pane === "number") {
+					pane = (pane + $scope.paneList.length) % $scope.paneList.length;
+					pane = $scope.paneList[pane];
+				}
+
+				$scope.activePane = $scope.selectedPane || pane;
+				$scope.selectedPane = pane;
+				$scope.inPane = null;
+
+				if(transTimer) $timeout.cancel(transTimer);
+				transTimer = $timeout(function() {
+					$scope.activePane = $scope.selectedPane;
+					$scope.inPane = $scope.selectedPane;
+				}, transDuration);
+			};
+
+			$scope.getMenuList = function() {
+				if($scope.selectedPane && $scope.selectedPane.menuList) {
+					return $scope.selectedPane.menuList;
+				}
+				return $scope.menuList;
+			};
+
+			$scope.tabSwitchUpdate = function (src, tgt) {
+				var _srcIndex = $.inArray(src.data, $scope.sortableModel);
+				var _tgtIndex = $.inArray(tgt.data, $scope.sortableModel);
+
+				if(_srcIndex !== -1 && _tgtIndex !== -1) {
+					$scope.sortableModel[_srcIndex] = tgt.data;
+					$scope.sortableModel[_tgtIndex] = src.data;
+				}
+			};
+
+			// =================== Linker ===================
+			function _linkerProperties(pane) {
+				Object.defineProperties(pane, {
+					selected: {
+						get: function () {
+							return $scope.selectedPane === this;
+						}
+					},
+					active: {
+						get: function () {
+							return $scope.activePane === this;
+						}
+					},
+					in: {
+						get: function () {
+							return $scope.inPane === this;
+						}
+					}
+				});
+			}
+
+			this.addPane = function(pane) {
+				$scope.paneList.push(pane);
+
+				// Register properties
+				_linkerProperties(pane);
+
+				// Update select pane
+				if(pane.title === $scope.selected || !$scope.selectedPane) {
+					$scope.setSelect(pane);
+				}
+			};
+
+			this.deletePane = function(pane) {
+				common.array.remove(pane, $scope.paneList);
+
+				if($scope.selectedPane === pane) {
+					$scope.selectedPane = $scope.activePane = $scope.inPane = $scope.paneList[0];
+				}
+			};
+
+			// ===================== UI =====================
+			$header = $element.find("> .nav-tabs-custom > .box-body");
+			$footer = $element.find("> .nav-tabs-custom > .box-footer");
+
+			$scope.hasHeader = function() {
+				return !!$header.children().length;
+			};
+			$scope.hasFooter = function() {
+				return !!$footer.children().length;
+			};
+
+			// ================= Interface ==================
+			_holder_updateTimes = 0;
+			_holder = {
+				scope: $scope,
+				element: $element,
+				setSelect: $scope.setSelect
+			};
+
+			Object.defineProperty(_holder, 'selectedPane', {
+				get: function() {return $scope.selectedPane;}
+			});
+
+			$scope.$watch("holder", function(newValue, oldValue) {
+				// Holder times update
+				setTimeout(function() {
+					_holder_updateTimes = 0;
+				}, 0);
+				_holder_updateTimes += 1;
+				if(_holder_updateTimes > 100) throw "Holder conflict";
+
+				$scope.holder = _holder;
+			});
+		},
+
+		template :
+			'<div class="nav-tabs-custom">' +
+				// Menu
+				'<div class="box-tools pull-right" ng-if="getMenuList() && getMenuList().length">' +
+					'<div ng-repeat="menu in getMenuList() track by $index" class="inline">' +
+						// Button
+						'<button class="btn btn-box-tool" ng-click="menu.func($event)" ng-if="!menu.list"' +
+							' uib-tooltip="{{menu.title}}" tooltip-enable="menu.title" tooltip-append-to-body="true">' +
+							'<span class="fa fa-{{menu.icon}}"></span>' +
+						'</button>' +
+
+						// Dropdown Group
+						'<div class="btn-group" ng-if="menu.list">' +
+							'<button class="btn btn-box-tool dropdown-toggle" data-toggle="dropdown"' +
+								' uib-tooltip="{{menu.title}}" tooltip-enable="menu.title" tooltip-append-to-body="true">' +
+								'<span class="fa fa-{{menu.icon}}"></span>' +
+							'</button>' +
+							'<ul class="dropdown-menu left" role="menu">' +
+								'<li ng-repeat="item in menu.list track by $index" ng-class="{danger: item.danger, disabled: item.disabled}">' +
+									'<a ng-click="!item.disabled && item.func($event)" ng-class="{strong: item.strong}">' +
+										'<span class="fa fa-{{item.icon}}"></span> {{item.title}}' +
+									'</a>' +
+								'</li>' +
+							'</ul>' +
+						'</div>' +
+					'</div>' +
+				'</div>' +
+
+				'<ul uie-sortable sortable-enabled="!!sortableModel" sortable-update-func="tabSwitchUpdate" ng-model="paneList" class="nav nav-tabs" ng-class="{\'pull-right\': title}">' +
+					// Tabs
+					'<li ng-repeat="pane in getPaneList() track by $index" ng-class="{active: selectedPane === pane}">' +
+						'<a ng-click="setSelect(pane);">{{pane.title}}</a>' +
+					'</li>' +
+
+					// Title
+					'<li class="pull-left header" ng-if="title">' +
+						'<i class="fa fa-{{icon}}" ng-if="icon"></i> {{title}}' +
+					'</li>' +
+
+				'</ul>' +
+				'<div class="box-body" ng-transclude="header" ng-show="paneList.length && hasHeader()"></div>' +
+				'<div class="tab-content" ng-transclude="pane"></div>' +
+				'<div class="box-footer" ng-transclude="footer" ng-show="paneList.length && hasFooter()"></div>' +
+			'</div>'
+	};
+}).directive('pane', function() {
+	'use strict';
+
+	return {
+		require : '^tabs',
+		restrict : 'AE',
+		transclude : true,
+		scope : {
+			title : '@',
+			data: '=?data',
+			menuList: "=?menu"
+		},
+		link : function(scope, element, attrs, tabsController) {
+			tabsController.addPane(scope);
+			scope.$on('$destroy', function() {
+				tabsController.deletePane(scope);
+			});
+		},
+		template : '<div class="tab-pane fade" ng-class="{active: active, in: in}" ng-transclude></div>',
+		replace : true
+	};
+}).directive('footer', function() {
+	'use strict';
+
+	return {
+		require : '^tabs',
+		restrict : 'AE',
+		transclude : true,
+		scope : {},
+		controller: function($scope, $element) {
+		},
+		template : '<div ng-transclude></div>',
+		replace : true
+	};
+});
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/js/ctrl/authController.js
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/_app/public/js/ctrl/authController.js b/eagle-webservice/src/main/webapp/_app/public/js/ctrl/authController.js
new file mode 100644
index 0000000..dbdb704
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/_app/public/js/ctrl/authController.js
@@ -0,0 +1,91 @@
+/*
+ * 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 eagleControllers = angular.module('eagleControllers');
+	// =============================================================
+	// =                     User Profile List                     =
+	// =============================================================
+	eagleControllers.controller('authLoginCtrl', function (PageConfig, Site, Authorization, Application, $scope) {
+		PageConfig.hideSidebar = true;
+		PageConfig.hideApplication = true;
+		PageConfig.hideSite = true;
+		PageConfig.hideUser = true;
+
+		$scope.username = "";
+		$scope.password = "";
+		$scope.lock = false;
+		$scope.loginSuccess = false;
+
+		if(localStorage) {
+			$scope.rememberUser = localStorage.getItem("rememberUser") !== "false";
+
+			if($scope.rememberUser) {
+				$scope.username = localStorage.getItem("username");
+				$scope.password = localStorage.getItem("password");
+			}
+		}
+
+		// UI
+		setTimeout(function () {
+			$("#username").focus();
+		});
+
+		// Login
+		$scope.login = function (event, forceSubmit) {
+			if ($scope.lock) return;
+
+			if (event.which === 13 || forceSubmit) {
+				$scope.lock = true;
+
+				Authorization.login($scope.username, $scope.password).then(function (success) {
+					if (success) {
+						// Check user remember
+						localStorage.setItem("rememberUser", $scope.rememberUser);
+						if($scope.rememberUser) {
+							localStorage.setItem("username", $scope.username);
+							localStorage.setItem("password", $scope.password);
+						} else {
+							localStorage.removeItem("username");
+							localStorage.removeItem("password");
+						}
+
+						// Initial environment
+						$scope.loginSuccess = true;
+						console.log("[Login] Login success! Reload data...");
+						Authorization.reload().then(function() {}, function() {console.warn("Site error!");});
+						Application.reload().then(function() {}, function() {console.warn("Site error!");});
+						Site.reload().then(function() {}, function() {console.warn("Site error!");});
+						Authorization.path(true);
+					} else {
+						$.dialog({
+							title: "OPS",
+							content: "User name or password not correct."
+						}).on("hidden.bs.modal", function () {
+							$("#username").focus();
+						});
+					}
+				}).finally(function () {
+					$scope.lock = false;
+				});
+			}
+		};
+	});
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/js/ctrl/configurationController.js
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/_app/public/js/ctrl/configurationController.js b/eagle-webservice/src/main/webapp/_app/public/js/ctrl/configurationController.js
new file mode 100644
index 0000000..e59198d
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/_app/public/js/ctrl/configurationController.js
@@ -0,0 +1,377 @@
+/*
+ * 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 eagleControllers = angular.module('eagleControllers');
+
+	// =============================================================
+	// =                         Function                          =
+	// =============================================================
+	function watchEdit($scope, key) {
+		$scope.changed = false;
+		setTimeout(function() {
+			var _func = $scope.$watch(key, function(newValue, oldValue) {
+				if(angular.equals(newValue, oldValue)) return;
+				$scope.changed = true;
+				_func();
+			}, true);
+		}, 100);
+	}
+
+	// =============================================================
+	// =                       Configuration                       =
+	// =============================================================
+	// ========================== Feature ==========================
+	eagleControllers.controller('configFeatureCtrl', function ($scope, PageConfig, Application, Entities, UI) {
+		PageConfig.hideApplication = true;
+		PageConfig.hideSite = true;
+		$scope._pageLock = false;
+
+		PageConfig
+			.addNavPath("Home", "/")
+			.addNavPath("Feature Config");
+
+		// ================== Feature ==================
+		// Current feature
+		$scope.feature = Application.featureList[0];
+		$scope.setFeature = function (feature) {
+			$scope.feature = feature;
+		};
+
+		// Feature list
+		$scope.features = {};
+		$.each(Application.featureList, function(i, feature) {
+			$scope.features[feature.tags.feature] = $.extend({}, feature, true);
+		});
+
+		// Create feature
+		$scope.newFeature = function() {
+			UI.createConfirm("Feature", {}, [
+				{name: "Feature Name", field: "name"}
+			], function(entity) {
+				if(entity.name && $.map($scope.features, function(feature, name) {
+						return name.toUpperCase() === entity.name.toUpperCase() ? true : null;
+					}).length) {
+					return "Feature name conflict!";
+				}
+			}).then(null, null, function(holder) {
+				Entities.updateEntity(
+					"FeatureDescService",
+					{tags: {feature: holder.entity.name}},
+					{timestamp: false}
+				)._promise.then(function() {
+					holder.closeFunc();
+					location.reload();
+				});
+			});
+		};
+
+		// Delete feature
+		$scope.deleteFeature = function(feature) {
+			UI.deleteConfirm(feature.tags.feature).then(null, null, function(holder) {
+				Entities.delete("FeatureDescService", {feature: feature.tags.feature})._promise.then(function() {
+					holder.closeFunc();
+					location.reload();
+				});
+			});
+		};
+
+		// Save feature
+		$scope.saveAll = function() {
+			$scope._pageLock = true;
+			var _list = $.map($scope.features, function(feature) {
+				return feature;
+			});
+			Entities.updateEntity("FeatureDescService", _list, {timestamp: false})._promise.success(function() {
+				location.reload();
+			}).finally(function() {
+				$scope._pageLock = false;
+			});
+		};
+
+		// Watch config update
+		watchEdit($scope, "features");
+	});
+
+	// ======================== Application ========================
+	eagleControllers.controller('configApplicationCtrl', function ($scope, $timeout, PageConfig, Application, Entities, Feature, UI) {
+		PageConfig.hideApplication = true;
+		PageConfig.hideSite = true;
+		$scope._pageLock = false;
+
+		PageConfig
+			.addNavPath("Home", "/")
+			.addNavPath("Application Config");
+
+		// ================ Application ================
+		// Current application
+		$scope.application = Application.list[0];
+		$scope.setApplication = function (application) {
+			$scope.application = application;
+		};
+
+		// Application list
+		$scope.applications = {};
+		$.each(Application.list, function(i, application) {
+			var _application = $scope.applications[application.tags.application] = $.extend({}, application, {features: application.features.slice()}, true);
+			_application.optionalFeatures = $.map(Application.featureList, function(feature) {
+				var featurePlugin = Feature.get(feature.tags.feature);
+				if(featurePlugin.config.global) return null;
+				if(!common.array.find(feature.tags.feature, _application.features)) {
+					return feature.tags.feature;
+				}
+			});
+		});
+
+		// Create application
+		$scope.newApplication = function() {
+			UI.createConfirm("Application", {}, [
+				{name: "Application Name", field: "name"}
+			], function(entity) {
+				if(entity.name && $.map($scope.applications, function(application, name) {
+						return name.toUpperCase() === entity.name.toUpperCase() ? true : null;
+					}).length) {
+					return "Application name conflict!";
+				}
+			}).then(null, null, function(holder) {
+				Entities.updateEntity(
+					"ApplicationDescService",
+					{tags: {application: holder.entity.name}},
+					{timestamp: false}
+				)._promise.then(function() {
+					holder.closeFunc();
+					location.reload();
+				});
+			});
+		};
+
+		// Delete application
+		$scope.deleteApplication = function(application) {
+			UI.deleteConfirm(application.tags.application).then(null, null, function(holder) {
+				Entities.delete("ApplicationDescService", {application: application.tags.application})._promise.then(function() {
+					holder.closeFunc();
+					location.reload();
+				});
+			});
+		};
+
+		// ================= Function ==================
+		// Configuration check
+		$scope.configCheck = function(config) {
+			if(config && !common.parseJSON(config, false)) {
+				return "Invalid JSON format";
+			}
+		};
+
+		// Feature
+		$scope._feature = "";
+		function highlightFeature(feature) {
+			$scope._feature = feature;
+
+			$timeout(function() {
+				$scope._feature = "";
+			}, 100);
+		}
+
+		$scope.addFeature = function(feature, application) {
+			application.features.push(feature);
+			common.array.remove(feature, application.optionalFeatures);
+			highlightFeature(feature);
+			$scope.changed = true;
+		};
+
+		$scope.removeFeature = function(feature, application) {
+			application.optionalFeatures.push(feature);
+			common.array.remove(feature, application.features);
+			$scope.changed = true;
+		};
+
+		$scope.moveFeature = function(feature, list, offset) {
+			common.array.moveOffset(feature, list, offset);
+			highlightFeature(feature);
+			$scope.changed = true;
+		};
+
+		// Save feature
+		$scope.saveAll = function() {
+			$scope._pageLock = true;
+
+			var _list = $.map($scope.applications, function(application) {
+				return application;
+			});
+			Entities.updateEntity("ApplicationDescService", _list, {timestamp: false})._promise.success(function() {
+				location.reload();
+			}).finally(function() {
+				$scope._pageLock = false;
+			});
+		};
+
+		// Watch config update
+		watchEdit($scope, "applications");
+	});
+
+	// ============================ Site ===========================
+	eagleControllers.controller('configSiteCtrl', function ($scope, $timeout, PageConfig, Site, Application, Entities, UI) {
+		PageConfig.hideApplication = true;
+		PageConfig.hideSite = true;
+		$scope._pageLock = false;
+
+		PageConfig
+			.addNavPath("Home", "/")
+			.addNavPath("Site Config");
+
+		// =================== Site ====================
+		// Current site
+		$scope.site = Site.list[0];
+		$scope.setSite = function (site) {
+			$scope.site = site;
+		};
+
+
+		// Site list
+		$scope.sites = {};
+		$.each(Site.list, function(i, site) {
+			var _site = $scope.sites[site.tags.site] = $.extend({}, site, true);
+			var _applications = [];
+			var _optionalApplications = [];
+
+			Object.defineProperties(_site, {
+				applications: {
+					get: function() {return _applications;}
+				},
+				optionalApplications: {
+					get: function() {return _optionalApplications;}
+				}
+			});
+
+			$.each(Application.list, function(i, application) {
+				var _application = site.applicationList.set[application.tags.application];
+				if(_application && _application.enabled) {
+					_site.applications.push(_application);
+				} else {
+					if(_application) {
+						_site.optionalApplications.push(_application);
+					} else {
+						_site.optionalApplications.push({
+							prefix: "eagleSiteApplication",
+							config: "",
+							enabled: false,
+							tags: {
+								application: application.tags.application,
+								site: site.tags.site
+							}
+						});
+					}
+				}
+			});
+		});
+
+		// Create site
+		$scope.newSite = function() {
+			UI.createConfirm("Site", {}, [
+				{name: "Site Name", field: "name"}
+			], function(entity) {
+				if(entity.name && $.map($scope.sites, function(site, name) {
+						return name.toUpperCase() === entity.name.toUpperCase() ? true : null;
+					}).length) {
+					return "Site name conflict!";
+				}
+			}).then(null, null, function(holder) {
+				Entities.updateEntity(
+					"SiteDescService",
+					{enabled: true, tags: {site: holder.entity.name}},
+					{timestamp: false}
+				)._promise.then(function() {
+					holder.closeFunc();
+					location.reload();
+				});
+			});
+		};
+
+		// Delete site
+		$scope.deleteSite = function(site) {
+			UI.deleteConfirm(site.tags.site).then(null, null, function(holder) {
+				Entities.delete("SiteDescService", {site: site.tags.site})._promise.then(function() {
+					holder.closeFunc();
+					location.reload();
+				});
+			});
+		};
+
+		// ================= Function ==================
+		$scope._application = "";
+		function highlightApplication(application) {
+			$scope._application = application;
+
+			$timeout(function() {
+				$scope._application = "";
+			}, 100);
+		}
+
+		$scope.addApplication = function(application, site) {
+			site.applications.push(application);
+			common.array.remove(application, site.optionalApplications);
+			application.enabled = true;
+			highlightApplication(application);
+			$scope.changed = true;
+		};
+
+		$scope.removeApplication = function(application, site) {
+			site.optionalApplications.push(application);
+			common.array.remove(application, site.applications);
+			application.enabled = false;
+			$scope.changed = true;
+		};
+
+		$scope.setApplication = function(application) {
+			var _oriConfig = application.config;
+			UI.updateConfirm("Application", {config: _oriConfig}, [
+				{name: "Configuration", field: "config", type: "blob"}
+			], function(entity) {
+				if(entity.config !== "" && !common.properties.check(entity.config)) {
+					return "Invalid Properties format";
+				}
+			}).then(null, null, function(holder) {
+				application.config = holder.entity.config;
+				holder.closeFunc();
+				if(_oriConfig !== application.config) $scope.changed = true;
+			});
+		};
+
+		// Save feature
+		$scope.saveAll = function() {
+			$scope._pageLock = true;
+
+			var _list = $.map($scope.sites, function(site) {
+				var _clone = $.extend({applications: site.applications.concat(site.optionalApplications)}, site);
+				return _clone;
+			});
+
+			Entities.updateEntity("SiteDescService", _list, {timestamp: false, hook: true})._promise.success(function() {
+				location.reload();
+			}).finally(function() {
+				$scope._pageLock = false;
+			});
+		};
+
+		// Watch config update
+		watchEdit($scope, "sites");
+	});
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/js/ctrl/main.js
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/_app/public/js/ctrl/main.js b/eagle-webservice/src/main/webapp/_app/public/js/ctrl/main.js
new file mode 100644
index 0000000..5064a1d
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/_app/public/js/ctrl/main.js
@@ -0,0 +1,42 @@
+/*
+ * 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 eagleControllers = angular.module('eagleControllers', ['ui.bootstrap', 'eagle.components', 'eagle.service']);
+
+	// ===========================================================
+	// =                        Controller                       =
+	// ===========================================================
+	eagleControllers.controller('landingCtrl', function($scope, $wrapState, Site, Application, PageConfig, FeaturePageConfig, Feature) {
+		var _app = Application.current();
+
+		PageConfig.pageTitle = _app ? _app.displayName : 'OPS';
+		PageConfig.pageSubTitle = Site.current().tags.site;
+
+		$scope.Application = Application;
+
+		var _navItemList = FeaturePageConfig.pageList;
+		if(_navItemList.length) {
+			console.log("[Landing] Auto redirect.", FeaturePageConfig);
+			var _match = _navItemList[0].url.match(/#\/([^\/]+)\/([^\/]+)/);
+			Feature.go(_match[1], _match[2]);
+		}
+	});
+})();

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/afb89794/eagle-webservice/src/main/webapp/_app/public/js/srv/applicationSrv.js
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/_app/public/js/srv/applicationSrv.js b/eagle-webservice/src/main/webapp/_app/public/js/srv/applicationSrv.js
new file mode 100644
index 0000000..187adb4
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/_app/public/js/srv/applicationSrv.js
@@ -0,0 +1,170 @@
+/*
+ * 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 serviceModule = angular.module('eagle.service');
+	var eagleApp = angular.module('eagleApp');
+
+	serviceModule.service('Application', function($q, $location, $wrapState, Entities) {
+		var Application = {};
+		var _current;
+		var _featureCache = {};// After loading feature will be in cache. Which will not load twice.
+		var _deferred;
+
+		Application.list = [];
+		Application.list.set = {};
+		Application.featureList = [];
+		Application.featureList.set = {};
+
+		// Set current application
+		Application.current = function(app, reload) {
+			if(arguments.length && _current !== app) {
+				var _prev = _current;
+				_current = app;
+
+				if(sessionStorage && _current) {
+					sessionStorage.setItem("application", _current.tags.application);
+				}
+
+				if(_prev && reload !== false) {
+					console.log("[Application] Switch. Redirect to landing page.");
+					$wrapState.go('landing', true);
+				}
+			}
+			return _current;
+		};
+		Application.find = function(appName) {
+			return common.array.find(appName, Application.list, "tags.application");
+		};
+
+		Application.reload = function() {
+			_deferred = $q.defer();
+
+			if(Application.list && Application.list._promise) Application.list._promise.abort();
+			if(Application.featureList && Application.featureList._promise) Application.featureList._promise.abort();
+
+			Application.list = Entities.queryEntities("ApplicationDescService", '');
+			Application.list.set = {};
+			Application.featureList = Entities.queryEntities("FeatureDescService", '');
+			Application.featureList.set = {};
+
+			Application.featureList._promise.then(function() {
+				var _promiseList;
+				// Load feature script
+				_promiseList = $.map(Application.featureList, function(feature) {
+					var _ajax_deferred, _script;
+					if(_featureCache[feature.tags.feature]) return;
+
+					_featureCache[feature.tags.feature] = true;
+					_ajax_deferred = $q.defer();
+					_script = document.createElement('script');
+					_script.type = 'text/javascript';
+					_script.src = "public/feature/" + feature.tags.feature + "/controller.js?_=" + eagleApp._TRS();
+					document.head.appendChild(_script);
+					_script.onload = function() {
+						feature._loaded = true;
+						_ajax_deferred.resolve();
+					};
+					_script.onerror = function() {
+						feature._loaded = false;
+						_featureCache[feature.tags.feature] = false;
+						_ajax_deferred.reject();
+					};
+					return _ajax_deferred.promise;
+				});
+
+				// Merge application & feature
+				Application.list._promise.then(function() {
+					// Fill feature set
+					$.each(Application.featureList, function(i, feature) {
+						Application.featureList.set[feature.tags.feature] = feature;
+					});
+
+					// Fill application set
+					$.each(Application.list, function(i, application) {
+						Application.list.set[application.tags.application] = application;
+						application.features = application.features || [];
+						var _configObj = common.parseJSON(application.config, {});
+						var _appFeatureList = $.map(application.features, function(featureName) {
+							var _feature = Application.featureList.set[featureName];
+							if(!_feature) {
+								console.warn("[Application] Feature not mapping:", application.tags.application, "-", featureName);
+							} else {
+								return _feature;
+							}
+						});
+
+						// Find feature
+						_appFeatureList.find = function(featureName) {
+							return common.array.find(featureName, _appFeatureList, "tags.feature");
+						};
+
+						Object.defineProperties(application, {
+							featureList: {
+								get: function () {
+									return _appFeatureList;
+								}
+							},
+							// Get format group name. Will mark as 'Others' if no group defined
+							group: {
+								get: function () {
+									return this.groupName || "Others";
+								}
+							},
+							configObj: {
+								get: function() {
+									return _configObj;
+								}
+							},
+							displayName: {
+								get: function() {
+									return this.alias || this.tags.application;
+								}
+							}
+						});
+					});
+
+					// Set current application
+					if(!Application.current() && sessionStorage && Application.find(sessionStorage.getItem("application"))) {
+						Application.current(Application.find(sessionStorage.getItem("application")));
+					}
+				});
+
+				// Process all promise
+				$q.all(_promiseList.concat(Application.list._promise)).finally(function() {
+					_deferred.resolve(Application);
+				});
+			}, function() {
+				_deferred.reject(Application);
+			});
+
+			return _deferred.promise;
+		};
+
+		Application._promise = function() {
+			if(!_deferred) {
+				Application.reload();
+			}
+			return _deferred.promise;
+		};
+
+		return Application;
+	});
+})();
\ No newline at end of file