eagle-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From ji...@apache.org
Subject incubator-eagle git commit: EAGLE-31 General monitoring UI support https://issues.apache.org/jira/browse/EAGLE-31
Date Mon, 29 Feb 2016 03:10:36 GMT
Repository: incubator-eagle
Updated Branches:
  refs/heads/master 1965b4aff -> 6f4fb5c4b


EAGLE-31 General monitoring UI support
https://issues.apache.org/jira/browse/EAGLE-31

create metric feature for adding metric dashboard & support user customize date storage

Author: @zombiej, @haoch, @qingwen220
Reviewer: @qingwen220

Closes #31


Project: http://git-wip-us.apache.org/repos/asf/incubator-eagle/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-eagle/commit/6f4fb5c4
Tree: http://git-wip-us.apache.org/repos/asf/incubator-eagle/tree/6f4fb5c4
Diff: http://git-wip-us.apache.org/repos/asf/incubator-eagle/diff/6f4fb5c4

Branch: refs/heads/master
Commit: 6f4fb5c4b4084067e4ad9abcfd0feef931cbd84e
Parents: 1965b4a
Author: jiljiang <jiljiang@ebay.com>
Authored: Mon Feb 29 11:04:47 2016 +0800
Committer: jiljiang <jiljiang@ebay.com>
Committed: Mon Feb 29 11:04:47 2016 +0800

----------------------------------------------------------------------
 .../src/main/bin/eagle-topology-init.sh         |  10 +-
 .../src/main/resources/hadoop-metric-init.sh    |   4 +-
 eagle-webservice/src/main/webapp/app/index.html |   4 +-
 .../src/main/webapp/app/partials/login.html     |  21 +-
 .../src/main/webapp/app/public/css/main.css     |  72 ++
 .../public/feature/common/page/policyList.html  |   2 +-
 .../app/public/feature/metrics/controller.js    | 397 +++++++++++
 .../public/feature/metrics/page/dashboard.html  | 201 ++++++
 .../src/main/webapp/app/public/js/app.ui.js     |   2 +-
 .../src/main/webapp/app/public/js/common.js     |  24 +
 .../webapp/app/public/js/components/nvd3.js     | 672 ++++++++++---------
 .../webapp/app/public/js/components/tabs.js     | 329 ++++++---
 .../webapp/app/public/js/ctrl/authController.js |  22 +
 .../app/public/js/srv/authorizationSrv.js       |  12 +
 eagle-webservice/src/main/webapp/grunt.json     |   2 +-
 eagle-webservice/src/main/webapp/package.json   |  28 +-
 16 files changed, 1363 insertions(+), 439 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/6f4fb5c4/eagle-assembly/src/main/bin/eagle-topology-init.sh
----------------------------------------------------------------------
diff --git a/eagle-assembly/src/main/bin/eagle-topology-init.sh b/eagle-assembly/src/main/bin/eagle-topology-init.sh
index 2a3032e..2e0c7b8 100755
--- a/eagle-assembly/src/main/bin/eagle-topology-init.sh
+++ b/eagle-assembly/src/main/bin/eagle-topology-init.sh
@@ -49,13 +49,15 @@ curl -u ${EAGLE_SERVICE_USER}:${EAGLE_SERVICE_PASSWD} -X POST -H 'Content-Type:a
 
 echo ""
 echo "Importing feature definitions ..."
-curl -u ${EAGLE_SERVICE_USER}:${EAGLE_SERVICE_PASSWD} -X POST -H 'Content-Type:application/json' "http://${EAGLE_SERVICE_HOST}:${EAGLE_SERVICE_PORT}/eagle-service/rest/entities?serviceName=FeatureDescService" -d '[{"prefix":"eagleFeatureDesc","tags":{"feature":"common"},"desc":"Provide the Policy & Alert feature.","version":"01"}]'
+curl -u ${EAGLE_SERVICE_USER}:${EAGLE_SERVICE_PASSWD} -X POST -H 'Content-Type:application/json' "http://${EAGLE_SERVICE_HOST}:${EAGLE_SERVICE_PORT}/eagle-service/rest/entities?serviceName=FeatureDescService" -d '[{"prefix":"eagleFeatureDesc","tags":{"feature":"common"},"desc":"Provide the Policy & Alert feature.","version":"v0.3.0"}]'
 
-curl -u ${EAGLE_SERVICE_USER}:${EAGLE_SERVICE_PASSWD} -X POST -H 'Content-Type:application/json' "http://${EAGLE_SERVICE_HOST}:${EAGLE_SERVICE_PORT}/eagle-service/rest/entities?serviceName=FeatureDescService" -d '[{"prefix":"eagleFeatureDesc","tags":{"feature":"classification"},"desc":"Sensitivity browser of the data classification.","version":"01"}]'
+curl -u ${EAGLE_SERVICE_USER}:${EAGLE_SERVICE_PASSWD} -X POST -H 'Content-Type:application/json' "http://${EAGLE_SERVICE_HOST}:${EAGLE_SERVICE_PORT}/eagle-service/rest/entities?serviceName=FeatureDescService" -d '[{"prefix":"eagleFeatureDesc","tags":{"feature":"classification"},"desc":"Sensitivity browser of the data classification.","version":"v0.3.0"}]'
 
-curl -u ${EAGLE_SERVICE_USER}:${EAGLE_SERVICE_PASSWD} -X POST -H 'Content-Type:application/json' "http://${EAGLE_SERVICE_HOST}:${EAGLE_SERVICE_PORT}/eagle-service/rest/entities?serviceName=FeatureDescService" -d '[{"prefix":"eagleFeatureDesc","tags":{"feature":"userProfile"},"desc":"Machine learning of the user profile","version":"01"}]'
+curl -u ${EAGLE_SERVICE_USER}:${EAGLE_SERVICE_PASSWD} -X POST -H 'Content-Type:application/json' "http://${EAGLE_SERVICE_HOST}:${EAGLE_SERVICE_PORT}/eagle-service/rest/entities?serviceName=FeatureDescService" -d '[{"prefix":"eagleFeatureDesc","tags":{"feature":"userProfile"},"desc":"Machine learning of the user profile","version":"v0.3.0"}]'
 
-curl -u ${EAGLE_SERVICE_USER}:${EAGLE_SERVICE_PASSWD} -X POST -H 'Content-Type:application/json' "http://${EAGLE_SERVICE_HOST}:${EAGLE_SERVICE_PORT}/eagle-service/rest/entities?serviceName=FeatureDescService" -d '[{"prefix":"eagleFeatureDesc","tags":{"feature":"metadata"},"desc":"Stream metadata viewer","version":"01"}]'
+curl -u ${EAGLE_SERVICE_USER}:${EAGLE_SERVICE_PASSWD} -X POST -H 'Content-Type:application/json' "http://${EAGLE_SERVICE_HOST}:${EAGLE_SERVICE_PORT}/eagle-service/rest/entities?serviceName=FeatureDescService" -d '[{"prefix":"eagleFeatureDesc","tags":{"feature":"metadata"},"desc":"Stream metadata viewer","version":"v0.3.0"}]'
+
+curl -u ${EAGLE_SERVICE_USER}:${EAGLE_SERVICE_PASSWD} -X POST -H 'Content-Type:application/json' "http://${EAGLE_SERVICE_HOST}:${EAGLE_SERVICE_PORT}/eagle-service/rest/entities?serviceName=FeatureDescService" -d '[{"prefix":"eagleFeatureDesc","tags":{"feature":"metrics"},"desc":"Metrics dashboard","version":"v0.3.0"}]'
 
 
 ## AlertStreamService: alert streams generated from data source

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/6f4fb5c4/eagle-hadoop-metric/src/main/resources/hadoop-metric-init.sh
----------------------------------------------------------------------
diff --git a/eagle-hadoop-metric/src/main/resources/hadoop-metric-init.sh b/eagle-hadoop-metric/src/main/resources/hadoop-metric-init.sh
index 17923c7..6ba119c 100755
--- a/eagle-hadoop-metric/src/main/resources/hadoop-metric-init.sh
+++ b/eagle-hadoop-metric/src/main/resources/hadoop-metric-init.sh
@@ -34,7 +34,7 @@ curl -u ${EAGLE_SERVICE_USER}:${EAGLE_SERVICE_PASSWD} -X POST -H 'Content-Type:a
            "application":"hadoopJmxMetricDataSource"
         },
         "enabled": true,
-        "config":"{}"
+        "config": "{\"druid\": {\"coordinator\": \"coordinatorHost:port\", \"broker\": \"brokerHost:port\"}}"
      }
   ]
   '
@@ -177,4 +177,4 @@ curl -u ${EAGLE_SERVICE_USER}:${EAGLE_SERVICE_PASSWD} -X POST -H 'Content-Type:a
 
 ## Finished
 echo ""
-echo "Finished initialization for eagle topology"
\ No newline at end of file
+echo "Finished initialization for eagle topology"

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/6f4fb5c4/eagle-webservice/src/main/webapp/app/index.html
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/app/index.html b/eagle-webservice/src/main/webapp/app/index.html
index f3d61a4..f1e1689 100644
--- a/eagle-webservice/src/main/webapp/app/index.html
+++ b/eagle-webservice/src/main/webapp/app/index.html
@@ -103,7 +103,7 @@
 									<!-- Menu Footer-->
 									<li class="user-footer">
 										<div class="pull-left" ng-if="Auth.isRole('ROLE_ADMIN')">
-											<a href="#/config/site" class="btn btn-default btn-flat">Configuration</a>
+											<a href="#/config/site" class="btn btn-default btn-flat">Management</a>
 										</div>
 										<div class="pull-right">
 											<a ng-click="logout();" class="btn btn-default btn-flat">Sign out</a>
@@ -203,7 +203,7 @@
 		<script src="../node_modules/angular-resource/angular-resource.js"></script>
 		<script src="../node_modules/angular-route/angular-route.js"></script>
 		<script src="../node_modules/angular-animate/angular-animate.js"></script>
-		<script src="../node_modules/angular-ui-bootstrap/ui-bootstrap-tpls.js"></script>
+		<script src="../node_modules/angular-ui-bootstrap/dist/ui-bootstrap-tpls.js"></script>
 		<script src="../node_modules/angular-ui-router/release/angular-ui-router.js"></script>
 		<script src="../node_modules/d3/d3.js"></script>
 		<script src="../node_modules/zombiej-nvd3/build/nv.d3.js"></script>

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/6f4fb5c4/eagle-webservice/src/main/webapp/app/partials/login.html
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/app/partials/login.html b/eagle-webservice/src/main/webapp/app/partials/login.html
index 112a76c..7faef42 100644
--- a/eagle-webservice/src/main/webapp/app/partials/login.html
+++ b/eagle-webservice/src/main/webapp/app/partials/login.html
@@ -20,8 +20,8 @@
 	<div class="login-logo">
 		<a href="#/">Apache Eagle</a>
 	</div>
-	<!-- /.login-logo -->
-	<div class="login-box-body">
+
+	<div class="login-box-body" ng-show="!loginSuccess">
 		<p class="login-box-msg">Sign in to start your session</p>
 		<div class="form-group has-feedback">
 			<input type="text" class="form-control" placeholder="User Name" ng-model="username" ng-keypress="login($event)" autocomplete="off" id="username">
@@ -33,17 +33,22 @@
 		</div>
 		<div class="row">
 			<div class="col-xs-8">
-				<!--div class="checkbox">
-					<label> <input type="checkbox" /> Remember Me
+				<div class="checkbox">
+					<label> <input type="checkbox" ng-checked="rememberUser" ng-click="rememberUser = !rememberUser;" /> Remember Me
 					</label>
-				</div-->
+				</div>
 			</div>
-			<!-- /.col -->
 			<div class="col-xs-4">
 				<button class="btn btn-primary btn-block btn-flat" ng-click="login($event, true)" ng-disabled="lock">Sign In</button>
 			</div>
-			<!-- /.col -->
 		</div>
 	</div>
-	<!-- /.login-box-body -->
+
+	<div class="login-box-body text-center" ng-show="loginSuccess">
+		<p class="login-box-msg">Login success</p>
+		<p>
+			<span class="fa fa-refresh fa-spin"></span>
+			Loading environment...
+		</p>
+	</div>
 </div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/6f4fb5c4/eagle-webservice/src/main/webapp/app/public/css/main.css
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/app/public/css/main.css b/eagle-webservice/src/main/webapp/app/public/css/main.css
index 2e951f2..b32c4fe 100644
--- a/eagle-webservice/src/main/webapp/app/public/css/main.css
+++ b/eagle-webservice/src/main/webapp/app/public/css/main.css
@@ -295,6 +295,12 @@ ul.nav.nav-tabs li .btn {
 	background: #dd4b39;
 }
 
+/* Drop Down */
+.dropdown-menu.left {
+	right: 0;
+	left: auto;
+}
+
 .dropdown-submenu{position:relative;}
 .dropdown-submenu>.dropdown-menu{top:0;left:100%;margin-top:-6px;margin-left:-1px;-webkit-border-radius:0 6px 6px 6px;-moz-border-radius:0 6px 6px 6px;border-radius:0 6px 6px 6px;}
 .dropdown-submenu:hover>.dropdown-menu{display:block;}
@@ -319,6 +325,11 @@ ul.nav.nav-tabs li .btn {
 	box-shadow: none;
 }
 
+.checkbox.noMargin {
+	margin-top: 0;
+	margin-bottom: 5px;
+}
+
 /* UL */
 ul.path {
 	margin-left: 0;
@@ -395,6 +406,7 @@ ul.tree.tree-bordered > li > ul > li > a {
 	transition: none;
 }
 
+.nav.fixed-height,
 .products-list.fixed-height {
 	height: 402px;
 	overflow-y: auto;
@@ -435,10 +447,45 @@ ul.tree.tree-bordered > li > ul > li > a {
 }
 
 /* Chart */
+.nvd3-chart-wrapper {
+	position: relative;
+	//border: 1px solid rgba(0,0,0,0);
+}
+.nvd3-chart-wrapper:hover {
+	//border-color: #F4F4F4;
+}
+
+.nvd3-chart-wrapper .nvd3-chart-config {
+	position: absolute;
+	top: 5px;
+	right: 10px;
+	display: none;
+	border-radius: 3px;
+	padding: 0 5px;
+	background: rgba(0,0,0,0.7);
+}
+.nvd3-chart-wrapper:hover .nvd3-chart-config {
+	display: block;
+}
+
+.nvd3-chart-wrapper .nvd3-chart-config a {
+	color: rgba(255,255,255, 0.9);
+	padding: 5px 5px 4px 5px;
+	font-size: 16px;
+}
+.nvd3-chart-wrapper .nvd3-chart-config a:hover {
+	color: #FFFFFF;
+}
+
+.nvd3-chart-cntr {
+	padding-top: 10px;
+}
+
 .nvd3-chart-cntr > h3 {
 	text-align: center;
 	font-size: 16px;
 	font-weight: bolder;
+	margin: 0;
 }
 
 .nvd3-chart-cntr > svg.nvd3-svg {
@@ -457,17 +504,36 @@ body .tab-content>.tab-pane.active {
 	overflow-x: visible;
 	overflow-y: visible;
 }
+body .tab-content>.tab-pane.ng-animate {
+	transition: 0s;
+}
 
+body .modal-body .nav-pills > li > a,
 body .box-body .nav-pills > li > a {
 	padding: 5px 15px;
 	border: none;
 }
 
+body .modal-body .nav-stacked > li {
+	border-bottom: 1px solid #f4f4f4;
+	margin: 0;
+}
+body .modal-body .nav-stacked > li:last-child {
+	border-bottom: none;
+}
+
 /* Box */
 .box .guideline {
 	margin-top: 0;
 }
 
+/* Navigation Tab */
+.nav-tabs-custom .box-tools {
+	position: absolute;
+	right: 15px;
+	top: 8px;
+}
+
 /* Customize */
 #content {
 	position: relative;
@@ -600,6 +666,12 @@ td.text-ellipsis {
 	display: table-cell;
 }
 
+.text-breakall {
+	max-width: 100%;
+	display: inline-block;
+	word-wrap: break-word;
+}
+
 .btn.btn-xs.sm {
 	font-size: 12px;
 	padding: 2px 6px;

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/6f4fb5c4/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
index d7141a6..45d7d02 100644
--- 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
@@ -64,7 +64,7 @@
 				<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><a href="#/common/policyDetail/{{item.encodedRowkey}}" style="width: 200px;" class="text-breakall">{{item.tags.policyId}}</a></td>
 						<td>{{item.desc}}</td>
 						<td>{{item.owner}}</td>
 						<td>{{common.format.date(item.lastModifiedDate) || "-"}}</td>

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/6f4fb5c4/eagle-webservice/src/main/webapp/app/public/feature/metrics/controller.js
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/app/public/feature/metrics/controller.js b/eagle-webservice/src/main/webapp/app/public/feature/metrics/controller.js
new file mode 100644
index 0000000..eecd732
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/app/public/feature/metrics/controller.js
@@ -0,0 +1,397 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+(function() {
+	'use strict';
+
+	var featureControllers = angular.module('featureControllers');
+	var feature = featureControllers.register("metrics");
+
+	// ==============================================================
+	// =                       Initialization                       =
+	// ==============================================================
+
+	// ==============================================================
+	// =                         Controller                         =
+	// ==============================================================
+
+	// ========================= Dashboard ==========================
+	feature.navItem("dashboard", "Metrics", "line-chart");
+
+	feature.controller('dashboard', function(PageConfig, $scope, $http, $q, UI, Site, Authorization, Application, Entities) {
+		var _siteApp = Site.currentSiteApplication();
+		var _druidConfig = _siteApp.configObj.druid;
+
+		var _menu_newChart;
+
+		$scope.lock = false;
+
+		$scope.dataSourceListReady = false;
+		$scope.dataSourceList = [];
+		$scope.dashboard = {
+			groups: []
+		};
+		$scope.dashboardEntity = null;
+		$scope.dashboardReady = false;
+
+		$scope._newMetricFilter = "";
+		$scope._newMetricDataSrc = null;
+		$scope._newMetricDataMetric = null;
+
+		$scope.tabHolder = {};
+
+		$scope.endTime = app.time.now();
+		$scope.startTime = $scope.endTime.clone();
+
+		// =================== Initialization ===================
+		if(!_druidConfig || !_druidConfig.coordinator || !_druidConfig.broker) {
+			$.dialog({
+				title: "OPS",
+				content: "Druid configuration can't be empty!"
+			});
+			return;
+		}
+
+		$scope.autoRefreshList = [
+			{title: "Last 1 Month", timeDes: "day", getStartTime: function(endTime) {return endTime.clone().subtract(1, "month");}},
+			{title: "Last 1 Day", timeDes: "thirty_minute", getStartTime: function(endTime) {return endTime.clone().subtract(1, "day");}},
+			{title: "Last 6 Hour", timeDes: "fifteen_minute", getStartTime: function(endTime) {return endTime.clone().subtract(6, "hour");}},
+			{title: "Last 2 Hour", timeDes: "fifteen_minute", getStartTime: function(endTime) {return endTime.clone().subtract(2, "hour");}},
+			{title: "Last 1 Hour", timeDes: "minute", getStartTime: function(endTime) {return endTime.clone().subtract(1, "hour");}}
+		];
+		$scope.autoRefreshSelect = $scope.autoRefreshList[2];
+
+		// ====================== Function ======================
+		$scope.setAuthRefresh = function(item) {
+			$scope.autoRefreshSelect = item;
+			$scope.chartRefresh(true);
+		};
+
+		$scope.refreshTimeDisplay = function() {
+			PageConfig.pageSubTitle = common.format.date($scope.startTime) + " ~ " + common.format.date($scope.endTime) + " [refresh interval: 30s]";
+		};
+		$scope.refreshTimeDisplay();
+
+		// ======================= Metric =======================
+		// Fetch metric data
+		$http.get(_druidConfig.coordinator + "/druid/coordinator/v1/metadata/datasources", {withCredentials: false}).then(function(data) {
+			var _endTime = new moment();
+			var _startTime = _endTime.clone().subtract(1, "day");
+			var _intervals = _startTime.toISOString() + "/" + _endTime.toISOString();
+
+			$scope.dataSourceList = $.map(data.data, function(dataSrc) {
+				return {
+					dataSource: dataSrc,
+					metricList: []
+				};
+			});
+
+			// List dataSource metrics
+			var _metrixList_promiseList = $.map($scope.dataSourceList, function(dataSrc) {
+				var _data = JSON.stringify({
+					"queryType": "groupBy",
+					"dataSource": dataSrc.dataSource,
+					"granularity": "all",
+					"dimensions": ["metric"],
+					"aggregations": [
+						{
+							"type":"count",
+							"name":"count"
+						}
+					],
+					"intervals": [_intervals]
+				});
+
+				return $http.post(_druidConfig.broker + "/druid/v2", _data, {withCredentials: false}).then(function(response) {
+					dataSrc.metricList = $.map(response.data, function(entity) {
+						return entity.event.metric;
+					});
+				});
+			});
+
+			$q.all(_metrixList_promiseList).finally(function() {
+				$scope.dataSourceListReady = true;
+
+				$scope._newMetricDataSrc = $scope.dataSourceList[0];
+				$scope._newMetricDataMetric = common.getValueByPath($scope._newMetricDataSrc, "metricList.0");
+			});
+		}, function() {
+			$.dialog({
+				title: "OPS",
+				content: "Fetch data source failed. Please check Site Application Metrics configuration."
+			});
+		});
+
+		// Filter data source
+		$scope.dataSourceMetricList = function(dataSrc, filter) {
+			filter = (filter || "").toLowerCase().trim().split(/\s+/);
+			return $.grep((dataSrc && dataSrc.metricList) || [], function(metric) {
+				for(var i = 0 ; i < filter.length ; i += 1) {
+					if(metric.toLowerCase().indexOf(filter[i]) === -1) return false;
+				}
+				return true;
+			});
+		};
+
+		// New metric select
+		$scope.newMetricSelectDataSource = function(dataSrc) {
+			if(dataSrc !== $scope._newMetricDataMetric) $scope._newMetricDataMetric = dataSrc.metricList[0];
+			$scope._newMetricDataSrc = dataSrc;
+		};
+		$scope.newMetricSelectMetric = function(metric) {
+			$scope._newMetricDataMetric = metric;
+		};
+
+		// Confirm new metric
+		$scope.confirmSelectMetric = function() {
+			var group = $scope.tabHolder.selectedPane.data;
+			$("#metricMDL").modal('hide');
+
+			group.charts.push({
+				chart: "line",
+				dataSource: $scope._newMetricDataSrc.dataSource,
+				metric: $scope._newMetricDataMetric,
+				aggregations: ["max"]
+			});
+
+			$scope.chartRefresh();
+		};
+
+		// ======================== Menu ========================
+		function newGroup() {
+			if($scope.lock) return;
+
+			UI.createConfirm("Group", {}, [{field: "name"}], function(entity) {
+				if(common.array.find(entity.name, $scope.dashboard.groups, "name")) {
+					return "Group name conflict";
+				}
+			}).then(null, null, function(holder) {
+				$scope.dashboard.groups.push({
+					name: holder.entity.name,
+					charts: []
+				});
+				holder.closeFunc();
+
+				setTimeout(function() {
+					$scope.tabHolder.setSelect(holder.entity.name);
+				}, 0);
+			});
+		}
+
+		function deleteGroup() {
+			var group = $scope.tabHolder.selectedPane.data;
+			UI.deleteConfirm(group.name).then(null, null, function(holder) {
+				common.array.remove(group, $scope.dashboard.groups);
+				holder.closeFunc();
+			});
+		}
+
+		_menu_newChart = {title: "Add Metric", func: function() {$scope.newChart();}};
+		Object.defineProperties(_menu_newChart, {
+			icon: {
+				get: function() {return $scope.dataSourceListReady ? 'plus' : 'refresh fa-spin';}
+			},
+			disabled: {
+				get: function() {return !$scope.dataSourceListReady;}
+			}
+		});
+
+		$scope.menu = Authorization.isRole('ROLE_ADMIN') ? [
+			{icon: "cog", title: "Configuration", list: [
+				_menu_newChart,
+				{icon: "trash", title: "Delete Group", danger: true, func: deleteGroup}
+			]},
+			{icon: "plus", title: "New Group", func: newGroup}
+		] : [];
+
+		// ===================== Dashboard ======================
+		$scope.dashboardList = Entities.queryEntities("GenericResourceService", {
+			site: Site.current().tags.site,
+			application: Application.current().tags.application
+		});
+		$scope.dashboardList._promise.then(function(list) {
+			$scope.dashboardEntity = list[0];
+			$scope.dashboard = $scope.dashboardEntity ? common.parseJSON($scope.dashboardEntity.value) : {groups: []};
+			$scope.chartRefresh();
+		}).finally(function() {
+			$scope.dashboardReady = true;
+		});
+
+		$scope.saveDashboard = function() {
+			$scope.lock = true;
+
+			if(!$scope.dashboardEntity) {
+				$scope.dashboardEntity = {
+					tags: {
+						site: Site.current().tags.site,
+						application: Application.current().tags.application,
+						name: "/metric_dashboard/dashboard/default"
+					}
+				};
+			}
+			$scope.dashboardEntity.value = common.stringify($scope.dashboard);
+
+			Entities.updateEntity("GenericResourceService", $scope.dashboardEntity)._promise.then(function() {
+				$.dialog({
+					title: "Done",
+					content: "Save success!"
+				});
+			}, function() {
+				$.dialog({
+					title: "POS",
+					content: "Save failed. Please retry."
+				});
+			}).finally(function() {
+				$scope.lock = false;
+			});
+		};
+
+		// ======================= Chart ========================
+		$scope.configTargetChart = null;
+		$scope.configPreviewChart = null;
+
+		$scope.chartConfig = {
+			xType: "time"
+		};
+
+		$scope.chartTypeList = [
+			{icon: "line-chart", chart: "line"},
+			{icon: "area-chart", chart: "area"},
+			{icon: "bar-chart", chart: "column"},
+			{icon: "pie-chart", chart: "pie"}
+		];
+
+		$scope.chartSeriesList = [
+			{name: "Min", series: "min"},
+			{name: "Max", series: "max"}
+		];
+
+		$scope.newChart = function() {
+			$("#metricMDL").modal();
+		};
+
+		$scope.configPreviewChartMinimumCheck = function() {
+			$scope.configPreviewChart.min = $scope.configPreviewChart.min === 0 ? undefined : 0;
+			window.ccc1 = $scope.getChartConfig($scope.configPreviewChart);
+		};
+
+		$scope.seriesChecked = function(chart, series) {
+			if(!chart) return false;
+			return $.inArray(series, chart.aggregations || []) !== -1;
+		};
+		$scope.seriesCheckClick = function(chart, series) {
+			if(!chart) return;
+			if($scope.seriesChecked(chart, series)) {
+				common.array.remove(series, chart.aggregations);
+			} else {
+				chart.aggregations.push(series);
+			}
+			$scope.chartSeriesUpdate(chart);
+		};
+
+		$scope.chartSeriesUpdate = function(chart) {
+			chart._data = $.map(chart._oriData, function(series) {
+				if($.inArray(series.key, chart.aggregations) !== -1) return series;
+			});
+		};
+
+		$scope.getChartConfig = function(chart) {
+			if(!chart) return null;
+
+			var _config = chart._config = chart._config || $.extend({}, $scope.chartConfig);
+			_config.yMin = chart.min;
+
+			return _config;
+		};
+
+		$scope.configChart = function(chart) {
+			$scope.configTargetChart = chart;
+			$scope.configPreviewChart = $.extend({}, chart);
+			delete $scope.configPreviewChart._config;
+			$("#chartMDL").modal();
+			setTimeout(function() {
+				$(window).resize();
+			}, 200);
+		};
+
+		$scope.confirmUpdateChart = function() {
+			$("#chartMDL").modal('hide');
+			common.extend($scope.configTargetChart, $scope.configPreviewChart);
+			$scope.chartSeriesUpdate($scope.configTargetChart);
+			if($scope.configTargetChart._holder) $scope.configTargetChart._holder.refreshAll();
+		};
+
+		$scope.deleteChart = function(group, chart) {
+			UI.deleteConfirm(chart.metric).then(null, null, function(holder) {
+				common.array.remove(chart, group.charts);
+				holder.closeFunc();
+				$scope.chartRefresh();
+			});
+		};
+
+		$scope.chartRefresh = function(forceRefresh) {
+			setTimeout(function() {
+				$scope.endTime = app.time.now();
+				$scope.startTime = $scope.autoRefreshSelect.getStartTime($scope.endTime);
+				var _intervals = $scope.startTime.toISOString() + "/" + $scope.endTime.toISOString();
+
+				$scope.refreshTimeDisplay();
+
+				$.each($scope.dashboard.groups, function (i, group) {
+					$.each(group.charts, function (j, chart) {
+						var _data = JSON.stringify({
+							"queryType": "groupBy",
+							"dataSource": chart.dataSource,
+							"granularity": $scope.autoRefreshSelect.timeDes,
+							"dimensions": ["metric"],
+							"filter": {"type": "selector", "dimension": "metric", "value": chart.metric},
+							"aggregations": [
+								{
+									"type": "max",
+									"name": "max",
+									"fieldName": "maxValue"
+								},
+								{
+									"type": "min",
+									"name": "min",
+									"fieldName": "maxValue"
+								}
+							],
+							"intervals": [_intervals]
+						});
+
+						if (!chart._data || forceRefresh) {
+							$http.post(_druidConfig.broker + "/druid/v2", _data, {withCredentials: false}).then(function (response) {
+								chart._oriData = nvd3.convert.druid([response.data]);
+								$scope.chartSeriesUpdate(chart);
+								if(chart._holder) chart._holder.refresh();
+							});
+						} else {
+							if(chart._holder) chart._holder.refresh();
+						}
+					});
+				});
+			}, 0);
+		};
+
+		setInterval(function() {
+			if(!$scope.dashboardReady) return;
+			$scope.chartRefresh(true);
+		}, 1000 * 30);
+	});
+})();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/6f4fb5c4/eagle-webservice/src/main/webapp/app/public/feature/metrics/page/dashboard.html
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/app/public/feature/metrics/page/dashboard.html b/eagle-webservice/src/main/webapp/app/public/feature/metrics/page/dashboard.html
new file mode 100644
index 0000000..d7e937f
--- /dev/null
+++ b/eagle-webservice/src/main/webapp/app/public/feature/metrics/page/dashboard.html
@@ -0,0 +1,201 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<div class="page-fixed">
+	<div class="dropdown inline">
+		<button data-toggle="dropdown" class="btn btn-primary">
+			<span class="fa fa-clock-o"></span> {{autoRefreshSelect.title}}
+		</button>
+		<ul class="dropdown-menu left">
+			<li ng-repeat="item in autoRefreshList track by $index">
+				<a ng-click="setAuthRefresh(item)">
+					<span class="fa fa-clock-o"></span>
+					<span ng-class="{'text-bold': item === autoRefreshSelect}">{{item.title}}</span>
+				</a>
+			</li>
+		</ul>
+	</div>
+
+	<button class="btn btn-primary" ng-if="Auth.isRole('ROLE_ADMIN')" ng-show="dashboard.groups.length"
+			ng-click="saveDashboard()" ng-disabled="lock">
+		<span class="fa fa-floppy-o"></span> Save
+	</button>
+</div>
+
+<div class="box box-default" ng-if="!dashboard.groups.length">
+	<div class="box-header with-border">
+		<h3 class="box-title">Empty Dashboard</h3>
+	</div>
+	<div class="box-body">
+		<div ng-show="!dashboardReady">
+			Loading...
+		</div>
+		<div ng-show="dashboardReady">
+			Current dashboard is empty.
+			<span ng-if="Auth.isRole('ROLE_ADMIN')">Click <a ng-click="newGroup()">here</a> to create a new group.</span>
+		</div>
+	</div>
+	<div class="overlay" ng-show="!dashboardReady">
+		<i class="fa fa-refresh fa-spin"></i>
+	</div>
+</div>
+
+<div tabs menu="menu" holder="tabHolder" ng-show="dashboard.groups.length">
+	<pane ng-repeat="group in dashboard.groups" data-data="group" data-title="{{group.name}}">
+		<div class="row">
+			<div ng-repeat="chart in group.charts track by $index" class="col-md-6">
+				<div class="nvd3-chart-wrapper">
+					<div nvd3="chart._data" data-holder="chart._holder" data-title="{{chart.metric}}" data-watching="false"
+						 data-chart="{{chart.chart || 'line'}}" data-config="getChartConfig(chart)" class="nvd3-chart-cntr"></div>
+					<div class="nvd3-chart-config" ng-if="Auth.isRole('ROLE_ADMIN')">
+						<a class="fa fa-cog" ng-click="configChart(chart)"></a>
+						<a class="fa fa-trash" ng-click="deleteChart(group, chart)"></a>
+					</div>
+				</div>
+			</div>
+		</div>
+
+		<p ng-if="!group.charts.length">
+			Empty group.
+			<span ng-if="Auth.isRole('ROLE_ADMIN')">Click <a ng-click="newChart()">here</a> to add metric.</span>
+		</p>
+	</pane>
+</div>
+
+
+
+<!-- Modal: Metric selector -->
+<div class="modal fade" id="metricMDL" tabindex="-1" role="dialog">
+	<div class="modal-dialog modal-lg" role="document">
+		<div class="modal-content">
+			<div class="modal-header">
+				<button type="button" class="close" data-dismiss="modal" aria-label="Close">
+					<span aria-hidden="true">&times;</span>
+				</button>
+				<h4 class="modal-title">Select Metric</h4>
+			</div>
+			<div class="modal-body">
+				<div class="input-group" style="margin-bottom: 10px;">
+					<input type="text" class="form-control" placeholder="Filter..." ng-model="_newMetricFilter" />
+					<span class="input-group-btn">
+						<button class="btn btn-default btn-flat" ng-click="_newMetricFilter = '';"><span class="fa fa-times"></span></button>
+					</span>
+				</div>
+
+				<div class="row">
+					<div class="col-md-4">
+						<ul class="nav nav-pills nav-stacked fixed-height">
+							<li class="disabled"><a>Data Source</a></li>
+							<li ng-repeat="dataSrc in dataSourceList track by $index" ng-class="{active: _newMetricDataSrc === dataSrc}">
+								<a ng-click="newMetricSelectDataSource(dataSrc)">{{dataSrc.dataSource}}</a>
+							</li>
+						</ul>
+					</div>
+					<div class="col-md-8">
+						<ul class="nav nav-pills nav-stacked fixed-height">
+							<li class="disabled"><a>Metric</a></li>
+							<li ng-repeat="metric in dataSourceMetricList(_newMetricDataSrc, _newMetricFilter) track by $index" ng-class="{active: _newMetricDataMetric === metric}">
+								<a ng-click="newMetricSelectMetric(metric)">{{metric}}</a>
+							</li>
+						</ul>
+					</div>
+				</div>
+			</div>
+			<div class="modal-footer">
+				<span class="text-primary pull-left">{{_newMetricDataSrc.dataSource}} <span class="fa fa-arrow-right"></span> {{_newMetricDataMetric}}</span>
+				<button type="button" class="btn btn-default" data-dismiss="modal">
+					Close
+				</button>
+				<button type="button" class="btn btn-primary" ng-click="confirmSelectMetric()">
+					Select
+				</button>
+			</div>
+		</div>
+	</div>
+</div>
+
+
+
+<!-- Modal: Chart configuration -->
+<div class="modal fade" id="chartMDL" tabindex="-1" role="dialog">
+	<div class="modal-dialog modal-lg" role="document">
+		<div class="modal-content">
+			<div class="modal-header">
+				<button type="button" class="close" data-dismiss="modal" aria-label="Close">
+					<span aria-hidden="true">&times;</span>
+				</button>
+				<h4 class="modal-title">Chart Configuration</h4>
+			</div>
+			<div class="modal-body">
+				<div class="row">
+					<div class="col-md-6">
+						<div class="nvd3-chart-wrapper">
+							<div nvd3="configPreviewChart._data" data-title="{{configPreviewChart.metric}}" data-holder="configPreviewChart._holder"
+								 data-watching="true" data-chart="{{configPreviewChart.chart || 'line'}}" data-config="getChartConfig(configPreviewChart)" class="nvd3-chart-cntr"></div>
+						</div>
+					</div>
+					<div class="col-md-6">
+						<!-- Chart Configuration -->
+						<table class="table">
+							<tbody>
+							<tr>
+								<th width="100">Chart Type</th>
+								<td>
+									<div class="btn-group" data-toggle="buttons">
+										<label class="btn btn-default btn-xs" ng-class="{active: (configPreviewChart.chart || 'line') === type.chart}"
+											   ng-repeat="type in chartTypeList track by $index" ng-click="configPreviewChart.chart = type.chart;">
+											<input type="radio" name="chartType" autocomplete="off"
+												   ng-checked="(configPreviewChart.chart || 'line') === type.chart">
+											<span class="fa fa-{{type.icon}}"></span>
+										</label>
+									</div>
+								</td>
+							</tr>
+							<tr>
+								<th>Minimum</th>
+								<td><input type="checkbox" ng-checked="configPreviewChart.min === 0" ng-disabled="configPreviewChart.chart === 'area' || configPreviewChart.chart === 'pie'"
+										   ng-click="configPreviewChartMinimumCheck()" /></td>
+							</tr>
+							<tr>
+								<th>Series</th>
+								<td>
+									<div class="checkbox noMargin" ng-repeat="series in chartSeriesList track by $index">
+										<label>
+											<input type="checkbox" ng-checked="seriesChecked(configPreviewChart, series.series)"
+												   ng-click="seriesCheckClick(configPreviewChart, series.series)" />
+											{{series.name}}
+										</label>
+									</div>
+								</td>
+							</tr>
+							</tbody>
+						</table>
+					</div>
+				</div>
+			</div>
+			<div class="modal-footer">
+				<button type="button" class="btn btn-default" data-dismiss="modal">
+					Close
+				</button>
+				<button type="button" class="btn btn-primary" ng-click="confirmUpdateChart()">
+					Confirm
+				</button>
+			</div>
+		</div>
+	</div>
+</div>
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/6f4fb5c4/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
index 28727ba..893ae0f 100644
--- a/eagle-webservice/src/main/webapp/app/public/js/app.ui.js
+++ b/eagle-webservice/src/main/webapp/app/public/js/app.ui.js
@@ -68,7 +68,7 @@
 	var _modal = $.fn.modal;
 	$.fn.modal = function() {
 		setTimeout(function() {
-			$(this).find("input, textarea").filter(':visible:first').focus();
+			$(this).find("input[type='text'], textarea").filter(':visible:first').focus();
 		}.bind(this), 500);
 		_modal.apply(this, arguments);
 		return this;

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/6f4fb5c4/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
index 706eeec..92c4f86 100644
--- a/eagle-webservice/src/main/webapp/app/public/js/common.js
+++ b/eagle-webservice/src/main/webapp/app/public/js/common.js
@@ -97,6 +97,13 @@ common.parseJSON = function (str, defaultVal) {
 	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;
@@ -105,6 +112,15 @@ common.isEmpty = function(val) {
 	}
 };
 
+common.extend = function(target, origin) {
+	$.each(origin, function(key, value) {
+		if(/^(_|\$)/.test(key)) return;
+
+		target[key] = value;
+	});
+	return target;
+};
+
 // ====================== Format ======================
 common.format = {};
 
@@ -118,6 +134,14 @@ common.format.date = function(val, type) {
 		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]' : '');
 	}

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/6f4fb5c4/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
index 3e51deb..cc2f335 100644
--- a/eagle-webservice/src/main/webapp/app/public/js/components/nvd3.js
+++ b/eagle-webservice/src/main/webapp/app/public/js/components/nvd3.js
@@ -1,300 +1,374 @@
-/*
- * 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              =
-	// ============================================
-	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;
-		});
-	};
-
-	// ============================================
-	// =                    UI                    =
-	// ============================================
-	// Resize with refresh
-	function chartResize() {
-		$.each(nvd3.charts, function(i, chart) {
-			if(chart) chart.update();
-		});
-	}
-	$(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
- */
-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.
-		},
-		controller: function($scope, $element, $attrs, $timeout) {
-			var _config, _chartType;
-			var _chart;
-			var _chartCntr;
-
-			// 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[0].y; });
-						break;
-					default :
-						throw "Type not defined: " + _chartType;
-				}
-
-				// 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"];
-					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());
-							}
-							_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;
-				}
-
-				updateData();
-			}
-
-			// Update chart data
-			function updateData() {
-				var _min, _max;
-
-				// 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 === undefined || unit.y < _min) _min = unit.y;
-							if(_max === undefined || unit.y > _max) _max = unit.y;
-						});
-					});
-					if(_min === 0 && _max === 0) {
-						_chart.forceY([0, 10]);
-					} 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!
-			}
-
-			// ================================================================
-			// =                           Watching                           =
-			// ================================================================
-			// Ignore initial checking
-			$timeout(function() {
-				if ($scope.watching !== "false") {
-					$scope.$watch("nvd3", function(newValue, oldValue) {
-						//noinspection JSValidateTypes
-						if (newValue === oldValue) return;
-
-						updateData();
-					}, true);
-
-					// All watching mode
-					if ($scope.watching === "true") {
-						$scope.$watch("[chart, config]", function(newValue, oldValue) {
-							//noinspection JSValidateTypes
-							if (newValue === oldValue) return;
-
-							initChart();
-						}, true);
-					}
-				}
-			});
-
-			// ================================================================
-			// =                           Start Up                           =
-			// ================================================================
-			initChart();
-
-			// ================================================================
-			// =                           Clean Up                           =
-			// ================================================================
-			$scope.$on('$destroy', function() {
-				destroy();
-			});
-		},
-		template :
-		'<div>' +
-			'<h3>{{title || config.title}}</h3>' +
-			//'<svg style="min-height: 50px;"></svg>' +
-		'</div>',
-		replace: true
-	};
+/*
+ * 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.update();
+		});
+	}
+	$(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"];
+					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());
+							}
+							_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;
+				}
+
+				updateData();
+			}
+
+			// Update chart data
+			function updateData() {
+				var _min, _max;
+
+				// 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 === undefined || unit.y < _min) _min = unit.y;
+							if(_max === undefined || 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!
+			}
+
+			// ================================================================
+			// =                           Watching                           =
+			// ================================================================
+			// Ignore initial checking
+			$timeout(function() {
+				if ($scope.watching !== "false") {
+					$scope.$watch("nvd3", function(newValue, oldValue) {
+						//noinspection JSValidateTypes
+						if (newValue === oldValue) return;
+
+						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 || config.title}}</h3>' +
+		'</div>',
+		replace: true
+	};
 });
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/6f4fb5c4/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
index 032271a..675c984 100644
--- a/eagle-webservice/src/main/webapp/app/public/js/components/tabs.js
+++ b/eagle-webservice/src/main/webapp/app/public/js/components/tabs.js
@@ -1,108 +1,223 @@
-/*
- * 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 : true,
-		scope : {
-			title: "@",
-			icon: "@",
-			selected: "@?selected",
-
-			inner: "=?inner"
-		},
-		controller: function($scope, $element, $attrs, $timeout) {
-			var _selected = null;
-
-			var panes = $scope.panes = [];
-
-			$scope.getList = function() {
-				if($scope.inner) {
-					return $scope.panes;
-				} else {
-					return $scope.panes.slice().reverse();
-				}
-			};
-
-			$scope.select = function(pane, updateBind) {
-				angular.forEach(panes, function(pane) {
-					pane.selected = false;
-				});
-				pane.selected = true;
-				_selected = pane;
-
-				if(updateBind !== false && $attrs.selected) {
-					$scope.$parent[$attrs.selected] = _selected.title;
-				}
-			};
-
-			this.addPane = function(pane) {
-				if (panes.length === 0 || ($attrs.selected && $scope.$parent[$attrs.selected] === pane.title)) {
-					$scope.select(pane, false);
-				}
-				panes.push(pane);
-			};
-
-			// Listen tab selected change
-			if($attrs.selected) {
-				$scope.$parent.$watch($attrs.selected, function(value) {
-					$.each(panes, function(i, pane) {
-						if(value === pane.title) {
-							$scope.select(pane, false);
-							return false;
-						}
-					});
-				});
-			}
-		},
-		template : '<div ng-class="inner ? \'\' : \'nav-tabs-custom\'">' +
-			'<ul class="nav nav-tabs ui-sortable-handle" ng-class="inner ? \'\' : \'pull-right\'">' +
-				'<li ng-repeat="pane in getList()" ng-class="{active:pane.selected}">' +
-					'<a href="" ng-click="select(pane)">{{pane.title}}</a>' +
-				'</li>' +
-				'<li class="pull-left header"><i class="fa fa-{{icon}}"></i> {{title}}</li>' +
-			'</ul>' +
-			'<div class="tab-content" ng-transclude></div>' +
-		'</div>',
-		replace : true
-	};
-}).directive('pane', function() {
-	return {
-		require : '^tabs',
-		restrict : 'AE',
-		transclude : true,
-		scope : {
-			title : '@'
-		},
-		controller: function($scope, $element, $timeout) {
-			// Initialization
-			var $innerScope = angular.element($element).scope();
-			$innerScope.app = app;
-			$innerScope.common = common;
-			$innerScope._parent = $scope.$parent.$parent.$parent;
-		},
-		link : function(scope, element, attrs, tabsController) {
-			tabsController.addPane(scope);
-		},
-		template : '<div class="tab-pane" ng-class="{active: selected}" ng-transclude="parent">' + '</div>',
-		replace : true
-	};
+/*
+ * 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",
+
+			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.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");
+				}
+
+				$scope.activePane = $scope.selectedPane || pane;
+				$scope.selectedPane = pane;
+
+				if(transTimer) $timeout.cancel(transTimer);
+				transTimer = $timeout(function() {
+					$scope.activePane = $scope.selectedPane;
+				}, transDuration);
+			};
+
+			// =================== 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.selectedPane === 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.paneList[0];
+					$scope.activePane = $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="menuList && menuList.length">' +
+					'<div ng-repeat="menu in menuList 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)">' +
+										'<span class="fa fa-{{item.icon}}"></span> {{item.title}}' +
+									'</a>' +
+								'</li>' +
+							'</ul>' +
+						'</div>' +
+					'</div>' +
+				'</div>' +
+
+				'<ul 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'
+		},
+		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/6f4fb5c4/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
index 1b4f602..dbdb704 100644
--- a/eagle-webservice/src/main/webapp/app/public/js/ctrl/authController.js
+++ b/eagle-webservice/src/main/webapp/app/public/js/ctrl/authController.js
@@ -32,6 +32,16 @@
 		$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 () {
@@ -47,6 +57,18 @@
 
 				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!");});

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/6f4fb5c4/eagle-webservice/src/main/webapp/app/public/js/srv/authorizationSrv.js
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/app/public/js/srv/authorizationSrv.js b/eagle-webservice/src/main/webapp/app/public/js/srv/authorizationSrv.js
index 337b567..dad9f6d 100644
--- a/eagle-webservice/src/main/webapp/app/public/js/srv/authorizationSrv.js
+++ b/eagle-webservice/src/main/webapp/app/public/js/srv/authorizationSrv.js
@@ -126,6 +126,18 @@
 			return _deferred.promise;
 		};
 
+		// Call web service to keep session
+		setInterval(function() {
+			if(!content.isLogin) return;
+
+			$http.get(app.getURL('userProfile')).then(null, function (response) {
+				if(response.status === 403) {
+					console.log("[Session] Out of date...", response);
+					content.needLogin();
+				}
+			});
+		}, 1000 * 60 * 5);
+
 		return content;
 	});
 })();
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/6f4fb5c4/eagle-webservice/src/main/webapp/grunt.json
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/grunt.json b/eagle-webservice/src/main/webapp/grunt.json
index c921862..15f58d9 100644
--- a/eagle-webservice/src/main/webapp/grunt.json
+++ b/eagle-webservice/src/main/webapp/grunt.json
@@ -16,7 +16,7 @@
 				"node_modules/angular-resource/angular-resource.min.js",
 				"node_modules/angular-route/angular-route.min.js",
 				"node_modules/angular-animate/angular-animate.min.js",
-				"node_modules/angular-ui-bootstrap/ui-bootstrap-tpls.min.js",
+				"node_modules/angular-ui-bootstrap/dist/ui-bootstrap-tpls.js",
 				"node_modules/angular-ui-router/release/angular-ui-router.min.js",
 				"node_modules/d3/d3.min.js",
 				"node_modules/zombiej-nvd3/build/nv.d3.min.js",

http://git-wip-us.apache.org/repos/asf/incubator-eagle/blob/6f4fb5c4/eagle-webservice/src/main/webapp/package.json
----------------------------------------------------------------------
diff --git a/eagle-webservice/src/main/webapp/package.json b/eagle-webservice/src/main/webapp/package.json
index 1e6f8d4..a85bf4a 100644
--- a/eagle-webservice/src/main/webapp/package.json
+++ b/eagle-webservice/src/main/webapp/package.json
@@ -8,21 +8,21 @@
 	},
 	"license": "Apache-2.0",
 	"dependencies": {
-		"jquery"				: "1.11.3",
-		"bootstrap"				: "3.3.5",
-		"moment"				: "2.10.6",
-		"moment-timezone"		: "0.4.1",
-		"font-awesome"			: "4.4.0",
+		"jquery"				: "1.12.0",
+		"bootstrap"				: "3.3.6",
+		"moment"				: "2.11.2",
+		"moment-timezone"		: "0.5.0",
+		"font-awesome"			: "4.5.0",
 		"admin-lte"				: "2.3.2",
-		"angular"				: "1.4.7",
-		"angular-resource"		: "1.4.7",
-		"angular-route"			: "1.4.7",
-		"angular-cookies"		: "1.4.7",
-		"angular-animate"		: "1.4.7",
-		"angular-ui-bootstrap"	: "0.14.3",
-		"angular-ui-router"		: "~0.2.17",
-		"d3"					: "3.5.14",
-		"zombiej-nvd3"			: "1.8.1-1",
+		"angular"				: "1.5.0",
+		"angular-resource"		: "1.5.0",
+		"angular-route"			: "1.5.0",
+		"angular-cookies"		: "1.5.0",
+		"angular-animate"		: "1.5.0",
+		"angular-ui-bootstrap"	: "1.1.2",
+		"angular-ui-router"		: "~0.2.18",
+		"d3"					: "3.5.16",
+		"zombiej-nvd3"			: "1.8.2-1",
 		"jquery-slimscroll"		:"1.3.6",
 		"zombiej-bootstrap-components"		: "1.1.1"
 	},


Mime
View raw message