فهرست منبع

:sparkles: 添加新特性。 add turbine 、 sba customer ui

冷冷 6 سال پیش
والد
کامیت
955fd80929
45فایلهای تغییر یافته به همراه2743 افزوده شده و 77 حذف شده
  1. 1 1
      pigx-auth/src/main/java/com/pig4cloud/pigx/auth/endpoint/PigxTokenEndpoint.java
  2. 1 1
      pigx-common/pigx-common-core/src/main/java/com/pig4cloud/pigx/common/core/constant/SecurityConstants.java
  3. 1 1
      pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/service/impl/MobileServiceImpl.java
  4. 10 5
      pigx-visual/pigx-monitor/pom.xml
  5. 6 2
      pigx-visual/pigx-monitor/src/main/java/com/pig4cloud/pigx/monitor/PigxMonitorApplication.java
  6. 0 67
      pigx-visual/pigx-monitor/src/main/java/com/pig4cloud/pigx/monitor/config/WebSecurityConfigurer.java
  7. 8 0
      pigx-visual/pigx-monitor/src/main/resources/bootstrap.yml
  8. 200 0
      pigx-visual/pigx-monitor/src/main/resources/static/hystrix/components/hystrixCommand/hystrixCommand.css
  9. 542 0
      pigx-visual/pigx-monitor/src/main/resources/static/hystrix/components/hystrixCommand/hystrixCommand.js
  10. BIN
      pigx-visual/pigx-monitor/src/main/resources/static/hystrix/components/hystrixCommand/magnifying-glass-icon-20.png
  11. BIN
      pigx-visual/pigx-monitor/src/main/resources/static/hystrix/components/hystrixCommand/magnifying-glass-icon.png
  12. 77 0
      pigx-visual/pigx-monitor/src/main/resources/static/hystrix/components/hystrixCommand/templates/hystrixCircuit.html
  13. 40 0
      pigx-visual/pigx-monitor/src/main/resources/static/hystrix/components/hystrixCommand/templates/hystrixCircuitContainer.html
  14. 6 0
      pigx-visual/pigx-monitor/src/main/resources/static/hystrix/components/hystrixCommand/templates/hystrixCircuitProperties.html
  15. 141 0
      pigx-visual/pigx-monitor/src/main/resources/static/hystrix/components/hystrixThreadPool/hystrixThreadPool.css
  16. 343 0
      pigx-visual/pigx-monitor/src/main/resources/static/hystrix/components/hystrixThreadPool/hystrixThreadPool.js
  17. 33 0
      pigx-visual/pigx-monitor/src/main/resources/static/hystrix/components/hystrixThreadPool/templates/hystrixThreadPool.html
  18. 34 0
      pigx-visual/pigx-monitor/src/main/resources/static/hystrix/components/hystrixThreadPool/templates/hystrixThreadPoolContainer.html
  19. 71 0
      pigx-visual/pigx-monitor/src/main/resources/static/hystrix/css/global.css
  20. 105 0
      pigx-visual/pigx-monitor/src/main/resources/static/hystrix/css/monitor.css
  21. 102 0
      pigx-visual/pigx-monitor/src/main/resources/static/hystrix/css/resets.css
  22. 21 0
      pigx-visual/pigx-monitor/src/main/resources/static/hystrix/css/simplegrid/1236_grid.css
  23. 33 0
      pigx-visual/pigx-monitor/src/main/resources/static/hystrix/css/simplegrid/720_grid.css
  24. 24 0
      pigx-visual/pigx-monitor/src/main/resources/static/hystrix/css/simplegrid/986_grid.css
  25. 19 0
      pigx-visual/pigx-monitor/src/main/resources/static/hystrix/css/simplegrid/LICENSE.txt
  26. 1 0
      pigx-visual/pigx-monitor/src/main/resources/static/hystrix/css/simplegrid/README.txt
  27. 27 0
      pigx-visual/pigx-monitor/src/main/resources/static/hystrix/css/simplegrid/percentage_grid.css
  28. BIN
      pigx-visual/pigx-monitor/src/main/resources/static/hystrix/images/hystrix-logo-tagline-tiny.png
  29. BIN
      pigx-visual/pigx-monitor/src/main/resources/static/hystrix/images/hystrix-logo.png
  30. 12 0
      pigx-visual/pigx-monitor/src/main/resources/static/hystrix/js/jquery.tinysort.min.js
  31. 43 0
      pigx-visual/pigx-monitor/src/main/resources/static/hystrix/js/tmpl.js
  32. 58 0
      pigx-visual/pigx-monitor/src/main/resources/templates/hystrix/index.ftl
  33. 202 0
      pigx-visual/pigx-monitor/src/main/resources/templates/hystrix/monitor.ftl
  34. 394 0
      pigx-visual/pigx-monitor/src/main/resources/ui/assets/css/sba-core.css
  35. BIN
      pigx-visual/pigx-monitor/src/main/resources/ui/assets/img/favicon-danger.png
  36. BIN
      pigx-visual/pigx-monitor/src/main/resources/ui/assets/img/favicon.png
  37. 9 0
      pigx-visual/pigx-monitor/src/main/resources/ui/assets/img/icon-spring-boot-admin.svg
  38. 7 0
      pigx-visual/pigx-monitor/src/main/resources/ui/assets/js/event-source-polyfill.js
  39. 1 0
      pigx-visual/pigx-monitor/src/main/resources/ui/assets/js/event-source-polyfill.js.map
  40. 2 0
      pigx-visual/pigx-monitor/src/main/resources/ui/assets/js/sba-core.js
  41. 1 0
      pigx-visual/pigx-monitor/src/main/resources/ui/assets/js/sba-core.js.map
  42. 17 0
      pigx-visual/pigx-monitor/src/main/resources/ui/assets/js/vendors.js
  43. 1 0
      pigx-visual/pigx-monitor/src/main/resources/ui/assets/js/vendors.js.map
  44. 70 0
      pigx-visual/pigx-monitor/src/main/resources/ui/index.html
  45. 80 0
      pigx-visual/pigx-monitor/src/main/resources/ui/login.html

+ 1 - 1
pigx-auth/src/main/java/com/pig4cloud/pigx/auth/endpoint/PigxTokenEndpoint.java

@@ -76,7 +76,7 @@ public class PigxTokenEndpoint {
 	 *
 	 * @param authHeader Authorization
 	 */
-	@DeleteMapping("/token")
+	@DeleteMapping("/logout")
 	public R<Boolean> logout(@RequestHeader(value = HttpHeaders.AUTHORIZATION, required = false) String authHeader) {
 		if (StringUtils.hasText(authHeader)) {
 			String tokenValue = authHeader.replace("Bearer", "").trim();

+ 1 - 1
pigx-common/pigx-common-core/src/main/java/com/pig4cloud/pigx/common/core/constant/SecurityConstants.java

@@ -31,7 +31,7 @@ public interface SecurityConstants {
 	/**
 	 * 验证码长度
 	 */
-	int CODE_SIZE = 4;
+	String CODE_SIZE = "4";
 	/**
 	 * 角色前缀
 	 */

+ 1 - 1
pigx-upms/pigx-upms-biz/src/main/java/com/pig4cloud/pigx/admin/service/impl/MobileServiceImpl.java

@@ -73,7 +73,7 @@ public class MobileServiceImpl implements MobileService {
 			return new R<>(Boolean.FALSE, "手机号未注册");
 		}
 
-		String code = RandomUtil.randomNumbers(4);
+		String code = RandomUtil.randomNumbers(Integer.parseInt(SecurityConstants.CODE_SIZE));
 		log.debug("手机号生成验证码成功:{},{}", mobile, code);
 		redisTemplate.opsForValue().set(CommonConstant.DEFAULT_CODE_KEY + mobile, code
 			, SecurityConstants.CODE_TIME, TimeUnit.SECONDS);

+ 10 - 5
pigx-visual/pigx-monitor/pom.xml

@@ -49,6 +49,16 @@
 			<artifactId>spring-boot-admin-starter-server</artifactId>
 			<version>${monitor.version}</version>
 		</dependency>
+		<!--hystrix-->
+		<dependency>
+			<groupId>org.springframework.cloud</groupId>
+			<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
+		</dependency>
+		<!--turbine-->
+		<dependency>
+			<groupId>org.springframework.cloud</groupId>
+			<artifactId>spring-cloud-starter-netflix-turbine</artifactId>
+		</dependency>
 		<!--Redis-->
 		<dependency>
 			<groupId>org.springframework.boot</groupId>
@@ -76,11 +86,6 @@
 			<groupId>org.springframework.boot</groupId>
 			<artifactId>spring-boot-starter-undertow</artifactId>
 		</dependency>
-		<!--security-->
-		<dependency>
-			<groupId>org.springframework.boot</groupId>
-			<artifactId>spring-boot-starter-security</artifactId>
-		</dependency>
 	</dependencies>
 
 	<build>

+ 6 - 2
pigx-visual/pigx-monitor/src/main/java/com/pig4cloud/pigx/monitor/PigxMonitorApplication.java

@@ -22,15 +22,19 @@ package com.pig4cloud.pigx.monitor;
 
 import de.codecentric.boot.admin.server.config.EnableAdminServer;
 import org.springframework.boot.SpringApplication;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.client.SpringCloudApplication;
+import org.springframework.cloud.netflix.hystrix.dashboard.EnableHystrixDashboard;
+import org.springframework.cloud.netflix.turbine.EnableTurbine;
 
 /**
  * @author lengleng
  * @date 2018年06月21日
  * 监控中心
  */
+@EnableTurbine
 @EnableAdminServer
-@SpringBootApplication
+@EnableHystrixDashboard
+@SpringCloudApplication
 public class PigxMonitorApplication {
 
 	public static void main(String[] args) {

+ 0 - 67
pigx-visual/pigx-monitor/src/main/java/com/pig4cloud/pigx/monitor/config/WebSecurityConfigurer.java

@@ -1,67 +0,0 @@
-/*
- *
- *      Copyright (c) 2018-2025, lengleng All rights reserved.
- *
- *  Redistribution and use in source and binary forms, with or without
- *  modification, are permitted provided that the following conditions are met:
- *
- * Redistributions of source code must retain the above copyright notice,
- *  this list of conditions and the following disclaimer.
- *  Redistributions in binary form must reproduce the above copyright
- *  notice, this list of conditions and the following disclaimer in the
- *  documentation and/or other materials provided with the distribution.
- *  Neither the name of the pig4cloud.com developer nor the names of its
- *  contributors may be used to endorse or promote products derived from
- *  this software without specific prior written permission.
- *  Author: lengleng (wangiegie@gmail.com)
- *
- */
-
-package com.pig4cloud.pigx.monitor.config;
-
-import de.codecentric.boot.admin.server.config.AdminServerProperties;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.security.config.annotation.web.builders.HttpSecurity;
-import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
-import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
-
-/**
- * WebSecurityConfigurer
- *
- * @author lishangbu
- * @date 2018/10/8
- */
-@Configuration
-public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
-	private final String adminContextPath;
-
-	public WebSecurityConfigurer(AdminServerProperties adminServerProperties) {
-		this.adminContextPath = adminServerProperties.getContextPath();
-	}
-
-	@Override
-	protected void configure(HttpSecurity http) throws Exception {
-		// @formatter:off
-		SavedRequestAwareAuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
-		successHandler.setTargetUrlParameter("redirectTo");
-		successHandler.setDefaultTargetUrl(adminContextPath + "/");
-
-		http
-			.headers().frameOptions().disable()
-			.and().authorizeRequests()
-			.antMatchers(adminContextPath + "/assets/**"
-				, adminContextPath + "/login"
-				, adminContextPath + "/actuator/**"
-			).permitAll()
-			.anyRequest().authenticated()
-			.and()
-			.formLogin().loginPage(adminContextPath + "/login")
-			.successHandler(successHandler).and()
-			.logout().logoutUrl(adminContextPath + "/logout")
-			.and()
-			.httpBasic().and()
-			.csrf()
-			.disable();
-		// @formatter:on
-	}
-}

+ 8 - 0
pigx-visual/pigx-monitor/src/main/resources/bootstrap.yml

@@ -13,6 +13,10 @@ spring:
       discovery:
         enabled: true
         service-id: pigx-config
+  boot:
+    admin:
+      ui:
+        resource-locations: 'classpath:/ui/,classpath:/static/'
 
 # 注册中心配置
 eureka:
@@ -21,3 +25,7 @@ eureka:
   client:
     service-url:
       defaultZone: http://pig:pig@pigx-eureka:1025/eureka/
+turbine:
+  app-config: pigx-upms,pigx-auth
+  cluster-name-expression: new String("default")
+  combine-host-port: true

+ 200 - 0
pigx-visual/pigx-monitor/src/main/resources/static/hystrix/components/hystrixCommand/hystrixCommand.css

@@ -0,0 +1,200 @@
+.dependencies .spacer {
+	width: 100%;
+	margin: 0 auto;
+	padding-top:4px;
+	clear:both;
+}
+
+
+.dependencies .last {
+	margin-right: 0px;
+}
+
+.dependencies span.loading {
+	display: block;
+	padding-top: 6%;
+	padding-bottom: 6%;
+	color: gray;
+	text-align: center;
+}
+
+.dependencies span.loading.failed {
+	color: red;
+}
+
+
+.dependencies div.monitor {
+	float: left;
+	margin-right:5px;
+	margin-top:5px;
+}
+
+.dependencies div.monitor p.name {
+	font-weight:bold;
+	font-size: 10pt;
+	text-align: right;	
+	padding-bottom: 5px;
+}
+
+.dependencies div.monitor_data {
+	margin: 0 auto;	
+}
+
+/* override the HREF when we have specified it as a tooltip to not act like a link */
+.dependencies div.monitor_data a.tooltip {
+	text-decoration: none;
+	cursor: default;	
+}
+
+.dependencies div.monitor_data div.counters {
+	text-align: right;	
+	padding-bottom: 10px;
+	font-size: 10pt;
+	clear: both;
+	
+}
+
+.dependencies div.monitor_data div.counters div.cell {
+	display: inline;
+	float: right;
+}
+
+.dependencies .borderRight {
+	border-right: 1px solid grey;
+	padding-right: 6px;
+	padding-left: 8px;
+}
+
+.dependencies div.cell .line {
+	display: block;
+}
+
+.dependencies div.monitor_data a,
+.dependencies span.rate_value {
+	font-weight:bold;	
+}
+
+
+.dependencies span.smaller {
+	font-size: 8pt;
+	color: grey;	
+}
+
+
+
+.dependencies div.tableRow {
+	width:100%;
+	white-space: nowrap;
+	font-size: 8pt;	
+	margin: 0 auto;
+	clear:both;
+	padding-left:26%;
+}
+
+.dependencies div.tableRow .cell {
+	float:left;
+}
+
+.dependencies div.tableRow .header {
+	width:18%;
+	text-align:right;
+	padding-right:2%;
+}
+
+.dependencies div.tableRow .data {
+	width:17%;
+	font-weight: bold;
+	text-align:right;
+}
+
+
+.dependencies div.monitor {
+	width: 245px; /* we want a fixed width instead of percentage as I want the boxes to be a set size and then fill in as many as can fit in each row ... this allows 3 columns on an iPad */
+	height: 155px;
+}
+
+.dependencies .success {
+	color: green;	
+}
+.dependencies .shortCircuited {
+	color: blue;	
+}
+.dependencies .timeout {
+	color: #FF9900;	 /* shade of orange */
+}
+.dependencies .failure {
+	color: red;	
+}
+
+.badRequest {
+	color: #00CC99;
+}
+
+.dependencies .rejected {
+	color: purple;
+}
+	
+.dependencies .exceptionsThrown {
+	color: brown;
+}
+
+.dependencies div.monitor_data a.rate {
+	color: black;	
+	font-size: 11pt;
+}
+
+.dependencies div.rate {
+	padding-top: 1px;
+	clear:both;
+	text-align:right;
+}
+
+.dependencies .errorPercentage {
+	color: grey;	
+}
+
+.dependencies div.cell .errorPercentage {
+	padding-left:5px;
+	font-size: 12pt !important;
+}
+
+
+.dependencies div.monitor div.chart {
+}
+
+.dependencies div.monitor div.chart svg {
+}
+
+.dependencies div.monitor div.chart svg text {
+	fill: white;
+}
+
+
+.dependencies div.circuitStatus {
+	width:100%;
+	white-space: nowrap;
+	font-size: 9pt;	
+	margin: 0 auto;
+	clear:both;
+	text-align:right;
+	padding-top: 4px;
+}
+
+.dependencies #hidden {
+    width:1px;
+    height:1px;
+    background: lightgrey;
+    display: none;
+}
+
+
+
+/* sparkline */
+.dependencies path {
+	stroke: steelblue;
+	stroke-width: 1;
+	fill: none;
+}
+
+
+}

+ 542 - 0
pigx-visual/pigx-monitor/src/main/resources/static/hystrix/components/hystrixCommand/hystrixCommand.js

@@ -0,0 +1,542 @@
+
+(function(window) {
+
+	// cache the templates we use on this page as global variables (asynchronously)
+	jQuery.get(getRelativePath("components/hystrixCommand/templates/hystrixCircuit.html"), function(data) {
+		hystrixTemplateCircuit = data;
+	});
+	jQuery.get(getRelativePath("components/hystrixCommand/templates/hystrixCircuitContainer.html"), function(data) {
+		hystrixTemplateCircuitContainer = data;
+	});
+
+	function getRelativePath(path) {
+		var p = location.pathname.slice(0, location.pathname.lastIndexOf("/")+1);
+		return p + path;
+	}
+
+	/**
+	 * Object containing functions for displaying and updating the UI with streaming data.
+	 *
+	 * Publish this externally as "HystrixCommandMonitor"
+	 */
+	window.HystrixCommandMonitor = function(containerId, args) {
+
+		var self = this; // keep scope under control
+		self.args = args;
+		if(self.args == undefined) {
+			self.args = {};
+		}
+
+		this.containerId = containerId;
+
+		/**
+		 * Initialization on construction
+		 */
+		// intialize various variables we use for visualization
+		var maxXaxisForCircle="40%";
+		var maxYaxisForCircle="40%";
+		var maxRadiusForCircle="125";
+
+		// CIRCUIT_BREAKER circle visualization settings
+		self.circuitCircleRadius = d3.scale.pow().exponent(0.5).domain([0, 400]).range(["5", maxRadiusForCircle]); // requests per second per host
+		self.circuitCircleYaxis = d3.scale.linear().domain([0, 400]).range(["30%", maxXaxisForCircle]);
+		self.circuitCircleXaxis = d3.scale.linear().domain([0, 400]).range(["30%", maxYaxisForCircle]);
+		self.circuitColorRange = d3.scale.linear().domain([10, 25, 40, 50]).range(["green", "#FFCC00", "#FF9900", "red"]);
+		self.circuitErrorPercentageColorRange = d3.scale.linear().domain([0, 10, 35, 50]).range(["grey", "black", "#FF9900", "red"]);
+
+		/**
+		 * We want to keep sorting in the background since data values are always changing, so this will re-sort every X milliseconds
+		 * to maintain whatever sort the user (or default) has chosen.
+		 *
+		 * In other words, sorting only for adds/deletes is not sufficient as all but alphabetical sort are dynamically changing.
+		 */
+		setInterval(function() {
+			// sort since we have added a new one
+			self.sortSameAsLast();
+		}, 10000);
+
+
+		/**
+		 * END of Initialization on construction
+		 */
+
+		/**
+		 * Event listener to handle new messages from EventSource as streamed from the server.
+		 */
+		/* public */ self.eventSourceMessageListener = function(e) {
+			var data = JSON.parse(e.data);
+			if(data) {
+				// check for reportingHosts (if not there, set it to 1 for singleHost vs cluster)
+				if(!data.reportingHosts) {
+					data.reportingHosts = 1;
+				}
+
+				if(data && data.type == 'HystrixCommand') {
+					if (data.deleteData == 'true') {
+						deleteCircuit(data.escapedName);
+					} else {
+						displayCircuit(data);
+					}
+				}
+			}
+		};
+
+		/**
+		 * Pre process the data before displying in the UI.
+		 * e.g   Get Averages from sums, do rate calculation etc.
+		 */
+		function preProcessData(data) {
+			// set defaults for values that may be missing from older streams
+			setIfMissing(data, "rollingCountBadRequests", 0);
+			// assert all the values we need
+			validateData(data);
+			// escape string used in jQuery & d3 selectors
+			data.escapedName = data.name.replace(/([ !"#$%&'()*+,./:;<=>?@[\]^`{|}~])/g,'\\$1');
+			// do math
+			convertAllAvg(data);
+			calcRatePerSecond(data);
+		}
+
+		function setIfMissing(data, key, defaultValue) {
+			if(data[key] == undefined) {
+				data[key] = defaultValue;
+			}
+		}
+
+		/**
+		 * Since the stream of data can be aggregated from multiple hosts in a tiered manner
+		 * the aggregation just sums everything together and provides us the denominator (reportingHosts)
+		 * so we must divide by it to get an average per instance value.
+		 *
+		 * We want to do this on any numerical values where we want per instance rather than cluster-wide sum.
+		 */
+		function convertAllAvg(data) {
+			convertAvg(data, "errorPercentage", true);
+			convertAvg(data, "latencyExecute_mean", false);
+			convertAvg(data, "latencyTotal_mean", false);
+		}
+
+		function convertAvg(data, key, decimal) {
+			if (decimal) {
+				data[key] = getInstanceAverage(data[key], data["reportingHosts"], decimal);
+			} else {
+				data[key] = getInstanceAverage(data[key], data["reportingHosts"], decimal);
+			}
+		}
+
+		function getInstanceAverage(value, reportingHosts, decimal) {
+			if (decimal) {
+				return roundNumber(value/reportingHosts);
+			} else {
+				return Math.floor(value/reportingHosts);
+			}
+		}
+
+		function calcRatePerSecond(data) {
+			var numberSeconds = data["propertyValue_metricsRollingStatisticalWindowInMilliseconds"] / 1000;
+
+			var totalRequests = data["requestCount"];
+			if (totalRequests < 0) {
+				totalRequests = 0;
+			}
+			data["ratePerSecond"] =  roundNumber(totalRequests / numberSeconds);
+			data["ratePerSecondPerHost"] =  roundNumber(totalRequests / numberSeconds / data["reportingHosts"]) ;
+	    }
+
+		function validateData(data) {
+			assertNotNull(data,"reportingHosts");
+            assertNotNull(data,"type");
+            assertNotNull(data,"name");
+            assertNotNull(data,"group");
+            // assertNotNull(data,"currentTime");
+            assertNotNull(data,"isCircuitBreakerOpen");
+            assertNotNull(data,"errorPercentage");
+            assertNotNull(data,"errorCount");
+            assertNotNull(data,"requestCount");
+            assertNotNull(data,"rollingCountCollapsedRequests");
+            assertNotNull(data,"rollingCountExceptionsThrown");
+            assertNotNull(data,"rollingCountFailure");
+            assertNotNull(data,"rollingCountFallbackFailure");
+            assertNotNull(data,"rollingCountFallbackRejection");
+            assertNotNull(data,"rollingCountFallbackSuccess");
+            assertNotNull(data,"rollingCountResponsesFromCache");
+            assertNotNull(data,"rollingCountSemaphoreRejected");
+            assertNotNull(data,"rollingCountShortCircuited");
+            assertNotNull(data,"rollingCountSuccess");
+            assertNotNull(data,"rollingCountThreadPoolRejected");
+            assertNotNull(data,"rollingCountTimeout");
+            assertNotNull(data,"rollingCountBadRequests");
+            assertNotNull(data,"currentConcurrentExecutionCount");
+            assertNotNull(data,"latencyExecute_mean");
+            assertNotNull(data,"latencyExecute");
+            assertNotNull(data,"latencyTotal_mean");
+            assertNotNull(data,"latencyTotal");
+            assertNotNull(data,"propertyValue_circuitBreakerRequestVolumeThreshold");
+            assertNotNull(data,"propertyValue_circuitBreakerSleepWindowInMilliseconds");
+            assertNotNull(data,"propertyValue_circuitBreakerErrorThresholdPercentage");
+            assertNotNull(data,"propertyValue_circuitBreakerForceOpen");
+						assertNotNull(data,"propertyValue_circuitBreakerForceClosed");
+            assertNotNull(data,"propertyValue_executionIsolationStrategy");
+            assertNotNull(data,"propertyValue_executionIsolationThreadTimeoutInMilliseconds");
+            assertNotNull(data,"propertyValue_executionIsolationThreadInterruptOnTimeout");
+            // assertNotNull(data,"propertyValue_executionIsolationThreadPoolKeyOverride");
+            assertNotNull(data,"propertyValue_executionIsolationSemaphoreMaxConcurrentRequests");
+            assertNotNull(data,"propertyValue_fallbackIsolationSemaphoreMaxConcurrentRequests");
+            assertNotNull(data,"propertyValue_requestCacheEnabled");
+            assertNotNull(data,"propertyValue_requestLogEnabled");
+            assertNotNull(data,"propertyValue_metricsRollingStatisticalWindowInMilliseconds");
+		}
+
+		function assertNotNull(data, key) {
+			if(data[key] == undefined) {
+				throw new Error("Key Missing: " + key + " for " + data.name);
+			}
+		}
+
+		/**
+		 * Method to display the CIRCUIT data
+		 *
+		 * @param data
+		 */
+		/* private */ function displayCircuit(data) {
+
+			try {
+				preProcessData(data);
+			} catch (err) {
+				log("Failed preProcessData: " + err.message);
+				return;
+			}
+
+			// add the 'addCommas' function to the 'data' object so the HTML templates can use it
+			data.addCommas = addCommas;
+			// add the 'roundNumber' function to the 'data' object so the HTML templates can use it
+			data.roundNumber = roundNumber;
+			// add the 'getInstanceAverage' function to the 'data' object so the HTML templates can use it
+			data.getInstanceAverage = getInstanceAverage;
+
+			var addNew = false;
+			// check if we need to create the container
+			if(!$('#CIRCUIT_' + data.escapedName).length) {
+				// args for display
+				if(self.args.includeDetailIcon != undefined && self.args.includeDetailIcon) {
+					data.includeDetailIcon = true;
+				}else {
+					data.includeDetailIcon = false;
+				}
+
+				// it doesn't exist so add it
+				var html = tmpl(hystrixTemplateCircuitContainer, data);
+				// remove the loading thing first
+				$('#' + containerId + ' span.loading').remove();
+				// now create the new data and add it
+				$('#' + containerId + '').append(html);
+
+				// add the default sparkline graph
+				d3.selectAll('#graph_CIRCUIT_' + data.escapedName + ' svg').append("svg:path");
+
+				// remember this is new so we can trigger a sort after setting data
+				addNew = true;
+			}
+
+
+			// now update/insert the data
+			$('#CIRCUIT_' + data.escapedName + ' div.monitor_data').html(tmpl(hystrixTemplateCircuit, data));
+
+			var ratePerSecond = data.ratePerSecond;
+			var ratePerSecondPerHost = data.ratePerSecondPerHost;
+			var ratePerSecondPerHostDisplay = ratePerSecondPerHost;
+			var errorThenVolume = (data.errorPercentage * 100000000) +  ratePerSecond;
+
+			// set the rates on the div element so it's available for sorting
+			$('#CIRCUIT_' + data.escapedName).attr('rate_value', ratePerSecond);
+			$('#CIRCUIT_' + data.escapedName).attr('error_then_volume', errorThenVolume);
+
+			// update errorPercentage color on page
+			$('#CIRCUIT_' + data.escapedName + ' a.errorPercentage').css('color', self.circuitErrorPercentageColorRange(data.errorPercentage));
+
+			updateCircle('circuit', '#CIRCUIT_' + data.escapedName + ' circle', ratePerSecondPerHostDisplay, data.errorPercentage);
+
+			if(data.graphValues) {
+				// we have a set of values to initialize with
+				updateSparkline('circuit', '#CIRCUIT_' + data.escapedName + ' path', data.graphValues);
+			} else {
+				updateSparkline('circuit', '#CIRCUIT_' + data.escapedName + ' path', ratePerSecond);
+			}
+
+			if(addNew) {
+				// sort since we added a new circuit
+				self.sortSameAsLast();
+			}
+		}
+
+		/* round a number to X digits: num => the number to round, dec => the number of decimals */
+		/* private */ function roundNumber(num) {
+			var dec=1;
+			var result = Math.round(num*Math.pow(10,dec))/Math.pow(10,dec);
+			var resultAsString = result.toString();
+			if(resultAsString.indexOf('.') == -1) {
+				resultAsString = resultAsString + '.0';
+			}
+			return resultAsString;
+		};
+
+
+
+
+		/* private */ function updateCircle(variablePrefix, cssTarget, rate, errorPercentage) {
+			var newXaxisForCircle = self[variablePrefix + 'CircleXaxis'](rate);
+			if(parseInt(newXaxisForCircle) > parseInt(maxXaxisForCircle)) {
+				newXaxisForCircle = maxXaxisForCircle;
+			}
+			var newYaxisForCircle = self[variablePrefix + 'CircleYaxis'](rate);
+			if(parseInt(newYaxisForCircle) > parseInt(maxYaxisForCircle)) {
+				newYaxisForCircle = maxYaxisForCircle;
+			}
+			var newRadiusForCircle = self[variablePrefix + 'CircleRadius'](rate);
+			if(parseInt(newRadiusForCircle) > parseInt(maxRadiusForCircle)) {
+				newRadiusForCircle = maxRadiusForCircle;
+			}
+
+			d3.selectAll(cssTarget)
+				.transition()
+				.duration(400)
+				.attr("cy", newYaxisForCircle)
+				.attr("cx", newXaxisForCircle)
+				.attr("r", newRadiusForCircle)
+				.style("fill", self[variablePrefix + 'ColorRange'](errorPercentage));
+		}
+
+		/* private */ function updateSparkline(variablePrefix, cssTarget, newDataPoint) {
+				var currentTimeMilliseconds = new Date().getTime();
+				var data = self[variablePrefix + cssTarget + '_data'];
+				if(typeof data == 'undefined') {
+					// else it's new
+					if(typeof newDataPoint == 'object') {
+						// we received an array of values, so initialize with it
+						data = newDataPoint;
+					} else {
+						// v: VALUE, t: TIME_IN_MILLISECONDS
+						data = [{"v":parseFloat(newDataPoint),"t":currentTimeMilliseconds}];
+					}
+					self[variablePrefix + cssTarget + '_data'] = data;
+				} else {
+					if(typeof newDataPoint == 'object') {
+						/* if an array is passed in we'll replace the cached one */
+						data = newDataPoint;
+					} else {
+						// else we just add to the existing one
+						data.push({"v":parseFloat(newDataPoint),"t":currentTimeMilliseconds});
+					}
+				}
+
+				while(data.length > 200) { // 400 should be plenty for the 2 minutes we have the scale set to below even with a very low update latency
+					// remove data so we don't keep increasing forever
+					data.shift();
+				}
+
+				if(data.length == 1 && data[0].v == 0) {
+					//console.log("we have a single 0 so skipping");
+					// don't show if we have a single 0
+					return;
+				}
+
+				if(data.length > 1 && data[0].v == 0 && data[1].v != 0) {
+					//console.log("we have a leading 0 so removing it");
+					// get rid of a leading 0 if the following number is not a 0
+					data.shift();
+				}
+
+				var xScale = d3.time.scale().domain([new Date(currentTimeMilliseconds-(60*1000*2)), new Date(currentTimeMilliseconds)]).range([0, 140]);
+
+				var yMin = d3.min(data, function(d) { return d.v; });
+				var yMax = d3.max(data, function(d) { return d.v; });
+				var yScale = d3.scale.linear().domain([yMin, yMax]).nice().range([60, 0]); // y goes DOWN, so 60 is the "lowest"
+
+				sparkline = d3.svg.line()
+				// assign the X function to plot our line as we wish
+				.x(function(d,i) {
+					// return the X coordinate where we want to plot this datapoint based on the time
+					return xScale(new Date(d.t));
+				})
+				.y(function(d) {
+					return yScale(d.v);
+				})
+				.interpolate("basis");
+
+				d3.selectAll(cssTarget).attr("d", sparkline(data));
+		}
+
+		/* private */ function deleteCircuit(circuitName) {
+			$('#CIRCUIT_' + circuitName).remove();
+		}
+
+	};
+
+	// public methods for sorting
+	HystrixCommandMonitor.prototype.sortByVolume = function() {
+		var direction = "desc";
+		if(this.sortedBy == 'rate_desc') {
+			direction = 'asc';
+		}
+		this.sortByVolumeInDirection(direction);
+	};
+
+	HystrixCommandMonitor.prototype.sortByVolumeInDirection = function(direction) {
+		this.sortedBy = 'rate_' + direction;
+		$('#' + this.containerId + ' div.monitor').tsort({order: direction, attr: 'rate_value'});
+	};
+
+	HystrixCommandMonitor.prototype.sortAlphabetically = function() {
+		var direction = "asc";
+		if(this.sortedBy == 'alph_asc') {
+			direction = 'desc';
+		}
+		this.sortAlphabeticalInDirection(direction);
+	};
+
+	HystrixCommandMonitor.prototype.sortAlphabeticalInDirection = function(direction) {
+		this.sortedBy = 'alph_' + direction;
+		$('#' + this.containerId + ' div.monitor').tsort("p.name", {order: direction});
+	};
+
+
+	HystrixCommandMonitor.prototype.sortByError = function() {
+		var direction = "desc";
+		if(this.sortedBy == 'error_desc') {
+			direction = 'asc';
+		}
+		this.sortByErrorInDirection(direction);
+	};
+
+	HystrixCommandMonitor.prototype.sortByErrorInDirection = function(direction) {
+		this.sortedBy = 'error_' + direction;
+		$('#' + this.containerId + ' div.monitor').tsort(".errorPercentage .value", {order: direction});
+	};
+
+	HystrixCommandMonitor.prototype.sortByErrorThenVolume = function() {
+		var direction = "desc";
+		if(this.sortedBy == 'error_then_volume_desc') {
+			direction = 'asc';
+		}
+		this.sortByErrorThenVolumeInDirection(direction);
+	};
+
+	HystrixCommandMonitor.prototype.sortByErrorThenVolumeInDirection = function(direction) {
+		this.sortedBy = 'error_then_volume_' + direction;
+		$('#' + this.containerId + ' div.monitor').tsort({order: direction, attr: 'error_then_volume'});
+	};
+
+	HystrixCommandMonitor.prototype.sortByLatency90 = function() {
+		var direction = "desc";
+		if(this.sortedBy == 'lat90_desc') {
+			direction = 'asc';
+		}
+		this.sortedBy = 'lat90_' + direction;
+		this.sortByMetricInDirection(direction, ".latency90 .value");
+	};
+
+	HystrixCommandMonitor.prototype.sortByLatency99 = function() {
+		var direction = "desc";
+		if(this.sortedBy == 'lat99_desc') {
+			direction = 'asc';
+		}
+		this.sortedBy = 'lat99_' + direction;
+		this.sortByMetricInDirection(direction, ".latency99 .value");
+	};
+
+	HystrixCommandMonitor.prototype.sortByLatency995 = function() {
+		var direction = "desc";
+		if(this.sortedBy == 'lat995_desc') {
+			direction = 'asc';
+		}
+		this.sortedBy = 'lat995_' + direction;
+		this.sortByMetricInDirection(direction, ".latency995 .value");
+	};
+
+	HystrixCommandMonitor.prototype.sortByLatencyMean = function() {
+		var direction = "desc";
+		if(this.sortedBy == 'latMean_desc') {
+			direction = 'asc';
+		}
+		this.sortedBy = 'latMean_' + direction;
+		this.sortByMetricInDirection(direction, ".latencyMean .value");
+	};
+
+	HystrixCommandMonitor.prototype.sortByLatencyMedian = function() {
+		var direction = "desc";
+		if(this.sortedBy == 'latMedian_desc') {
+			direction = 'asc';
+		}
+		this.sortedBy = 'latMedian_' + direction;
+		this.sortByMetricInDirection(direction, ".latencyMedian .value");
+	};
+
+	HystrixCommandMonitor.prototype.sortByMetricInDirection = function(direction, metric) {
+		$('#' + this.containerId + ' div.monitor').tsort(metric, {order: direction});
+	};
+
+	// this method is for when new divs are added to cause the elements to be sorted to whatever the user last chose
+	HystrixCommandMonitor.prototype.sortSameAsLast = function() {
+		if(this.sortedBy == 'alph_asc') {
+			this.sortAlphabeticalInDirection('asc');
+		} else if(this.sortedBy == 'alph_desc') {
+			this.sortAlphabeticalInDirection('desc');
+		} else if(this.sortedBy == 'rate_asc') {
+			this.sortByVolumeInDirection('asc');
+		} else if(this.sortedBy == 'rate_desc') {
+			this.sortByVolumeInDirection('desc');
+		} else if(this.sortedBy == 'error_asc') {
+			this.sortByErrorInDirection('asc');
+		} else if(this.sortedBy == 'error_desc') {
+			this.sortByErrorInDirection('desc');
+		} else if(this.sortedBy == 'error_then_volume_asc') {
+			this.sortByErrorThenVolumeInDirection('asc');
+		} else if(this.sortedBy == 'error_then_volume_desc') {
+			this.sortByErrorThenVolumeInDirection('desc');
+		} else if(this.sortedBy == 'lat90_asc') {
+			this.sortByMetricInDirection('asc', '.latency90 .value');
+		} else if(this.sortedBy == 'lat90_desc') {
+			this.sortByMetricInDirection('desc', '.latency90 .value');
+		} else if(this.sortedBy == 'lat99_asc') {
+			this.sortByMetricInDirection('asc', '.latency99 .value');
+		} else if(this.sortedBy == 'lat99_desc') {
+			this.sortByMetricInDirection('desc', '.latency99 .value');
+		} else if(this.sortedBy == 'lat995_asc') {
+			this.sortByMetricInDirection('asc', '.latency995 .value');
+		} else if(this.sortedBy == 'lat995_desc') {
+			this.sortByMetricInDirection('desc', '.latency995 .value');
+		} else if(this.sortedBy == 'latMean_asc') {
+			this.sortByMetricInDirection('asc', '.latencyMean .value');
+		} else if(this.sortedBy == 'latMean_desc') {
+			this.sortByMetricInDirection('desc', '.latencyMean .value');
+		} else if(this.sortedBy == 'latMedian_asc') {
+			this.sortByMetricInDirection('asc', '.latencyMedian .value');
+		} else if(this.sortedBy == 'latMedian_desc') {
+			this.sortByMetricInDirection('desc', '.latencyMedian .value');
+		}
+	};
+
+	// default sort type and direction
+	this.sortedBy = 'alph_asc';
+
+
+	// a temporary home for the logger until we become more sophisticated
+	function log(message) {
+		console.log(message);
+	};
+
+	function addCommas(nStr){
+	  nStr += '';
+	  if(nStr.length <=3) {
+		  return nStr; //shortcut if we don't need commas
+	  }
+	  x = nStr.split('.');
+	  x1 = x[0];
+	  x2 = x.length > 1 ? '.' + x[1] : '';
+	  var rgx = /(\d+)(\d{3})/;
+	  while (rgx.test(x1)) {
+	    x1 = x1.replace(rgx, '$1' + ',' + '$2');
+	  }
+	  return x1 + x2;
+	}
+})(window);

BIN
pigx-visual/pigx-monitor/src/main/resources/static/hystrix/components/hystrixCommand/magnifying-glass-icon-20.png


BIN
pigx-visual/pigx-monitor/src/main/resources/static/hystrix/components/hystrixCommand/magnifying-glass-icon.png


+ 77 - 0
pigx-visual/pigx-monitor/src/main/resources/static/hystrix/components/hystrixCommand/templates/hystrixCircuit.html

@@ -0,0 +1,77 @@
+	<div class="counters">
+		
+		<div class="cell line">
+			<a href="javascript://" title="Error Percentage [Timed-out + Threadpool Rejected + Failure / Total]" class="tooltip errorPercentage"><span class="value"><%= errorPercentage %></span> %</a>
+		</div>
+		
+		<div class="cell borderRight">
+			<% if(propertyValue_executionIsolationStrategy == 'THREAD') { %>
+				<a href="javascript://" title="Timed-out Request Count" class="line tooltip timeout"><%= addCommas(rollingCountTimeout) %></a>
+				<a href="javascript://" title="Threadpool Rejected Request Count" class="line tooltip rejected"><%= addCommas(rollingCountThreadPoolRejected) %></a>
+			<% } %>
+			<% if(propertyValue_executionIsolationStrategy == 'SEMAPHORE') { %>
+				<a href="javascript://" title="Semaphore Rejected Request Count" class="line tooltip rejected"><%= addCommas(rollingCountSemaphoreRejected) %></a>
+			<% } %>
+			<a href="javascript://" title="Failure Request Count" class="line tooltip failure"><%= addCommas(rollingCountFailure) %></a> 
+		</div>
+		<div class="cell borderRight">
+			<a href="javascript://" title="Successful Request Count" class="line tooltip success"><%= addCommas(rollingCountSuccess) %></a>
+			<a href="javascript://" title="Short-circuited Request Count" class="line tooltip shortCircuited"><%= addCommas(rollingCountShortCircuited) %></a>
+			<a href="javascript://" title="Bad Request Count" class="line tooltip badRequest"><%= addCommas(rollingCountBadRequests) %></a>
+			<br>
+		</div>
+	</div>
+
+	<div class="rate">
+		<a href="javascript://" title="Total Request Rate per Second per Reporting Host" class="tooltip rate"><span class="smaller">Host: </span><span class="ratePerSecondPerHost"><%= addCommas(roundNumber(ratePerSecondPerHost)) %></span>/s</a>
+	</div>
+	<div class="rate">	
+		<a href="javascript://" title="Total Request Rate per Second for Cluster" class="tooltip rate"><span class="smaller">Cluster: </span><span class="ratePerSecond"><%= addCommas(roundNumber(ratePerSecond)) %></span>/s</a>
+	</div>
+		
+		<div class="circuitStatus">
+		<% if(propertyValue_circuitBreakerForceClosed) { %>
+			<span class="smaller">[ <font color="orange">Forced Closed</font> ]</span>
+		<% } %>
+		<% if(propertyValue_circuitBreakerForceOpen) { %>
+			Circuit <font color="red">Forced Open</font>
+		<% } else { %>
+			<% if(isCircuitBreakerOpen == reportingHosts) { %>
+				Circuit <font color="red">Open</font>
+			<% } else if(isCircuitBreakerOpen == 0) { %>
+				Circuit <font color="green">Closed</font>
+			<% } else {
+				/* We have some circuits that are open */  
+			%>
+				Circuit <font color="orange"><%= isCircuitBreakerOpen.toString().replace("true", "Open").replace("false", "Closed") %>)</font>
+			<% }  %>
+		<% } %>
+		</div>
+		
+		<div class="spacer"></div>
+		
+		<div class="tableRow">
+			<% if(typeof reportingHosts != 'undefined') { %>
+			<div class="cell header">Hosts</div>
+			<div class="cell data"><%= reportingHosts %></div>
+			<% } else { %>
+			<div class="cell header">Host</div>
+			<div class="cell data">Single</div>
+			<% } %>
+			<div class="cell header">90th</div>
+			<div class="cell data latency90"><span class="value"><%= getInstanceAverage(latencyExecute['90'], reportingHosts, false) %></span>ms</div>
+		</div>
+		<div class="tableRow">
+			<div class="cell header">Median</div>
+			<div class="cell data latencyMedian"><span class="value"><%= getInstanceAverage(latencyExecute['50'], reportingHosts, false) %></span>ms</div>
+			<div class="cell header">99th</div>
+			<div class="cell data latency99"><span class="value"><%= getInstanceAverage(latencyExecute['99'], reportingHosts, false) %></span>ms</div>
+		</div>
+		<div class="tableRow">
+			<div class="cell header">Mean</div>
+			<div class="cell data latencyMean"><span class="value"><%= latencyExecute_mean %></span>ms</div>
+			<div class="cell header">99.5th</div>
+			<div class="cell data latency995"><span class="value"><%= getInstanceAverage(latencyExecute['99.5'], reportingHosts, false) %></span>ms</div>
+		</div>
+	
+<!-- 	<a href="">History</a> <a href="">EPIC</a> <a href="">Actions</a> -->

+ 40 - 0
pigx-visual/pigx-monitor/src/main/resources/static/hystrix/components/hystrixCommand/templates/hystrixCircuitContainer.html

@@ -0,0 +1,40 @@
+<div class="monitor" id="CIRCUIT_<%= name %>" style="position:relative;">
+	<%
+		var displayName = name;
+		var toolTip = "";
+		if(displayName.length > 32) {
+			displayName = displayName.substring(0,4) + "..." + displayName.substring(displayName.length-20, displayName.length);
+			toolTip = "title=\"" + name + "\"";
+		}
+	%>
+	
+	<div id="chart_CIRCUIT_<%= name %>" class="chart" style="position:absolute;top:0px;left:0; float:left; width:100%; height:100%;"></div>
+    <div style="position:absolute;top:0;width:100%;height:15px;opacity:0.8; background:white;">
+    	<% if(includeDetailIcon) { %>
+    	<p class="name" <%= toolTip %> style="padding-right:16px">
+    		<%= displayName %>
+    		<a href="../dependencies/command.jsp?name=<%= name %>"><img src="components/hystrixCommand/magnifying-glass-icon-20.png" height="14px" width="14px" border="0" style="position: absolute; right:0px;"></a>
+    	</p>
+    	<% }  else { %>
+    	<p class="name" <%= toolTip %>><%= displayName %></p>
+    	<% }  %>
+    </div>
+	<div style="position:absolute;top:15px;; opacity:0.8; background:white; width:100%; height:95%;">
+		<div class="monitor_data"></div>
+	</div>
+	<div id="graph_CIRCUIT_<%= name %>" class="graph" style="position:absolute;top:25px;left:0; float:left; width:140px; height:62px;"></div>
+
+	<script>
+		var y = 200;
+		/* escape with two backslashes */
+		var vis = d3.select("#chart_CIRCUIT_<%= name.replace(/([ !"#$%&'()*+,./:;<=>?@[\]^`{|}~])/g,'\\\\$1') %>").append("svg:svg").attr("width", "100%").attr("height", "100%");
+		/* add a circle -- we don't use the data point, we set it manually, so just passing in [1] */
+		var circle = vis.selectAll("circle").data([1]).enter().append("svg:circle");
+		/* setup the initial styling and sizing of the circle */
+		circle.style("fill", "green").attr("cx", "30%").attr("cy", "30%").attr("r", 5);
+		
+		/* add the line graph - it will be populated by javascript, no default to show here */
+		/* escape with two backslashes */
+		var graph = d3.select("#graph_CIRCUIT_<%= name.replace(/([ !"#$%&'()*+,./:;<=>?@[\]^`{|}~])/g,'\\\\$1') %>").append("svg:svg").attr("width", "100%").attr("height", "100%");
+	</script>
+</div>

+ 6 - 0
pigx-visual/pigx-monitor/src/main/resources/static/hystrix/components/hystrixCommand/templates/hystrixCircuitProperties.html

@@ -0,0 +1,6 @@
+		<div class="tableRow">
+			<div class="cell header">Median</div>
+			<div class="cell data"><span class="value"><%= sla_medianLastMinute %></span>ms</div>
+			<div class="cell header">99th</div>
+			<div class="cell data"><span class="value"><%= sla_percentile99LastMinute %></span>ms</div>
+		</div>

+ 141 - 0
pigx-visual/pigx-monitor/src/main/resources/static/hystrix/components/hystrixThreadPool/hystrixThreadPool.css

@@ -0,0 +1,141 @@
+.dependencyThreadPools .spacer {
+	width: 100%;
+	margin: 0 auto;
+	padding-top:4px;
+	clear:both;
+}
+
+
+.dependencyThreadPools .last {
+	margin-right: 0px;
+}
+
+.dependencyThreadPools span.loading {
+	display: block;
+	padding-top: 6%;
+	padding-bottom: 6%;
+	color: gray;
+	text-align: center;
+}
+
+.dependencyThreadPools span.loading.failed {
+	color: red;
+}
+
+
+.dependencyThreadPools div.monitor {
+	float: left;
+	margin-right:5px; /* these are tweaked to look good on desktop and iPad portrait, and fit things densely */
+	margin-top:5px;
+}
+
+.dependencyThreadPools div.monitor p.name {
+	font-weight:bold;
+	font-size: 10pt;
+	text-align: right;	
+	padding-bottom: 5px;
+}
+
+.dependencyThreadPools div.monitor_data {
+	margin: 0 auto;	
+}
+
+.dependencyThreadPools span.smaller {
+	font-size: 8pt;
+	color: grey;	
+}
+
+
+.dependencyThreadPools div.tableRow {
+	width:100%;
+	white-space: nowrap;
+	font-size: 8pt;	
+	margin: 0 auto;
+	clear:both;
+}
+
+.dependencyThreadPools div.tableRow .cell {
+	float:left;
+}
+
+.dependencyThreadPools div.tableRow .header {
+	text-align:right;
+	padding-right:5px;
+}
+
+.dependencyThreadPools div.tableRow .header.left {
+	width:85px;
+}
+
+.dependencyThreadPools div.tableRow .header.right {
+	width:75px;
+}
+
+.dependencyThreadPools div.tableRow .data {
+	font-weight: bold;
+	text-align:right;
+}
+
+.dependencyThreadPools div.tableRow .data.left {
+	width:30px;
+}
+
+.dependencyThreadPools div.tableRow .data.right {
+	width:45px;
+}
+
+.dependencyThreadPools div.monitor {
+	width: 245px; /* we want a fixed width instead of percentage as I want the boxes to be a set size and then fill in as many as can fit in each row ... this allows 3 columns on an iPad */
+	height: 110px;
+}
+
+
+
+
+
+/* override the HREF when we have specified it as a tooltip to not act like a link */
+.dependencyThreadPools div.monitor_data a.tooltip {
+	text-decoration: none;
+	cursor: default;	
+}
+
+.dependencyThreadPools div.monitor_data a.rate {
+	font-weight:bold;
+	color: black;	
+	font-size: 11pt;
+}
+
+.dependencyThreadPools div.rate {
+	padding-top: 1px;
+	clear:both;
+	text-align:right;
+}
+
+.dependencyThreadPools span.rate_value {
+	font-weight:bold;	
+}
+
+
+
+
+
+
+
+.dependencyThreadPools div.monitor div.chart {
+}
+
+.dependencyThreadPools div.monitor div.chart svg {
+}
+
+.dependencyThreadPools div.monitor div.chart svg text {
+	fill: white;
+}
+
+.dependencyThreadPools #hidden {
+    width:1px;
+    height:1px;
+    background: lightgrey;
+    display: none;
+}
+
+

+ 343 - 0
pigx-visual/pigx-monitor/src/main/resources/static/hystrix/components/hystrixThreadPool/hystrixThreadPool.js

@@ -0,0 +1,343 @@
+
+(function(window) {
+
+	// cache the templates we use on this page as global variables (asynchronously)
+	jQuery.get(getRelativePath("components/hystrixThreadPool/templates/hystrixThreadPool.html"), function(data) {
+		htmlTemplate = data;
+	});
+	jQuery.get(getRelativePath("components/hystrixThreadPool/templates/hystrixThreadPoolContainer.html"), function(data) {
+		htmlTemplateContainer = data;
+	});
+
+	function getRelativePath(path) {
+		var p = location.pathname.slice(0, location.pathname.lastIndexOf("/")+1);
+		return p + path;
+	}
+
+	/**
+	 * Object containing functions for displaying and updating the UI with streaming data.
+	 * 
+	 * Publish this externally as "HystrixThreadPoolMonitor"
+	 */
+	window.HystrixThreadPoolMonitor = function(containerId) {
+		
+		var self = this; // keep scope under control
+		
+		this.containerId = containerId;
+		
+		/**
+		 * Initialization on construction
+		 */
+		// intialize various variables we use for visualization
+		var maxXaxisForCircle="40%";
+		var maxYaxisForCircle="40%";
+		var maxRadiusForCircle="125";
+		var maxDomain = 2000;
+		
+		self.circleRadius = d3.scale.pow().exponent(0.5).domain([0, maxDomain]).range(["5", maxRadiusForCircle]); // requests per second per host
+		self.circleYaxis = d3.scale.linear().domain([0, maxDomain]).range(["30%", maxXaxisForCircle]);
+		self.circleXaxis = d3.scale.linear().domain([0, maxDomain]).range(["30%", maxYaxisForCircle]);
+		self.colorRange = d3.scale.linear().domain([10, 25, 40, 50]).range(["green", "#FFCC00", "#FF9900", "red"]);
+		self.errorPercentageColorRange = d3.scale.linear().domain([0, 10, 35, 50]).range(["grey", "black", "#FF9900", "red"]);
+
+		/**
+		 * We want to keep sorting in the background since data values are always changing, so this will re-sort every X milliseconds
+		 * to maintain whatever sort the user (or default) has chosen.
+		 * 
+		 * In other words, sorting only for adds/deletes is not sufficient as all but alphabetical sort are dynamically changing.
+		 */
+		setInterval(function() {
+			// sort since we have added a new one
+			self.sortSameAsLast();
+		}, 1000)
+		
+		/**
+		 * END of Initialization on construction
+		 */
+		
+		/**
+		 * Event listener to handle new messages from EventSource as streamed from the server.
+		 */
+		/* public */ self.eventSourceMessageListener = function(e) {
+			var data = JSON.parse(e.data);
+			if(data) {
+				// check for reportingHosts (if not there, set it to 1 for singleHost vs cluster)
+				if(!data.reportingHosts) {
+					data.reportingHosts = 1;
+				}
+				
+				if(data && data.type == 'HystrixThreadPool') {
+					if (data.deleteData == 'true') {
+						deleteThreadPool(data.escapedName);
+					} else {
+						displayThreadPool(data);
+					}
+				}
+			}
+		}
+		
+		/**
+		 * Pre process the data before displying in the UI. 
+		 * e.g   Get Averages from sums, do rate calculation etc. 
+		 */
+		function preProcessData(data) {
+			validateData(data);
+			// escape string used in jQuery & d3 selectors
+			data.escapedName = data.name.replace(/([ !"#$%&'()*+,./:;<=>?@[\]^`{|}~])/g,'\\$1');
+			// do math
+			converAllAvg(data);
+			calcRatePerSecond(data);
+		}
+		
+		function converAllAvg(data) {
+			convertAvg(data, "propertyValue_queueSizeRejectionThreshold", false);
+		}
+		
+		function convertAvg(data, key, decimal) {
+			if (decimal) {
+				data[key] = roundNumber(data[key]/data["reportingHosts"]);
+			} else {
+				data[key] = Math.floor(data[key]/data["reportingHosts"]);
+			}
+		}
+		
+		function calcRatePerSecond(data) {
+			var numberSeconds = data["propertyValue_metricsRollingStatisticalWindowInMilliseconds"] / 1000;
+
+			var totalThreadsExecuted = data["rollingCountThreadsExecuted"];
+			if (totalThreadsExecuted < 0) {
+				totalThreadsExecuted = 0;
+			}
+			data["ratePerSecond"] =  roundNumber(totalThreadsExecuted / numberSeconds);
+			data["ratePerSecondPerHost"] =  roundNumber(totalThreadsExecuted / numberSeconds / data["reportingHosts"]);
+	    }
+
+		function validateData(data) {
+			
+			assertNotNull(data,"type");
+			assertNotNull(data,"name");
+			// assertNotNull(data,"currentTime");
+			assertNotNull(data,"currentActiveCount");
+			assertNotNull(data,"currentCompletedTaskCount");
+			assertNotNull(data,"currentCorePoolSize");
+			assertNotNull(data,"currentLargestPoolSize");
+			assertNotNull(data,"currentMaximumPoolSize");
+			assertNotNull(data,"currentPoolSize");
+			assertNotNull(data,"currentQueueSize");
+			assertNotNull(data,"currentTaskCount");
+			assertNotNull(data,"rollingCountThreadsExecuted");
+			assertNotNull(data,"rollingMaxActiveThreads");
+			assertNotNull(data,"reportingHosts");
+
+            assertNotNull(data,"propertyValue_queueSizeRejectionThreshold");
+			assertNotNull(data,"propertyValue_metricsRollingStatisticalWindowInMilliseconds");
+		}
+		
+		function assertNotNull(data, key) {
+			if(data[key] == undefined) {
+				if (key == "dependencyOwner") {
+					data["dependencyOwner"] = data.name;
+				} else {
+					throw new Error("Key Missing: " + key + " for " + data.name)
+				}
+			}
+		}
+
+		/**
+		 * Method to display the THREAD_POOL data
+		 * 
+		 * @param data
+		 */
+		/* private */ function displayThreadPool(data) {
+			
+			try {
+				preProcessData(data);
+			} catch (err) {
+				log("Failed preProcessData: " + err.message);
+				return;
+			}
+			
+			// add the 'addCommas' function to the 'data' object so the HTML templates can use it
+			data.addCommas = addCommas;
+			// add the 'roundNumber' function to the 'data' object so the HTML templates can use it
+			data.roundNumber = roundNumber;
+			
+			var addNew = false;
+			// check if we need to create the container
+			if(!$('#THREAD_POOL_' + data.escapedName).length) {
+				// it doesn't exist so add it
+				var html = tmpl(htmlTemplateContainer, data);
+				// remove the loading thing first
+				$('#' + containerId + ' span.loading').remove();
+				// get the current last column and remove the 'last' class from it
+				$('#' + containerId + ' div.last').removeClass('last');
+				// now create the new data and add it
+				$('#' + containerId + '').append(html);
+				// add the 'last' class to the column we just added
+				$('#' + containerId + ' div.monitor').last().addClass('last');
+				
+				// add the default sparkline graph
+				d3.selectAll('#graph_THREAD_POOL_' + data.escapedName + ' svg').append("svg:path");
+				
+				// remember this is new so we can trigger a sort after setting data
+				addNew = true;
+			}
+			
+			// set the rate on the div element so it's available for sorting
+			$('#THREAD_POOL_' + data.escapedName).attr('rate_value', data.ratePerSecondPerHost);
+			
+			// now update/insert the data
+			$('#THREAD_POOL_' + data.escapedName + ' div.monitor_data').html(tmpl(htmlTemplate, data));
+
+			// set variables for circle visualization
+			var rate = data.ratePerSecondPerHost;
+			// we will treat each item in queue as 1% of an error visualization
+			// ie. 5 threads in queue per instance == 5% error percentage
+			var errorPercentage = data.currentQueueSize / data.reportingHosts; 
+			
+			updateCircle('#THREAD_POOL_' + data.escapedName + ' circle', rate, errorPercentage);
+			
+			if(addNew) {
+				// sort since we added a new circuit
+				self.sortSameAsLast();
+			}
+		}
+		
+		/* round a number to X digits: num => the number to round, dec => the number of decimals */
+		/* private */ function roundNumber(num) {
+			var dec=1; // we are hardcoding to support only 1 decimal so that our padding logic at the end is simple
+			var result = Math.round(num*Math.pow(10,dec))/Math.pow(10,dec);
+			var resultAsString = result.toString();
+			if(resultAsString.indexOf('.') == -1) {
+				resultAsString = resultAsString + '.';
+				for(var i=0; i<dec; i++) {
+					resultAsString = resultAsString + '0';
+				}
+			}
+			return resultAsString;
+		};
+		
+		
+		/* private */ function updateCircle(cssTarget, rate, errorPercentage) {
+			var newXaxisForCircle = self.circleXaxis(rate);
+			if(parseInt(newXaxisForCircle) > parseInt(maxXaxisForCircle)) {
+				newXaxisForCircle = maxXaxisForCircle;
+			}
+			var newYaxisForCircle = self.circleYaxis(rate);
+			if(parseInt(newYaxisForCircle) > parseInt(maxYaxisForCircle)) {
+				newYaxisForCircle = maxYaxisForCircle;
+			}
+			var newRadiusForCircle = self.circleRadius(rate);
+			if(parseInt(newRadiusForCircle) > parseInt(maxRadiusForCircle)) {
+				newRadiusForCircle = maxRadiusForCircle;
+			}
+			
+			d3.selectAll(cssTarget)
+				.transition()
+				.duration(400)
+				.attr("cy", newYaxisForCircle)
+				.attr("cx", newXaxisForCircle)
+				.attr("r", newRadiusForCircle)
+				.style("fill", self.colorRange(errorPercentage));
+		}
+		
+		/* private */ function deleteThreadPool(poolName) {
+			$('#THREAD_POOL_' + poolName).remove();
+		}
+		
+	}
+
+	// public methods for sorting
+	HystrixThreadPoolMonitor.prototype.sortByVolume = function() {
+		var direction = "desc";
+		if(this.sortedBy == 'rate_desc') {
+			direction = 'asc';
+		}
+		this.sortByVolumeInDirection(direction);
+	}
+
+	HystrixThreadPoolMonitor.prototype.sortByVolumeInDirection = function(direction) {
+		this.sortedBy = 'rate_' + direction;
+		$('#' + this.containerId + ' div.monitor').tsort({order: direction, attr: 'rate_value'});
+	}
+
+	HystrixThreadPoolMonitor.prototype.sortAlphabetically = function() {
+		var direction = "asc";
+		if(this.sortedBy == 'alph_asc') {
+			direction = 'desc';
+		}
+		this.sortAlphabeticalInDirection(direction);
+	}
+
+	HystrixThreadPoolMonitor.prototype.sortAlphabeticalInDirection = function(direction) {
+		this.sortedBy = 'alph_' + direction;
+		$('#' + this.containerId + ' div.monitor').tsort("p.name", {order: direction});
+	}
+
+	HystrixThreadPoolMonitor.prototype.sortByMetricInDirection = function(direction, metric) {
+		$('#' + this.containerId + ' div.monitor').tsort(metric, {order: direction});
+	}
+
+	// this method is for when new divs are added to cause the elements to be sorted to whatever the user last chose
+	HystrixThreadPoolMonitor.prototype.sortSameAsLast = function() {
+		if(this.sortedBy == 'alph_asc') {
+			this.sortAlphabeticalInDirection('asc');
+		} else if(this.sortedBy == 'alph_desc') {
+			this.sortAlphabeticalInDirection('desc');
+		} else if(this.sortedBy == 'rate_asc') {
+			this.sortByVolumeInDirection('asc');
+		} else if(this.sortedBy == 'rate_desc') {
+			this.sortByVolumeInDirection('desc');
+		} else if(this.sortedBy == 'error_asc') {
+			this.sortByErrorInDirection('asc');
+		} else if(this.sortedBy == 'error_desc') {
+			this.sortByErrorInDirection('desc');
+		} else if(this.sortedBy == 'lat90_asc') {
+			this.sortByMetricInDirection('asc', 'p90');
+		} else if(this.sortedBy == 'lat90_desc') {
+			this.sortByMetricInDirection('desc', 'p90');
+		} else if(this.sortedBy == 'lat99_asc') {
+			this.sortByMetricInDirection('asc', 'p99');
+		} else if(this.sortedBy == 'lat99_desc') {
+			this.sortByMetricInDirection('desc', 'p99');
+		} else if(this.sortedBy == 'lat995_asc') {
+			this.sortByMetricInDirection('asc', 'p995');
+		} else if(this.sortedBy == 'lat995_desc') {
+			this.sortByMetricInDirection('desc', 'p995');
+		} else if(this.sortedBy == 'latMean_asc') {
+			this.sortByMetricInDirection('asc', 'pMean');
+		} else if(this.sortedBy == 'latMean_desc') {
+			this.sortByMetricInDirection('desc', 'pMean');
+		} else if(this.sortedBy == 'latMedian_asc') {
+			this.sortByMetricInDirection('asc', 'pMedian');
+		} else if(this.sortedBy == 'latMedian_desc') {
+			this.sortByMetricInDirection('desc', 'pMedian');
+		}  
+	}
+
+	// default sort type and direction
+	this.sortedBy = 'alph_asc';
+
+
+	// a temporary home for the logger until we become more sophisticated
+	function log(message) {
+		console.log(message);
+	};
+
+	function addCommas(nStr){
+	  nStr += '';
+	  if(nStr.length <=3) {
+		  return nStr; //shortcut if we don't need commas
+	  }
+	  x = nStr.split('.');
+	  x1 = x[0];
+	  x2 = x.length > 1 ? '.' + x[1] : '';
+	  var rgx = /(\d+)(\d{3})/;
+	  while (rgx.test(x1)) {
+	    x1 = x1.replace(rgx, '$1' + ',' + '$2');
+	  }
+	  return x1 + x2;
+	}
+})(window)
+
+

+ 33 - 0
pigx-visual/pigx-monitor/src/main/resources/static/hystrix/components/hystrixThreadPool/templates/hystrixThreadPool.html

@@ -0,0 +1,33 @@
+
+	<div class="spacer"></div>
+
+	<div class="rate">
+		<a href="javascript://" title="Total Execution Rate per Second per Reporting Host" class="tooltip rate"><span class="smaller">Host: </span><span class="ratePerSecondPerHost"><%= addCommas(ratePerSecondPerHost) %></span>/s</a>
+	</div>
+	<div class="rate">	
+		<a href="javascript://" title="Total Execution Rate per Second for Cluster" class="tooltip rate"><span class="smaller">Cluster: </span><span class="ratePerSecond"><%= addCommas(ratePerSecond) %></span>/s</a>
+	</div>
+	
+	<div class="spacer"></div>
+	
+		<div class="tableRow">
+			<div class="cell header left">Active</div>
+			<div class="cell data left"><%= currentActiveCount%></div>
+			
+			<div class="cell header right">Max Active</div>
+			<div class="cell data right"><%= addCommas(rollingMaxActiveThreads)%></div>
+		</div>
+
+		<div class="tableRow">
+			<div class="cell header left">Queued</div>
+			<div class="cell data left"><%= currentQueueSize %></div>
+			<div class="cell header right">Executions</div>
+			<div class="cell data right"><%= addCommas(rollingCountThreadsExecuted)%></div>
+		</div>
+		<div class="tableRow">
+			<div class="cell header left">Pool Size</div>
+			<div class="cell data left"><%= currentPoolSize %></div>
+			<div class="cell header right">Queue Size</div>
+			<div class="cell data right"><%= propertyValue_queueSizeRejectionThreshold %></div>
+		</div>
+	

+ 34 - 0
pigx-visual/pigx-monitor/src/main/resources/static/hystrix/components/hystrixThreadPool/templates/hystrixThreadPoolContainer.html

@@ -0,0 +1,34 @@
+<div class="monitor" id="THREAD_POOL_<%= name %>" style="position:relative;">
+
+	<%
+		var displayName = name;
+		var toolTip = "";
+		if(displayName.length > 32) {
+			displayName = displayName.substring(0,4) + "..." + displayName.substring(displayName.length-20, displayName.length);
+			toolTip = "title=\"" + name + "\"";
+		}
+	%>
+	
+	<div id="chart_THREAD_POOL_<%= name %>" class="chart" style="position:absolute;top:0px;left:0; float:left; width:100%; height:100%;"></div>
+	<div style="position:absolute;top:0;width:100%;height:15px;opacity:0.8; background:white;"><p class="name" <%= toolTip %>><%= displayName %></p></div>
+	<div style="position:absolute;top:15px;; opacity:0.8; background:white; width:100%; height:95%;">
+		<div class="monitor_data"></div>
+	</div>
+	
+
+	<script>
+		<% if(typeof errorPercentage != 'undefined') { %>
+			var errorPercentageData = [<%= errorPercentage %>];
+		<% } else { %>
+			var errorPercentageData = [99];
+		<% } %>
+		var y = 200;
+		/* escape with two backslashes */
+		var vis = d3.select("#chart_THREAD_POOL_<%= name.replace(/([ !"#$%&'()*+,./:;<=>?@[\]^`{|}~])/g,'\\\\$1') %>").append("svg:svg").attr("width", "100%").attr("height", "100%");
+		/* add a circle -- we don't use the data point, we set it manually, so just passing in [1] */
+		var circle = vis.selectAll("circle").data([1]).enter().append("svg:circle");
+		/* setup the initial styling and sizing of the circle */
+		circle.style("fill", "green").attr("cx", "30%").attr("cy", "20%").attr("r", 5);
+	</script>
+	
+</div>

+ 71 - 0
pigx-visual/pigx-monitor/src/main/resources/static/hystrix/css/global.css

@@ -0,0 +1,71 @@
+@IMPORT url("resets.css");
+
+body {
+	font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
+}
+
+img, object, embed {
+	max-width: 100%;
+}
+
+img {
+	height: auto;
+}
+
+
+#header {
+	background: #FFFFFF url(../images/hystrix-logo-tagline-tiny.png) no-repeat scroll 99% 0%;
+	height: 65px;
+	margin-bottom: 5px;
+}
+
+#header h2 {
+	float:left;
+	color: black;
+	position:relative;
+	padding-left: 20px;
+	top: 26px;
+	font-size: 20px;
+	font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
+}
+
+#header .header_nav {
+	position:absolute;
+	top:48px;
+	right:15px;
+}
+
+#header .header_links {
+	float:left;
+	color: lightgray;
+	font-size: 18px;
+	top: 3px;
+	padding-left: 10px;
+	font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
+}
+
+#header .header_links a {
+	color: white;
+}
+
+#header .header_clusters {
+	float:left;
+	position:relative;
+	padding-left: 10px;
+	top: -1px;
+	font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
+}
+
+
+@media screen and (min-width: 1500px) {
+	
+	#header .header_nav {
+		top:13px;
+		right:130px;
+	}
+	
+	#header {
+		background: #FFFFFF url(../images/hystrix-logo-tagline-tiny.png) no-repeat scroll 99% 50%;
+		height: 65px;
+	}
+}

+ 105 - 0
pigx-visual/pigx-monitor/src/main/resources/static/hystrix/css/monitor.css

@@ -0,0 +1,105 @@
+.container {
+	padding-left: 20px;
+	padding-right: 20px;
+}
+
+.row {
+	width: 100%;
+	margin: 0 auto;
+	overflow: hidden;
+}
+
+.spacer {
+	width: 100%;
+	margin: 0 auto;
+	padding-top:4px;
+	clear:both;
+}
+
+
+.last {
+	margin-right: 0px;
+}
+
+.menubar {
+	overflow: hidden;
+	border-bottom: 1px solid black;
+}
+
+.menubar div {
+	padding-bottom:5px;
+
+	margin: 0 auto;
+	overflow: hidden;
+	
+	font-size: 80%;
+	font-family:'Bookman Old Style',Bookman,'URW Bookman L','Palatino Linotype',serif;
+	
+	float:left;
+}
+
+.menubar .title {
+	float: left;
+	padding-right: 20px;
+	
+	font-size: 110%;
+	font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
+	font-weight: bold;
+	
+	vertical-align: bottom;
+}
+
+.menubar .menu_actions {
+	float: left;
+	position:relative;
+	top: 4px;
+}
+
+.menubar .menu_legend {
+	float: right;
+	position:relative;
+	top: 4px;
+	
+}
+
+h3.sectionHeader {
+	color: black;
+	font-size: 110%;
+	padding-top: 4px;
+	padding-bottom: 4px;
+	padding-left: 8px;
+	font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
+	background: lightgrey;
+}
+
+.success {
+	color: green;	
+}
+.shortCircuited {
+	color: blue;	
+}
+.timeout {
+	color: #FF9900;	 /* shade of orange */
+}
+.failure {
+	color: red;	
+}
+
+.rejected {
+	color: purple;
+}
+	
+.exceptionsThrown {
+	color: brown;
+}
+
+.badRequest {
+	color: #00CC99;
+}
+
+@media screen and (max-width: 1100px) {
+	.container {
+		padding-left: 5px;
+		padding-right: 5px;
+	}
+}

+ 102 - 0
pigx-visual/pigx-monitor/src/main/resources/static/hystrix/css/resets.css

@@ -0,0 +1,102 @@
+/* 
+html5doctor.com Reset Stylesheet
+v1.6.1
+Last Updated: 2010-09-17
+Author: Richard Clark - http://richclarkdesign.com 
+Twitter: @rich_clark
+*/
+
+html, body, div, span, object, iframe,
+h1, h2, h3, h4, h5, h6, p, blockquote, pre,
+abbr, address, cite, code,
+del, dfn, em, img, ins, kbd, q, samp,
+small, strong, sub, sup, var,
+b, i,
+dl, dt, dd, ol, ul, li,
+fieldset, form, label, legend,
+table, caption, tbody, tfoot, thead, tr, th, td,
+article, aside, canvas, details, figcaption, figure, 
+footer, header, hgroup, menu, nav, section, summary,
+time, mark, audio, video {
+    margin:0;
+    padding:0;
+    border:0;
+    outline:0;
+    font-size:100%;
+    vertical-align:baseline;
+    background:transparent;
+}
+
+body {
+    line-height:1;
+}
+
+article,aside,details,figcaption,figure,
+footer,header,hgroup,menu,nav,section { 
+    display:block;
+}
+
+nav ul {
+    list-style:none;
+}
+
+blockquote, q {
+    quotes:none;
+}
+
+blockquote:before, blockquote:after,
+q:before, q:after {
+    content:'';
+    content:none;
+}
+
+a {
+    margin:0;
+    padding:0;
+    font-size:100%;
+    vertical-align:baseline;
+    background:transparent;
+}
+
+/* change colours to suit your needs */
+ins {
+    background-color:#ff9;
+    color:#000;
+    text-decoration:none;
+}
+
+/* change colours to suit your needs */
+mark {
+    background-color:#ff9;
+    color:#000; 
+    font-style:italic;
+    font-weight:bold;
+}
+
+del {
+    text-decoration: line-through;
+}
+
+abbr[title], dfn[title] {
+    border-bottom:1px dotted;
+    cursor:help;
+}
+
+table {
+    border-collapse:collapse;
+    border-spacing:0;
+}
+
+/* change border colour to suit your needs */
+hr {
+    display:block;
+    height:1px;
+    border:0;   
+    border-top:1px solid #cccccc;
+    margin:1em 0;
+    padding:0;
+}
+
+input, select {
+    vertical-align:middle;
+}

+ 21 - 0
pigx-visual/pigx-monitor/src/main/resources/static/hystrix/css/simplegrid/1236_grid.css

@@ -0,0 +1,21 @@
+/*	SimpleGrid - a fork of CSSGrid by Crowd Favorite (https://github.com/crowdfavorite/css-grid)
+ *	http://simplegrid.info
+ *	by Conor Muirhead (http://conor.cc) of Early LLC (http://earlymade.com)	
+ *  License: http://creativecommons.org/licenses/MIT/	*/
+
+/* Containers */
+body { font-size: 1.125em; }
+.grid{ width:1206px; }
+
+/* 6-Col Grid Sizes */
+.slot-0,.slot-1,.slot-2,.slot-3,.slot-4,.slot-5{ width:176px; } /* Sixths */
+.slot-0-1,.slot-1-2,.slot-2-3,.slot-3-4,.slot-4-5{ width:382px; } /* Thirds */
+.slot-0-1-2-3,.slot-1-2-3-4,.slot-2-3-4-5{ width:794px; } /* Two-Thirds */
+.slot-0-1-2-3-4,.slot-1-2-3-4-5{ width:1000px; } /* Five-Sixths */
+
+/* 4-Col Grid Sizes */
+.slot-6,.slot-7,.slot-8,.slot-9{ width:279px; } /* Quarters */
+.slot-6-7-8,.slot-7-8-9{ width:897px; } /* Three-Quarters */
+
+/* 6-Col/4-Col Shared Grid Sizes */
+.slot-0-1-2,.slot-1-2-3,.slot-2-3-4,.slot-3-4-5, .slot-6-7,.slot-7-8,.slot-8-9{ width:588px; } /* Halves */

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 33 - 0
pigx-visual/pigx-monitor/src/main/resources/static/hystrix/css/simplegrid/720_grid.css


+ 24 - 0
pigx-visual/pigx-monitor/src/main/resources/static/hystrix/css/simplegrid/986_grid.css

@@ -0,0 +1,24 @@
+/*	SimpleGrid - a fork of CSSGrid by Crowd Favorite (https://github.com/crowdfavorite/css-grid)
+ *	http://simplegrid.info
+ *	by Conor Muirhead (http://conor.cc) of Early LLC (http://earlymade.com)	
+ *  License: http://creativecommons.org/licenses/MIT/	*/
+
+/* Containers */
+body { font-size: 100%; }
+.grid{ width:966px; }
+
+/* Slots Setup */
+.slot-0,.slot-1,.slot-2,.slot-3,.slot-4,.slot-5,.slot-0-1,.slot-0-1-2,.slot-0-1-2-3,.slot-0-1-2-3-4,.slot-0-1-2-3-4-5,.slot-1-2,.slot-1-2-3,.slot-1-2-3-4,.slot-1-2-3-4-5,.slot-2-3,.slot-2-3-4,.slot-2-3-4-5,.slot-3-4,.slot-3-4-5,.slot-4-5,.slot-6,.slot-7,.slot-8,.slot-9,.slot-6-7,.slot-6-7-8,.slot-6-7-8-9,.slot-7-8,.slot-7-8-9,.slot-8-9{ display:inline; float:left; margin-left:30px; }
+
+/* 6-Col Grid Sizes */
+.slot-0,.slot-1,.slot-2,.slot-3,.slot-4,.slot-5{ width:136px; } /* Sixths */
+.slot-0-1,.slot-1-2,.slot-2-3,.slot-3-4,.slot-4-5{ width:302px; } /* Thirds */
+.slot-0-1-2-3,.slot-1-2-3-4,.slot-2-3-4-5{ width:634px; } /* Two-Thirds */
+.slot-0-1-2-3-4,.slot-1-2-3-4-5{ width:800px; } /* Five-Sixths */
+
+/* 4-Col Grid Sizes */
+.slot-6,.slot-7,.slot-8,.slot-9{ width:219px; } /* Quarters */
+.slot-6-7-8,.slot-7-8-9{ width:717px; } /* Three-Quarters */
+
+/* 6-Col/4-Col Shared Grid Sizes */
+.slot-0-1-2,.slot-1-2-3,.slot-2-3-4,.slot-3-4-5, .slot-6-7,.slot-7-8,.slot-8-9{ width:468px; } /* Halves */

+ 19 - 0
pigx-visual/pigx-monitor/src/main/resources/static/hystrix/css/simplegrid/LICENSE.txt

@@ -0,0 +1,19 @@
+Copyright (c) 2011 Crowd Favorite, Ltd.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 1 - 0
pigx-visual/pigx-monitor/src/main/resources/static/hystrix/css/simplegrid/README.txt

@@ -0,0 +1 @@
+http://simplegrid.info/

+ 27 - 0
pigx-visual/pigx-monitor/src/main/resources/static/hystrix/css/simplegrid/percentage_grid.css

@@ -0,0 +1,27 @@
+/* Extension of SimpleGrid by benjchristensen to allow percentage based sizing on very large displays	
+ *
+ * SimpleGrid - a fork of CSSGrid by Crowd Favorite (https://github.com/crowdfavorite/css-grid)
+ *	http://simplegrid.info
+ *	by Conor Muirhead (http://conor.cc) of Early LLC (http://earlymade.com)	
+ *  License: http://creativecommons.org/licenses/MIT/	*/
+
+/* Containers */
+body { font-size: 1.125em; }
+.grid{ width:100%; }
+
+/* Slots Setup */
+.slot-0,.slot-1,.slot-2,.slot-3,.slot-4,.slot-5,.slot-0-1,.slot-0-1-2,.slot-0-1-2-3,.slot-0-1-2-3-4,.slot-0-1-2-3-4-5,.slot-1-2,.slot-1-2-3,.slot-1-2-3-4,.slot-1-2-3-4-5,.slot-2-3,.slot-2-3-4,.slot-2-3-4-5,.slot-3-4,.slot-3-4-5,.slot-4-5,.slot-6,.slot-7,.slot-8,.slot-9,.slot-6-7,.slot-6-7-8,.slot-6-7-8-9,.slot-7-8,.slot-7-8-9,.slot-8-9{ display:inline; float:left; margin-left:0px; }
+
+
+/* 6-Col Grid Sizes */
+.slot-0,.slot-1,.slot-2,.slot-3,.slot-4,.slot-5{ width:16.6%; } /* Sixths */
+.slot-0-1,.slot-1-2,.slot-2-3,.slot-3-4,.slot-4-5{ width:33.3%; } /* Thirds */
+.slot-0-1-2-3,.slot-1-2-3-4,.slot-2-3-4-5{ width:66.6%; } /* Two-Thirds */
+.slot-0-1-2-3-4,.slot-1-2-3-4-5{ width:83.3%; } /* Five-Sixths */
+
+/* 4-Col Grid Sizes */
+.slot-6,.slot-7,.slot-8,.slot-9{ width:25%; } /* Quarters */
+.slot-6-7-8,.slot-7-8-9{ width:75%; } /* Three-Quarters */
+
+/* 6-Col/4-Col Shared Grid Sizes */
+.slot-0-1-2,.slot-1-2-3,.slot-2-3-4,.slot-3-4-5, .slot-6-7,.slot-7-8,.slot-8-9{ width:50%; } /* Halves */

BIN
pigx-visual/pigx-monitor/src/main/resources/static/hystrix/images/hystrix-logo-tagline-tiny.png


BIN
pigx-visual/pigx-monitor/src/main/resources/static/hystrix/images/hystrix-logo.png


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 12 - 0
pigx-visual/pigx-monitor/src/main/resources/static/hystrix/js/jquery.tinysort.min.js


+ 43 - 0
pigx-visual/pigx-monitor/src/main/resources/static/hystrix/js/tmpl.js

@@ -0,0 +1,43 @@
+
+//Simple JavaScript Templating
+//John Resig - http://ejohn.org/ - MIT Licensed
+// http://ejohn.org/blog/javascript-micro-templating/
+(function(window, undefined) {
+  var cache = {};
+
+  window.tmpl = function tmpl(str, data) {
+      try {
+          // Figure out if we're getting a template, or if we need to
+          // load the template - and be sure to cache the result.
+          var fn = !/\W/.test(str) ?
+          cache[str] = cache[str] ||
+          tmpl(document.getElementById(str).innerHTML) :
+    
+          // Generate a reusable function that will serve as a template
+          // generator (and which will be cached).
+          new Function("obj",
+          "var p=[],print=function(){p.push.apply(p,arguments);};" +
+    
+          // Introduce the data as local variables using with(){}
+          "with(obj){p.push('" +
+    
+          // Convert the template into pure JavaScript
+          str
+          .replace(/[\r\t\n]/g, " ")
+          .split("<%").join("\t")
+          .replace(/((^|%>)[^\t]*)'/g, "$1\r")
+          .replace(/\t=(.*?)%>/g, "',$1,'")
+          .split("\t").join("');")
+          .split("%>").join("p.push('")
+          .split("\r").join("\\'")
+          + "');}return p.join('');");
+    
+          //console.log(fn);
+          
+          // Provide some basic currying to the user
+          return data ? fn(data) : fn;
+      }catch(e) {
+          console.log(e);
+      }
+  };
+})(window);

+ 58 - 0
pigx-visual/pigx-monitor/src/main/resources/templates/hystrix/index.ftl

@@ -0,0 +1,58 @@
+<#import "/spring.ftl" as spring />
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
+<html>
+<head>
+  <base href="${basePath}">
+  <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
+  <title>Hystrix Dashboard</title>
+
+  <!-- Javascript to monitor and display -->
+  <script src="<@spring.url '/webjars/jquery/2.1.1/jquery.min.js'/>" type="text/javascript"></script>
+
+  <script>
+    function sendToMonitor() {
+
+      if($('#stream').val().length > 0) {
+        var url = "<@spring.url '/hystrix'/>/monitor?stream=" + encodeURIComponent($('#stream').val()) + "";
+        if($('#delay').val().length > 0) {
+          url += "&delay=" + $('#delay').val();
+        }
+        if($('#title').val().length > 0) {
+          url += "&title=" + encodeURIComponent($('#title').val());
+        }
+        location.href= url;
+      } else {
+        $('#message').html("The 'stream' value is required.");
+      }
+    }
+  </script>
+</head>
+<body>
+<div style="width:800px;margin:0 auto;">
+
+  <center>
+    <img width="264" height="233" src="<@spring.url '/hystrix'/>/images/hystrix-logo.png">
+    <br>
+    <br>
+
+    <h2>Hystrix Dashboard</h2>
+    <input id="stream" type="textfield" size="120" placeholder="http://hostname:port/turbine/turbine.stream"></input>
+    <br><br>
+    <i>Cluster via Turbine (default cluster):</i> http://turbine-hostname:port/turbine.stream
+    <br>
+    <i>Cluster via Turbine (custom cluster):</i> http://turbine-hostname:port/turbine.stream?cluster=[clusterName]
+    <br>
+    <i>Single Hystrix App:</i> http://hystrix-app:port/actuator/hystrix.stream
+    <br><br>
+    Delay: <input id="delay" type="textfield" size="10" placeholder="2000"></input>ms
+    &nbsp;&nbsp;&nbsp;&nbsp;
+    Title: <input id="title" type="textfield" size="60" placeholder="Example Hystrix App"></input><br>
+    <br>
+    <button onclick="sendToMonitor()">Monitor Stream</button>
+    <br><br>
+    <div id="message" style="color:red"></div>
+
+  </center>
+</div>
+</body>
+</html>

+ 202 - 0
pigx-visual/pigx-monitor/src/main/resources/templates/hystrix/monitor.ftl

@@ -0,0 +1,202 @@
+<#import "/spring.ftl" as spring />
+<!doctype html>
+<html lang="en">
+<head>
+  <base href="${basePath}">
+  <meta charset="utf-8" />
+  <title>Hystrix Monitor</title>
+  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+
+  <!-- Setup base for everything -->
+  <link rel="stylesheet" type="text/css" href="css/global.css" />
+
+  <!-- Our custom CSS -->
+  <link rel="stylesheet" type="text/css" href="css/monitor.css" />
+
+  <!-- d3 -->
+  <script type="text/javascript" src="<@spring.url '/webjars/d3js/3.4.11/d3.min.js'/>" ></script>
+
+  <!-- Javascript to monitor and display -->
+  <script type="text/javascript" src="<@spring.url '/webjars/jquery/2.1.1/jquery.min.js'/>" ></script>
+  <script type="text/javascript" src="js/jquery.tinysort.min.js"></script>
+  <script type="text/javascript" src="js/tmpl.js"></script>
+
+  <!-- HystrixCommand -->
+  <script type="text/javascript" src="components/hystrixCommand/hystrixCommand.js"></script>
+  <link rel="stylesheet" type="text/css" href="components/hystrixCommand/hystrixCommand.css" />
+
+  <!-- HystrixThreadPool -->
+  <script type="text/javascript" src="components/hystrixThreadPool/hystrixThreadPool.js"></script>
+  <link rel="stylesheet" type="text/css" href="components/hystrixThreadPool/hystrixThreadPool.css" />
+
+</head>
+<body>
+<div id="header">
+  <h2><span id="title_name"></span></h2>
+</div>
+
+<div class="container">
+  <div class="row">
+    <div class="menubar">
+      <div class="title">
+        Circuit
+      </div>
+      <div class="menu_actions">
+        Sort:
+        <a href="javascript://" onclick="hystrixMonitor.sortByErrorThenVolume();">Error then Volume</a> |
+        <a href="javascript://" onclick="hystrixMonitor.sortAlphabetically();">Alphabetical</a> |
+        <a href="javascript://" onclick="hystrixMonitor.sortByVolume();">Volume</a> |
+        <a href="javascript://" onclick="hystrixMonitor.sortByError();">Error</a> |
+        <a href="javascript://" onclick="hystrixMonitor.sortByLatencyMean();">Mean</a> |
+        <a href="javascript://" onclick="hystrixMonitor.sortByLatencyMedian();">Median</a> |
+        <a href="javascript://" onclick="hystrixMonitor.sortByLatency90();">90</a> |
+        <a href="javascript://" onclick="hystrixMonitor.sortByLatency99();">99</a> |
+        <a href="javascript://" onclick="hystrixMonitor.sortByLatency995();">99.5</a>
+      </div>
+      <div class="menu_legend">
+        <span class="success">Success</span> | <span class="shortCircuited">Short-Circuited</span> | <span class="badRequest"> Bad Request</span> | <span class="timeout">Timeout</span> | <span class="rejected">Rejected</span> | <span class="failure">Failure</span> | <span class="errorPercentage">Error %</span>
+      </div>
+    </div>
+  </div>
+  <div id="dependencies" class="row dependencies"><span class="loading">Loading ...</span></div>
+
+  <div class="spacer"></div>
+
+  <div class="row">
+    <div class="menubar">
+      <div class="title">
+        Thread Pools
+      </div>
+      <div class="menu_actions">
+        Sort: <a href="javascript://" onclick="dependencyThreadPoolMonitor.sortAlphabetically();">Alphabetical</a> |
+        <a href="javascript://" onclick="dependencyThreadPoolMonitor.sortByVolume();">Volume</a> |
+      </div>
+    </div>
+  </div>
+  <div id="dependencyThreadPools" class="row dependencyThreadPools"><span class="loading">Loading ...</span></div>
+</div>
+
+
+
+<script>
+  /**
+   * Queue up the monitor to start once the page has finished loading.
+   *
+   * This is an inline script and expects to execute once on page load.
+   */
+
+    // commands
+  var hystrixMonitor = new HystrixCommandMonitor('dependencies', {includeDetailIcon:false});
+
+  var stream = getUrlVars()["stream"];
+
+  console.log("Stream: " + stream)
+
+  if(stream != undefined) {
+    if(getUrlVars()["delay"] != undefined) {
+      stream = stream + "&delay=" + getUrlVars()["delay"];
+    }
+
+    var commandStream = "${contextPath}/proxy.stream?origin=" + stream;
+    var poolStream = "${contextPath}/proxy.stream?origin=" + stream;
+
+    if(getUrlVars()["title"] != undefined) {
+      $('#title_name').text("Hystrix Stream: " + decodeURIComponent(getUrlVars()["title"]))
+    } else {
+      $('#title_name').text("Hystrix Stream: " + decodeURIComponent(stream))
+    }
+  }
+  console.log("Command Stream: " + commandStream)
+
+  $(window).load(function() { // within load with a setTimeout to prevent the infinite spinner
+    setTimeout(function() {
+      if(commandStream == undefined) {
+        console.log("commandStream is undefined")
+        $("#dependencies .loading").html("The 'stream' argument was not provided.");
+        $("#dependencies .loading").addClass("failed");
+      } else {
+        // sort by error+volume by default
+        hystrixMonitor.sortByErrorThenVolume();
+
+        // start the EventSource which will open a streaming connection to the server
+        var source = new EventSource(commandStream);
+
+        // add the listener that will process incoming events
+        source.addEventListener('message', hystrixMonitor.eventSourceMessageListener, false);
+
+        //	source.addEventListener('open', function(e) {
+        //		console.console.log(">>> opened connection, phase: " + e.eventPhase);
+        //	    // Connection was opened.
+        //	}, false);
+
+        source.addEventListener('error', function(e) {
+          $("#dependencies .loading").html("Unable to connect to Command Metric Stream.");
+          $("#dependencies .loading").addClass("failed");
+          if (e.eventPhase == EventSource.CLOSED) {
+            // Connection was closed.
+            console.log("Connection was closed on error: " + JSON.stringify(e));
+          } else {
+            console.log("Error occurred while streaming: " + JSON.stringify(e));
+          }
+        }, false);
+      }
+    },0);
+  });
+
+  // thread pool
+  var dependencyThreadPoolMonitor = new HystrixThreadPoolMonitor('dependencyThreadPools');
+
+  $(window).load(function() { // within load with a setTimeout to prevent the infinite spinner
+    setTimeout(function() {
+      if(poolStream == undefined) {
+        console.log("poolStream is undefined")
+        $("#dependencyThreadPools .loading").html("The 'stream' argument was not provided.");
+        $("#dependencyThreadPools .loading").addClass("failed");
+      } else {
+        dependencyThreadPoolMonitor.sortByVolume();
+
+        // start the EventSource which will open a streaming connection to the server
+        var source = new EventSource(poolStream);
+
+        // add the listener that will process incoming events
+        source.addEventListener('message', dependencyThreadPoolMonitor.eventSourceMessageListener, false);
+
+        //	source.addEventListener('open', function(e) {
+        //		console.console.log(">>> opened connection, phase: " + e.eventPhase);
+        //	    // Connection was opened.
+        //	}, false);
+
+        source.addEventListener('error', function(e) {
+          $("#dependencies .loading").html("Unable to connect to Command Metric Stream.");
+          $("#dependencies .loading").addClass("failed");
+          if (e.eventPhase == EventSource.CLOSED) {
+            // Connection was closed.
+            console.log("Connection was closed on error: " + e);
+          } else {
+            console.log("Error occurred while streaming: " + e);
+          }
+        }, false);
+      }
+    },0);
+  });
+
+  //Read a page's GET URL variables and return them as an associative array.
+  // from: http://jquery-howto.blogspot.com/2009/09/get-url-parameters-values-with-jquery.html
+  function getUrlVars()
+  {
+    var vars = [], hash;
+    var hashes = window.location.href.slice(window.location.href.indexOf('?') + 1).split('&');
+    for(var i = 0; i < hashes.length; i++)
+    {
+      hash = hashes[i].split('=');
+      vars.push(hash[0]);
+      vars[hash[0]] = hash[1];
+    }
+    return vars;
+  }
+
+</script>
+
+
+</body>
+</html>

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 394 - 0
pigx-visual/pigx-monitor/src/main/resources/ui/assets/css/sba-core.css


BIN
pigx-visual/pigx-monitor/src/main/resources/ui/assets/img/favicon-danger.png


BIN
pigx-visual/pigx-monitor/src/main/resources/ui/assets/img/favicon.png


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 9 - 0
pigx-visual/pigx-monitor/src/main/resources/ui/assets/img/icon-spring-boot-admin.svg


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 7 - 0
pigx-visual/pigx-monitor/src/main/resources/ui/assets/js/event-source-polyfill.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 0
pigx-visual/pigx-monitor/src/main/resources/ui/assets/js/event-source-polyfill.js.map


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 2 - 0
pigx-visual/pigx-monitor/src/main/resources/ui/assets/js/sba-core.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 0
pigx-visual/pigx-monitor/src/main/resources/ui/assets/js/sba-core.js.map


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 17 - 0
pigx-visual/pigx-monitor/src/main/resources/ui/assets/js/vendors.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 0
pigx-visual/pigx-monitor/src/main/resources/ui/assets/js/vendors.js.map


+ 70 - 0
pigx-visual/pigx-monitor/src/main/resources/ui/index.html

@@ -0,0 +1,70 @@
+<!--
+  ~ Copyright 2014-2018 the original author or authors.
+  ~
+  ~ Licensed 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.
+  -->
+
+<!DOCTYPE html>
+
+<html>
+<head>
+    <base th:href="@{${adminContextPath} + '/'}" href="/">
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <meta name="format-detection" content="telephone=no,email=no">
+    <meta name="theme-color" content="#42d3a5">
+
+    <link rel="preload" href="assets/css/sba-core.css" as="style">
+    <th:block th:each="cssFile : ${cssExtensions}">
+        <link rel="preload" th:href="${cssFile.resourcePath}" as="style">
+    </th:block>
+    <link rel="preload" href="assets/js/vendors.js" as="script">
+    <th:block th:each="jsFile : ${jsExtensions}">
+        <link rel="preload" th:href="${jsFile.resourcePath}" as="script">
+    </th:block>
+    <link rel="preload" href="assets/js/sba-core.js" as="script">
+
+    <link href="assets/css/sba-core.css" rel="stylesheet">
+    <th:block th:each="cssFile : ${cssExtensions}">
+        <link th:href="${cssFile.resourcePath}" rel="stylesheet">
+    </th:block>
+    <link rel="shortcut icon" href="assets/img/favicon.png" type="image/png">
+    <title th:text="${uiSettings.title}">Spring Boot Admin</title>
+</head>
+<body>
+
+<div id="app"></div>
+
+<script th:inline="javascript">
+    var SBA = {
+        uiSettings: /*[[${uiSettings}]]*/ {},
+        user: /*[[${user}]]*/ null,
+        extensions: [],
+        /*[#th:block th:if="${_csrf}"]*/
+        csrf: {
+            parameterName: /*[[${_csrf.parameterName}]]*/ null,
+            headerName: /*[[${_csrf.headerName}]]*/ null
+        },
+        /*[/th:block]*/
+        use: function (ext) {
+            this.extensions.push(ext);
+        }
+    }
+</script>
+<script lang="javascript" src="assets/js/vendors.js" defer></script>
+<th:block th:each="jsFile : ${jsExtensions}">
+    <script lang="javascript" th:src="${jsFile.resourcePath}" defer></script>
+</th:block>
+<script lang="javascript" src="assets/js/sba-core.js" defer></script>
+</body>
+</html>

+ 80 - 0
pigx-visual/pigx-monitor/src/main/resources/ui/login.html

@@ -0,0 +1,80 @@
+<!DOCTYPE html>
+<!--
+  ~ Copyright 2014-2018 the original author or authors.
+  ~
+  ~ Licensed 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.
+  -->
+
+<html>
+<head>
+    <base th:href="@{${adminContextPath} + '/'}" href="/">
+    <meta charset="utf-8">
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+    <meta name="format-detection" content="telephone=no,email=no">
+    <meta name="theme-color" content="#42d3a5">
+    <link rel="shortcut icon" href="assets/img/favicon.png" type="image/png">
+    <link rel="preload" href="assets/css/sba-core.css" as="style">
+    <link href="assets/css/sba-core.css" rel="stylesheet">
+    <title>Spring Boot Admin - Login</title>
+</head>
+<body class="login">
+
+<section class="hero is-fullheight">
+    <div class="hero-body">
+        <div class="container has-text-centered">
+            <div class="column is-4 is-offset-4">
+                <div class="box">
+                    <figure class="image is-128x128 login--logo">
+                        <img src="assets/img/icon-spring-boot-admin.svg">
+                    </figure>
+                    <h1 class="title has-text-primary">Spring Boot Admin</h1>
+                    <form method="post">
+                        <input type="hidden"
+                               th:if="${_csrf}"
+                               th:name="${_csrf.parameterName}"
+                               th:value="${_csrf.token}"/>
+                        <div class="field">
+                            <p class="is-medium has-text-danger" th:unless="${param.error == null}">
+                                Invalid username or password
+                            </p>
+                            <p class="is-medium" th:unless="${param.logout == null}">
+                                Logout successful
+                            </p>
+                        </div>
+                        <div class="field">
+                            <div class="control">
+                                <input class="input is-medium" type="input" name="username" placeholder="Username"
+                                       autofocus th:classappend="${param.error != null} ? is-danger">
+                            </div>
+                        </div>
+                        <div class="field">
+                            <div class="control">
+                                <input class="input is-medium" type="password" name="password"
+                                       placeholder="Password" th:classappend="${param.error != null} ? is-danger">
+                            </div>
+                        </div>
+                        <div class="field">
+                            <div class="control">
+                                <input type="submit" class="button is-block is-primary is-medium is-fullwidth"
+                                       value="Login">
+                            </div>
+                        </div>
+                    </form>
+                </div>
+            </div>
+        </div>
+    </div>
+</section>
+
+</body>
+</html>