[EAGLE-1046] Eagle supports policies import to a new site from a policy prototype

https://issues.apache.org/jira/browse/EAGLE-1046

add prototype management page
support policy create with proto

Author: zombieJ <smith3816@gmail.com>
Author: Zhao, Qingwen <qingwzhao@apache.org>

Closes #963 from zombieJ/EAGLE-1046.
diff --git a/eagle-core/eagle-metadata/eagle-metadata-base/src/main/java/org/apache/eagle/metadata/resource/PolicyResource.java b/eagle-core/eagle-metadata/eagle-metadata-base/src/main/java/org/apache/eagle/metadata/resource/PolicyResource.java
index 13041cf..d09da4b 100644
--- a/eagle-core/eagle-metadata/eagle-metadata-base/src/main/java/org/apache/eagle/metadata/resource/PolicyResource.java
+++ b/eagle-core/eagle-metadata/eagle-metadata-base/src/main/java/org/apache/eagle/metadata/resource/PolicyResource.java
@@ -65,24 +65,25 @@
     @Consumes(MediaType.APPLICATION_JSON)
     @Produces(MediaType.APPLICATION_JSON)
     public RESTResponse<PolicyEntity> saveAsPolicyProto(PolicyEntity policyEntity,
-                                                        @QueryParam("needPolicyCreated") boolean needPolicyCreated) {
+                                                        @QueryParam("needPolicyProtoCreated") boolean needPolicyProtoCreated) {
         return RESTResponse.async(() -> {
             Preconditions.checkNotNull(policyEntity, "entity should not be null");
-            Preconditions.checkNotNull(policyEntity, "policy definition should not be null");
+            Preconditions.checkNotNull(policyEntity.getDefinition(), "policy definition should not be null");
             Preconditions.checkNotNull(policyEntity.getAlertPublishmentIds(), "alert publisher list should not be null");
 
             PolicyDefinition policyDefinition = policyEntity.getDefinition();
-            if (needPolicyCreated) {
-                OpResult result = metadataResource.addPolicy(policyDefinition);
-                if (result.code != 200) {
-                    throw new IllegalArgumentException(result.message);
-                }
-                result = metadataResource.addPublishmentsToPolicy(policyDefinition.getName(), policyEntity.getAlertPublishmentIds());
-                if (result.code != 200) {
-                    throw new IllegalArgumentException(result.message);
-                }
+            OpResult result = metadataResource.addPolicy(policyDefinition);
+            if (result.code != 200) {
+                throw new IllegalArgumentException(result.message);
             }
-            return importPolicyProto(policyEntity);
+            result = metadataResource.addPublishmentsToPolicy(policyDefinition.getName(), policyEntity.getAlertPublishmentIds());
+            if (result.code != 200) {
+                throw new IllegalArgumentException(result.message);
+            }
+            if (needPolicyProtoCreated) {
+                importPolicyProto(policyEntity);
+            }
+            return policyEntity;
         }).get();
     }
 
diff --git a/eagle-external/eagle-docker/resource/serf/bin/start-serf-agent.sh b/eagle-external/eagle-docker/resource/serf/bin/start-serf-agent.sh
index dbb8df0..035886a 100755
--- a/eagle-external/eagle-docker/resource/serf/bin/start-serf-agent.sh
+++ b/eagle-external/eagle-docker/resource/serf/bin/start-serf-agent.sh
@@ -1,5 +1,20 @@
 #!/bin/bash
 
+# 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.
+
 SERF_HOME=/usr/local/serf
 SERF_BIN=$SERF_HOME/bin/serf
 SERF_CONFIG_DIR=$SERF_HOME/etc
diff --git a/eagle-server/src/main/webapp/app/dev/partials/alert/policyDetail.html b/eagle-server/src/main/webapp/app/dev/partials/alert/policyDetail.html
index d5b3352..6ebb4f1 100644
--- a/eagle-server/src/main/webapp/app/dev/partials/alert/policyDetail.html
+++ b/eagle-server/src/main/webapp/app/dev/partials/alert/policyDetail.html
@@ -49,6 +49,12 @@
 			</tbody>
 		</table>
 	</div>
+	<div class="box-footer text-right">
+		<button class="btn btn-primary" ng-click="makePrototype()">
+			<span class="fa fa-file-code-o"></span>
+			Make as Prototype
+		</button>
+	</div>
 </div>
 
 
diff --git a/eagle-server/src/main/webapp/app/dev/partials/alert/policyEdit/advancedMode.html b/eagle-server/src/main/webapp/app/dev/partials/alert/policyEdit/advancedMode.html
index 1da0d3d..354d8d0 100644
--- a/eagle-server/src/main/webapp/app/dev/partials/alert/policyEdit/advancedMode.html
+++ b/eagle-server/src/main/webapp/app/dev/partials/alert/policyEdit/advancedMode.html
@@ -220,7 +220,7 @@
 				</div>
 
 				<label>
-					Publish Alerts
+					Alert Publishers *
 				</label>
 
 				<ul class="sm-padding">
@@ -252,6 +252,13 @@
 							<label>Schedule Parallelism *</label>
 							<input type="text" class="form-control" ng-model="policy.parallelismHint" ng-disabled="policyLock" />
 						</div>
+						<!--
+						<div class="checkbox">
+							<label>
+								<input type="checkbox" ng-checked="savePrototype" ng-click="savePrototype = !savePrototype">
+								Make this Policy as Prototype
+							</label>
+						</div> -->
 					</div>
 				</div>
 			</div>
diff --git a/eagle-server/src/main/webapp/app/dev/partials/alert/policyPrototypes.html b/eagle-server/src/main/webapp/app/dev/partials/alert/policyPrototypes.html
new file mode 100644
index 0000000..9f22ce4
--- /dev/null
+++ b/eagle-server/src/main/webapp/app/dev/partials/alert/policyPrototypes.html
@@ -0,0 +1,67 @@
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one
+  or more contributor license agreements.  See the NOTICE file
+  distributed with this work for additional information
+  regarding copyright ownership.  The ASF licenses this file
+  to you under the Apache License, Version 2.0 (the
+  "License"); you may not use this file except in compliance
+  with the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+  -->
+
+<div class="box box-primary">
+	<div class="box-header with-border">
+		<span class="fa fa-code-fork"></span>
+		<h3 class="box-title">
+			Prototype List
+		</h3>
+	</div>
+	<div class="box-body">
+		<div sort-table="prototypeList">
+			<table class="table table-bordered table-hover">
+				<thead>
+					<tr>
+						<th>
+							<input type="checkbox" ng-checked="getCheckedList().length === prototypeList.length && prototypeList.length !== 0" ng-click="doCheckAll()" />
+						</th>
+						<th>Name</th>
+						<th>Definition</th>
+						<th>Publishers</th>
+						<th width="110">Operation</th>
+					</tr>
+				</thead>
+				<tbody>
+					<tr>
+						<td>
+							<input type="checkbox" ng-checked="checkedPrototypes[item.name]" ng-click="checkedPrototypes[item.name] = !checkedPrototypes[item.name]" />
+						</td>
+						<td>{{item.name}}</td>
+						<td><pre>{{item.definition.definition.value}}</pre></td>
+						<td>
+							<ul class="no-margin">
+								<li ng-repeat="publisher in item.alertPublishmentIds track by $index">
+									{{publisher}}
+								</li>
+							</ul>
+						</td>
+						<td class="text-center">
+							<button class="btn btn-xs btn-primary" ng-click="createPolicy([item])">Export</button>
+							<button class="btn btn-xs btn-danger" ng-click="deletePrototype(item)">Delete</button>
+						</td>
+					</tr>
+				</tbody>
+			</table>
+		</div>
+
+	</div>
+	<div class="box-footer text-right">
+		<button class="btn btn-primary" ng-click="groupCreate()" ng-disabled="getCheckedList().length === 0">Export as Policies</button>
+	</div>
+</div>
diff --git a/eagle-server/src/main/webapp/app/dev/public/js/app.js b/eagle-server/src/main/webapp/app/dev/public/js/app.js
index 9948ef5..f136241 100644
--- a/eagle-server/src/main/webapp/app/dev/public/js/app.js
+++ b/eagle-server/src/main/webapp/app/dev/public/js/app.js
@@ -43,7 +43,7 @@
 		// ======================================================================================
 		// =                                   Router config                                    =
 		// ======================================================================================
-		var defaultRouterStates = ['site', 'alertList', 'policyList', 'streamList', 'policyCreate', 'policyEdit', 'alertDetail', 'policyDetail'];
+		var defaultRouterStates = ['site', 'alertList', 'policyList', 'streamList', 'policyPrototypes', 'policyCreate', 'policyEdit', 'alertDetail', 'policyDetail'];
 
 		function routeResolve(config) {
 			var resolve = {};
@@ -201,6 +201,12 @@
 					resolve: routeResolve()
 				})
 
+				.state('policyPrototypes', {
+					url: "/site/:siteId/policy/prototypes",
+					templateUrl: "partials/alert/policyPrototypes.html?_=" + window._TRS(),
+					controller: "policyPrototypesCtrl",
+					resolve: routeResolve()
+				})
 				.state('policyCreate', {
 					url: "/site/:siteId/policy/create",
 					templateUrl: "partials/alert/policyEdit/main.html?_=" + window._TRS(),
diff --git a/eagle-server/src/main/webapp/app/dev/public/js/components/sortTable.js b/eagle-server/src/main/webapp/app/dev/public/js/components/sortTable.js
index 7a312c9..8e74824 100644
--- a/eagle-server/src/main/webapp/app/dev/public/js/components/sortTable.js
+++ b/eagle-server/src/main/webapp/app/dev/public/js/components/sortTable.js
@@ -193,6 +193,12 @@
 						).appendTo($toolContainer);
 						$compile($pageSize)($scope);
 
+						// Non-Sort Column
+						$element.find("table thead th:not([sortpath])").each(function () {
+							var $this = $(this);
+							$compile($this)($scope);
+						});
+
 						// Sort Column
 						$element.find("table [sortpath]").each(function () {
 							var $this = $(this);
diff --git a/eagle-server/src/main/webapp/app/dev/public/js/ctrls/alertCtrl.js b/eagle-server/src/main/webapp/app/dev/public/js/ctrls/alertCtrl.js
index 743b4b6..2bf62f5 100644
--- a/eagle-server/src/main/webapp/app/dev/public/js/ctrls/alertCtrl.js
+++ b/eagle-server/src/main/webapp/app/dev/public/js/ctrls/alertCtrl.js
@@ -186,7 +186,7 @@
 		};
 	});
 
-	eagleControllers.controller('policyDetailCtrl', function ($scope, $wrapState, $interval, PageConfig, Time, Entity, CompatibleEntity, Policy) {
+	eagleControllers.controller('policyDetailCtrl', function ($scope, $wrapState, $interval, UI, PageConfig, Time, Entity, CompatibleEntity, Policy) {
 		PageConfig.title = "Policy";
 		PageConfig.subTitle = "Detail";
 		PageConfig.navPath = [
@@ -283,6 +283,33 @@
 			Policy.stop($scope.policy).then(updatePolicy);
 		};
 
+		$scope.makePrototype = function () {
+			$.dialog({
+				title: 'Confirm',
+				content: 'Do you want to make this policy as Prototype?',
+				confirm: true,
+			}, function (ret) {
+				if (!ret) return;
+
+				Entity.post('policyProto/create/' + $scope.policy.name, '')._then(function (res) {
+					var validate = res.data;
+					console.log(validate);
+					if(!validate.success) {
+						$.dialog({
+							title: "OPS",
+							content: validate.message
+						});
+						return;
+					} else {
+						$.dialog({
+							title: "Success",
+							content: "Please go to the prototype page to check updates"
+						});
+					}
+				});
+			});
+		};
+
 		var refreshInterval = $interval($scope.alertList._refresh, 1000 * 60);
 		$scope.$on('$destroy', function() {
 			$interval.cancel(refreshInterval);
diff --git a/eagle-server/src/main/webapp/app/dev/public/js/ctrls/alertEditCtrl.js b/eagle-server/src/main/webapp/app/dev/public/js/ctrls/alertEditCtrl.js
index 9308554..e4623b2 100644
--- a/eagle-server/src/main/webapp/app/dev/public/js/ctrls/alertEditCtrl.js
+++ b/eagle-server/src/main/webapp/app/dev/public/js/ctrls/alertEditCtrl.js
@@ -31,6 +31,100 @@
 		policyEditController.apply(this, newArgs);
 	}
 
+	eagleControllers.controller('policyPrototypesCtrl', function ($scope, $wrapState, Site, PageConfig, Entity, UI) {
+		PageConfig.title = "Policy Prototypes";
+
+		$scope.checkedPrototypes = {};
+
+		function refreshPrototypeList() {
+			$scope.prototypeList = Entity.query('policyProto');
+		}
+
+		$scope.deletePrototype = function (prototype) {
+			UI.deleteConfirm(prototype.name)(function (entity, closeFunc) {
+				Entity.delete("policyProto/" + prototype.uuid)._promise.then(function (res) {
+					var data = res.data;
+
+					if (data.success !== true) {
+						$.dialog({
+							title: 'OPS',
+							content: data.message
+						});
+					}
+				}).finally(function () {
+					closeFunc();
+					refreshPrototypeList();
+				});
+			});
+		};
+
+		$scope.getCheckedList = function () {
+			return $.map($scope.prototypeList, function (proto) {
+				return $scope.checkedPrototypes[proto.name] ? proto : null;
+			});
+		};
+
+		$scope.doCheckAll = function () {
+			if ($scope.getCheckedList().length === $scope.prototypeList.length) {
+				$scope.checkedPrototypes = {};
+			} else {
+				$.each($scope.prototypeList, function (i, proto) {
+					$scope.checkedPrototypes[proto.name] = true;
+				});
+			}
+		};
+
+		$scope.groupCreate = function () {
+			var list = $scope.getCheckedList();
+			$scope.createPolicy(list);
+		};
+
+		$scope.createPolicy = function (protoList) {
+			if (protoList.length === 0) {
+				$.dialog({
+					title: 'OPS',
+					content: 'Please select at least one prototype.',
+				});
+				return;
+			}
+
+			UI.createConfirm('Policy', {}, [
+				{field: "siteId", name: "Site Id", type: 'select', valueList: $.map(Site.list, function (site) {
+					return site.siteId;
+				})}
+			])(function (entity, closeFunc, unlock) {
+				var nameList = $.map(protoList, function (proto) {
+					return proto.name;
+				});
+				Entity.create("policyProto/exportByName/" + entity.siteId, nameList)._then(function (res) {
+					var data = res.data;
+
+					if(!data.success) {
+						$.dialog({
+							title: 'OPS',
+							content: data.message
+						});
+						unlock();
+						return;
+					} else {
+						$.dialog({
+							title: 'Success',
+							content: 'Click confirm to go to the policy page.',
+							confirm: true,
+						}, function (ret) {
+							if (!ret) return;
+
+							$wrapState.go('policyList', {siteId: entity.siteId });
+						});
+					}
+					closeFunc();
+				}, unlock);
+			});
+		};
+
+		refreshPrototypeList();
+	});
+
 	eagleControllers.controller('policyCreateCtrl', function ($scope, $q, $wrapState, $timeout, PageConfig, Entity, Policy) {
 		PageConfig.title = "Define Policy";
 		connectPolicyEditController({}, arguments);
@@ -88,6 +182,7 @@
 		$scope.streamGroups = {};
 		$scope.newPolicy = !$scope.policy.name;
 		$scope.autoPolicyDescription = $scope.newPolicy && !$scope.policy.description;
+		$scope.savePrototype = false;
 
 		PageConfig.navPath = [
 			{title: "Policy List", path: "/policies"},
@@ -445,28 +540,31 @@
 				$q.all(publisherPromiseList).then(function () {
 					console.log("Create publishers success...");
 
-					// Create policy
-					Entity.create("metadata/policies", $scope.policy)._then(function () {
+					var publisherNameList = $.map($scope.policyPublisherList, function (publisher) {
+						return publisher.name;
+					});
+
+					var policyPromise;
+
+					if ($scope.savePrototype) {
+						policyPromise = Entity.create('policyProto/create?needPolicyProtoCreated=true', {
+							definition: $scope.policy,
+							alertPublishmentIds: publisherNameList
+						});
+					} else {
+						policyPromise = Entity.create('policyProto/create?needPolicyProtoCreated=false', {
+							definition: $scope.policy,
+							alertPublishmentIds: publisherNameList
+						});
+					}
+
+					policyPromise._then(function () {
 						console.log("Create policy success...");
-						// Link with publisher
-						Entity.post("metadata/policies/" + $scope.policy.name + "/publishments/", $.map($scope.policyPublisherList, function (publisher) {
-							return publisher.name;
-						}))._then(function () {
-							// Link Success
-							$.dialog({
-								title: "Done",
-								content: "Close dialog to go to the policy detail page."
-							}, function () {
-								$wrapState.go("policyDetail", {name: $scope.policy.name, siteId: $scope.policy.siteId});
-							});
-						}, function (res) {
-							// Link Failed
-							$.dialog({
-								title: "OPS",
-								content: "Link publishers failed:" + res.data.message
-							});
-						}).finally(function () {
-							$scope.policyLock = false;
+						$.dialog({
+							title: "Done",
+							content: "Close dialog to go to the policy detail page."
+						}, function () {
+							$wrapState.go("policyDetail", {name: $scope.policy.name, siteId: $scope.policy.siteId});
 						});
 					}, function (res) {
 						var errormsg = "";
diff --git a/eagle-server/src/main/webapp/app/dev/public/js/services/authSrv.js b/eagle-server/src/main/webapp/app/dev/public/js/services/authSrv.js
index 0fa2d4a..1b11902 100644
--- a/eagle-server/src/main/webapp/app/dev/public/js/services/authSrv.js
+++ b/eagle-server/src/main/webapp/app/dev/public/js/services/authSrv.js
@@ -21,8 +21,10 @@
 
 	var serviceModule = angular.module('eagle.service');
 
-	serviceModule.service('Auth', function ($http) {
+	serviceModule.service('Auth', function ($http, $q) {
 		//$http.defaults.withCredentials = true;
+		var _promise;
+
 		var Auth = {
 			isLogin: false,
 			user: {},
@@ -48,7 +50,7 @@
 		};
 
 		Auth.sync = function (hash) {
-			return $http.get(_host + "/rest/auth/principal", {
+			_promise = $http.get(_host + "/rest/auth/principal", {
 				headers: {
 					'Authorization': "Basic " + hash
 				}
@@ -64,10 +66,20 @@
 			}, function () {
 				return false;
 			});
+
+			return _promise;
+		};
+
+		Auth.getPromise = function () {
+			return _promise;
 		};
 
 		if (localStorage && localStorage.getItem('auth')) {
 			Auth.sync(localStorage.getItem('auth'));
+		} else {
+			var deferred = $q.defer();
+			deferred.resolve();
+			_promise = deferred.promise;
 		}
 
 		Object.defineProperties(Auth, {
diff --git a/eagle-server/src/main/webapp/app/dev/public/js/services/entitySrv.js b/eagle-server/src/main/webapp/app/dev/public/js/services/entitySrv.js
index 2978a35..cdacbb5 100644
--- a/eagle-server/src/main/webapp/app/dev/public/js/services/entitySrv.js
+++ b/eagle-server/src/main/webapp/app/dev/public/js/services/entitySrv.js
@@ -34,10 +34,16 @@
 			list._promise = promise.then(function (res) {
 				var data = res.data;
 				list.splice(0);
-				Array.prototype.push.apply(list, data.data);
+				if (typeof data.data === 'object') {
+					Array.prototype.push.apply(list, data.data);
+				} else {
+					list.push(data.data);
+				}
 				list._done = true;
 
 				return res;
+			}, function (res) {
+				return res;
 			});
 			return withThen(list);
 		}
diff --git a/eagle-server/src/main/webapp/app/dev/public/js/services/pageSrv.js b/eagle-server/src/main/webapp/app/dev/public/js/services/pageSrv.js
index 5ce488d..582134c 100644
--- a/eagle-server/src/main/webapp/app/dev/public/js/services/pageSrv.js
+++ b/eagle-server/src/main/webapp/app/dev/public/js/services/pageSrv.js
@@ -103,11 +103,14 @@
 				{name: "Streams", path: "#/site/" + site.siteId + "/streams"},
 			];
 
-			if (Auth.isAdmin) {
-				alertPortal.push(
-					{name: "Define Policy", path: "#/site/" + site.siteId + "/policy/create"}
-				);
-			}
+			Auth.getPromise().then(function () {
+				if (Auth.isAdmin) {
+					alertPortal.push(
+						{name: "Prototypes", path: "#/site/" + site.siteId + "/policy/prototypes"},
+						{name: "Define Policy", path: "#/site/" + site.siteId + "/policy/create"}
+					);
+				}
+			});
 
 			return [
 				{name: site.siteName || site.siteId + " Home", icon: "home", path: "#/site/" + site.siteId},