mirror of
				https://github.com/ohwgiles/laminar.git
				synced 2025-06-13 12:54:29 +00:00 
			
		
		
		
	frontend love
This commit is contained in:
		
							parent
							
								
									0df97e95fd
								
							
						
					
					
						commit
						1e0a2ebc36
					
				| @ -57,7 +57,7 @@ add_custom_command(OUTPUT laminar.capnp.c++ laminar.capnp.h | ||||
| 
 | ||||
| # Zip and compile statically served resources | ||||
| generate_compressed_bins(${CMAKE_SOURCE_DIR}/src/resources index.html js/app.js | ||||
|     tpl/home.html tpl/job.html tpl/run.html tpl/log.html tpl/browse.html | ||||
|     tpl/home.html tpl/job.html tpl/run.html tpl/browse.html | ||||
|     favicon.ico favicon-152.png icon.png) | ||||
| # Download 3rd-party frontend JS libs... | ||||
| file(DOWNLOAD https://ajax.googleapis.com/ajax/libs/angularjs/1.3.14/angular.min.js | ||||
|  | ||||
| @ -8,6 +8,13 @@ | ||||
| ### | ||||
| #LAMINAR_HOME=/var/lib/laminar | ||||
| 
 | ||||
| ### | ||||
| ### LAMINAR_TITLE | ||||
| ### | ||||
| ### Page title to show in web frontend | ||||
| ### | ||||
| #LAMINAR_TITLE= | ||||
| 
 | ||||
| ### | ||||
| ### LAMINAR_KEEP_WORKDIR | ||||
| ### | ||||
|  | ||||
							
								
								
									
										117
									
								
								src/laminar.cpp
									
									
									
									
									
								
							
							
						
						
									
										117
									
								
								src/laminar.cpp
									
									
									
									
									
								
							| @ -118,48 +118,52 @@ void Laminar::sendStatus(LaminarClient* client) { | ||||
|                 client->sendMessage(log); | ||||
|             }); | ||||
|         } | ||||
|     } else if(client->scope.type == MonitorScope::RUN) { | ||||
|         Json j; | ||||
|         j.set("type", "status"); | ||||
|         j.startObject("data"); | ||||
|         db->stmt("SELECT startedAt, result, reason FROM builds WHERE name = ? AND number = ?") | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     Json j; | ||||
|     j.set("type", "status"); | ||||
|     j.startObject("data"); | ||||
|     j.set("title", getenv("LAMINAR_TITLE") ?: ""); | ||||
|     if(client->scope.type == MonitorScope::RUN) { | ||||
|         db->stmt("SELECT queuedAt,startedAt,completedAt, result, reason FROM builds WHERE name = ? AND number = ?") | ||||
|         .bind(client->scope.job, client->scope.num) | ||||
|         .fetch<time_t, int, std::string>([&](time_t started, int result, std::string reason) { | ||||
|         .fetch<time_t, time_t, time_t, int, std::string>([&](time_t queued, time_t started, time_t completed, int result, std::string reason) { | ||||
|             j.set("queued", started-queued); | ||||
|             j.set("started", started); | ||||
|             j.set("completed", completed); | ||||
|             j.set("duration", completed-started); | ||||
|             j.set("result", to_string(RunState(result))); | ||||
|             j.set("reason", reason); | ||||
|         }); | ||||
|         j.set("latestNum", int(buildNums[client->scope.job])); | ||||
|         j.startArray("artifacts"); | ||||
|         fs::path dir(fs::path(homeDir)/"archive"/client->scope.job/std::to_string(client->scope.num)); | ||||
|         fs::recursive_directory_iterator rdt(dir); | ||||
|         int prefixLen = (fs::path(homeDir)/"archive").string().length(); | ||||
|         int scopeLen = dir.string().length(); | ||||
|         for(fs::directory_entry e : rdt) { | ||||
|             if(!fs::is_regular_file(e)) | ||||
|                 continue; | ||||
|             j.StartObject(); | ||||
|             j.set("url", archiveUrl + e.path().string().substr(prefixLen)); | ||||
|             j.set("filename", e.path().string().substr(scopeLen+1)); | ||||
|             j.EndObject(); | ||||
|         if(fs::is_directory(dir)) { | ||||
|             fs::recursive_directory_iterator rdt(dir); | ||||
|             int prefixLen = (fs::path(homeDir)/"archive").string().length(); | ||||
|             int scopeLen = dir.string().length(); | ||||
|             for(fs::directory_entry e : rdt) { | ||||
|                 if(!fs::is_regular_file(e)) | ||||
|                     continue; | ||||
|                 j.StartObject(); | ||||
|                 j.set("url", archiveUrl + e.path().string().substr(prefixLen)); | ||||
|                 j.set("filename", e.path().string().substr(scopeLen+1)); | ||||
|                 j.EndObject(); | ||||
|             } | ||||
|         } | ||||
|         j.EndArray(); | ||||
|         j.EndObject(); | ||||
|         client->sendMessage(j.str()); | ||||
|     } else if(client->scope.type == MonitorScope::JOB) { | ||||
|         Json j; | ||||
|         j.set("type", "status"); | ||||
|         j.startObject("data"); | ||||
|         j.startArray("recent"); | ||||
|         db->stmt("SELECT * FROM builds WHERE name = ? ORDER BY completedAt DESC LIMIT 5") | ||||
|         db->stmt("SELECT number,startedAt,completedAt,result,reason FROM builds WHERE name = ? ORDER BY completedAt DESC LIMIT 25") | ||||
|         .bind(client->scope.job) | ||||
|         .fetch<str,int,str,time_t,time_t,time_t,int>([&](str name,int build,str node,time_t,time_t started,time_t completed,int result){ | ||||
|         .fetch<int,time_t,time_t,int,str>([&](int build,time_t started,time_t completed,int result,str reason){ | ||||
|             j.StartObject(); | ||||
|             j.set("name", name) | ||||
|              .set("number", build) | ||||
|              .set("node", node) | ||||
|             j.set("number", build) | ||||
|              .set("duration", completed - started) | ||||
|              .set("started", started) | ||||
|              .set("result", to_string(RunState(result))) | ||||
|              .set("reason", reason) | ||||
|              .EndObject(); | ||||
|         }); | ||||
|         j.EndArray(); | ||||
| @ -168,45 +172,52 @@ void Laminar::sendStatus(LaminarClient* client) { | ||||
|         for(auto it = p.first; it != p.second; ++it) { | ||||
|             const std::shared_ptr<Run> run = *it; | ||||
|             j.StartObject(); | ||||
|             j.set("name", run->name); | ||||
|             j.set("number", run->build); | ||||
|             j.set("node", run->node->name); | ||||
|             j.set("started", run->startedAt); | ||||
|             j.EndObject(); | ||||
|         } | ||||
|         j.EndArray(); | ||||
|         j.startArray("queued"); | ||||
|         int nQueued = 0; | ||||
|         for(const std::shared_ptr<Run> run : queuedJobs) { | ||||
|             if (run->name == client->scope.job) { | ||||
|                 j.StartObject(); | ||||
|                 j.set("name", run->name); | ||||
|                 j.EndObject(); | ||||
|                 nQueued++; | ||||
|             } | ||||
|         } | ||||
|         j.EndArray(); | ||||
|         j.EndObject(); | ||||
|         client->sendMessage(j.str()); | ||||
|         j.set("nQueued", nQueued); | ||||
|         db->stmt("SELECT number,startedAt FROM builds WHERE name = ? AND result = ? ORDER BY completedAt DESC LIMIT 1") | ||||
|         .bind(client->scope.job, int(RunState::SUCCESS)) | ||||
|         .fetch<int,time_t>([&](int build, time_t started){ | ||||
|             j.startObject("lastSuccess"); | ||||
|             j.set("number", build).set("started", started); | ||||
|             j.EndObject(); | ||||
|         }); | ||||
|         db->stmt("SELECT number,startedAt FROM builds WHERE name = ? AND result <> ? ORDER BY completedAt DESC LIMIT 1") | ||||
|         .bind(client->scope.job, int(RunState::SUCCESS)) | ||||
|         .fetch<int,time_t>([&](int build, time_t started){ | ||||
|             j.startObject("lastFailed"); | ||||
|             j.set("number", build).set("started", started); | ||||
|             j.EndObject(); | ||||
|         }); | ||||
| 
 | ||||
|     } else if(client->scope.type == MonitorScope::ALL) { | ||||
|         Json j; | ||||
|         j.set("type", "status"); | ||||
|         j.startObject("data"); | ||||
|         j.startArray("jobs"); | ||||
|         db->stmt("SELECT name FROM builds GROUP BY name") | ||||
|         .fetch<str>([&](str name){ | ||||
|         db->stmt("SELECT name,number,startedAt,result FROM builds GROUP BY name ORDER BY number DESC") | ||||
|         .fetch<str,int,time_t,int>([&](str name,int number, time_t started, int result){ | ||||
|             j.StartObject(); | ||||
|             j.set("name", name) | ||||
|              .EndObject(); | ||||
|             j.set("name", name); | ||||
|             j.set("number", number).set("result", to_string(RunState(result))).set("started", started); | ||||
|             j.startArray("tags"); | ||||
|             for(const str& t: jobTags[name]) { | ||||
|                 j.String(t.c_str()); | ||||
|             } | ||||
|             j.EndArray(); | ||||
|             j.EndObject(); | ||||
|         }); | ||||
|         j.EndArray(); | ||||
|         j.EndObject(); | ||||
|         client->sendMessage(j.str()); | ||||
|     } else { // Home page
 | ||||
|         Json j; | ||||
|         j.set("type", "status"); | ||||
|         j.startObject("data"); | ||||
|         j.startArray("recent"); | ||||
|         db->stmt("SELECT * FROM builds ORDER BY completedAt DESC LIMIT 5") | ||||
|         db->stmt("SELECT * FROM builds ORDER BY completedAt DESC LIMIT 15") | ||||
|         .fetch<str,int,str,time_t,time_t,time_t,int>([&](str name,int build,str node,time_t,time_t started,time_t completed,int result){ | ||||
|             j.StartObject(); | ||||
|             j.set("name", name) | ||||
| @ -262,10 +273,18 @@ void Laminar::sendStatus(LaminarClient* client) { | ||||
|             j.set(job.c_str(), count); | ||||
|         }); | ||||
|         j.EndObject(); | ||||
| 
 | ||||
|         j.startObject("timePerJob"); | ||||
|         db->stmt("SELECT name, AVG(completedAt-startedAt) FROM builds WHERE completedAt > ? GROUP BY name") | ||||
|                 .bind(time(0) - 7 * 86400) | ||||
|                 .fetch<str, int>([&](str job, int time){ | ||||
|             j.set(job.c_str(), time); | ||||
|         }); | ||||
|         j.EndObject(); | ||||
|         client->sendMessage(j.str()); | ||||
| 
 | ||||
|     } | ||||
|     j.EndObject(); | ||||
|     client->sendMessage(j.str()); | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| Laminar::~Laminar() { | ||||
|  | ||||
| @ -37,7 +37,6 @@ Resources::Resources() | ||||
|     INIT_RESOURCE("/tpl/home.html", tpl_home_html); | ||||
|     INIT_RESOURCE("/tpl/job.html", tpl_job_html); | ||||
|     INIT_RESOURCE("/tpl/run.html", tpl_run_html); | ||||
|     INIT_RESOURCE("/tpl/log.html", tpl_log_html); | ||||
|     INIT_RESOURCE("/tpl/browse.html", tpl_browse_html); | ||||
|     INIT_RESOURCE("/js/angular.min.js", js_angular_min_js); | ||||
|     INIT_RESOURCE("/js/angular-route.min.js", js_angular_route_min_js); | ||||
|  | ||||
| @ -17,11 +17,14 @@ | ||||
|  <script src="/js/app.js"></script> | ||||
|  <style> | ||||
|   body, html { height: 100%; } | ||||
|   .navbar { margin-bottom: 0; } | ||||
|   .navbar-brand { margin: 0 -15px; padding: 7px 15px } | ||||
|   .navbar-brand>img { display: inline; } | ||||
|   a.navbar-btn { color: #9d9d9d; } | ||||
|   a.navbar-btn.active { color: #fff; } | ||||
|   a.navbar-btn:hover { color: #fff; text-decoration: none; } | ||||
|   a.navbar-btn:focus { color: #fff; } | ||||
|   dt,dd { line-height: 2; } | ||||
|   canvas { | ||||
|    width: 100% !important; | ||||
|    max-width: 800px; | ||||
| @ -37,11 +40,15 @@ | ||||
|  <nav class="navbar navbar-inverse"> | ||||
|   <div class="container-fluid"> | ||||
|    <div> | ||||
|     <a class="navbar-brand" href="/"><img src="/icon.png"></a> | ||||
|     <a class="btn navbar-btn" href="/jobs">Jobs</a> | ||||
|     <a class="navbar-brand" href="/"><img src="/icon.png">{{title}}</a> | ||||
|     <a class="btn navbar-btn pull-right" href="/jobs">Jobs</a> | ||||
|    </div> | ||||
|   </div> | ||||
|  </nav> | ||||
|  <ol class="breadcrumb"> | ||||
|   <li ng-repeat="n in bc.nodes track by $index"><a href="{{n.href}}">{{n.label}}</a></li> | ||||
|   <li class="active">{{bc.current}}</li> | ||||
|  </ol> | ||||
|  <div ng-view></div> | ||||
| </body> | ||||
| </html> | ||||
|  | ||||
| @ -1,13 +1,3 @@ | ||||
| Laminar = { | ||||
| 	runIcon: function(result) { | ||||
| 		return result === "success" ? '<span style="color:forestgreen">✔</span>' : '<span style="color:crimson;">✘</span>'; | ||||
| 	}, | ||||
| 	jobFormatter: function(o) { | ||||
| 		o.duration = o.duration + "s" | ||||
| 		o.when = (new Date(1000 * o.started)).toLocaleString(); | ||||
| 		return o; | ||||
| 	} | ||||
| }; | ||||
| angular.module('laminar',['ngRoute','ngSanitize']) | ||||
| .config(function($routeProvider, $locationProvider, $sceProvider) { | ||||
| 	$routeProvider | ||||
| @ -27,10 +17,6 @@ angular.module('laminar',['ngRoute','ngSanitize']) | ||||
| 		templateUrl: 'tpl/run.html', | ||||
| 		controller: 'RunController' | ||||
| 	}) | ||||
| 	.when('/jobs/:name/:num/log', { | ||||
| 		templateUrl: 'tpl/log.html', | ||||
| 		controller: 'LogController' | ||||
| 	}) | ||||
| 	$locationProvider.html5Mode(true); | ||||
| 	$sceProvider.enabled(false); | ||||
| }) | ||||
| @ -44,19 +30,23 @@ angular.module('laminar',['ngRoute','ngSanitize']) | ||||
| 			}; | ||||
| 		}, | ||||
| 		logListener: function(callback) { | ||||
| 			var ws = new WebSocket("ws://" + location.host + $location.path()); | ||||
| 			var ws = new WebSocket("ws://" + location.host + $location.path() + '/log'); | ||||
| 			ws.onmessage = function(message) { | ||||
| 				callback(message.data); | ||||
| 			}; | ||||
| 		} | ||||
| 	}; | ||||
| }) | ||||
| .controller('mainController', function($scope, $ws, $interval){ | ||||
| .controller('mainController', function($rootScope, $scope, $ws, $interval){ | ||||
| 	$rootScope.bc = { | ||||
| 		nodes: [], | ||||
| 		current: 'Home' | ||||
| 	}; | ||||
| 
 | ||||
| 	$scope.jobsQueued = []; | ||||
| 	$scope.jobsRunning = []; | ||||
| 	$scope.jobsRecent = []; | ||||
| 		 | ||||
| 	var chtUtilization, chtBuildsPerDay, chtBuildsPerJob; | ||||
| 	var chtUtilization, chtBuildsPerDay, chtBuildsPerJob, chtTimePerJob; | ||||
| 	 | ||||
| 	var updateUtilization = function(busy) { | ||||
| 		chtUtilization.segments[0].value += busy ? 1 : -1; | ||||
| @ -66,16 +56,18 @@ angular.module('laminar',['ngRoute','ngSanitize']) | ||||
| 			 | ||||
| 	$ws.statusListener({ | ||||
| 		status: function(data) { | ||||
| 			$rootScope.title = data.title; | ||||
| 			// populate jobs
 | ||||
| 			$scope.jobsQueued = data.queued; | ||||
| 			$scope.jobsRunning = data.running; | ||||
| 			$scope.jobsRecent = data.recent.map(Laminar.jobFormatter); | ||||
| 			$scope.jobsRecent = data.recent; | ||||
| 
 | ||||
| 			$scope.$apply(); | ||||
| 			 | ||||
| 			// setup charts
 | ||||
| 			chtUtilization = new Chart(document.getElementById("chartUtil").getContext("2d")).Pie( | ||||
| 				[{value: data.executorsBusy, color:"sandybrown", label: "Busy"}, | ||||
| 				 {value: data.executorsTotal, color: "steelblue", label: "Idle"}], | ||||
| 				[{value: data.executorsBusy, color:"tan", label: "Busy"}, | ||||
| 				 {value: data.executorsTotal, color: "darkseagreen", label: "Idle"}], | ||||
| 				{animationEasing: 'easeInOutQuad'} | ||||
| 			); | ||||
| 			chtBuildsPerDay = new Chart(document.getElementById("chartBpd").getContext("2d")).Line({ | ||||
| @ -104,10 +96,17 @@ angular.module('laminar',['ngRoute','ngSanitize']) | ||||
| 			chtBuildsPerJob = new Chart(document.getElementById("chartBpj").getContext("2d")).HorizontalBar({ | ||||
| 				labels: Object.keys(data.buildsPerJob), | ||||
| 				datasets: [{ | ||||
| 					fillColor: "steelblue", | ||||
| 					fillColor: "lightsteelblue", | ||||
| 					data: Object.keys(data.buildsPerJob).map(function(e){return data.buildsPerJob[e];}) | ||||
| 				}] | ||||
| 			},{}); | ||||
| 			chtTimePerJob = new Chart(document.getElementById("chartTpj").getContext("2d")).HorizontalBar({ | ||||
| 				labels: Object.keys(data.timePerJob), | ||||
| 				datasets: [{ | ||||
| 					fillColor: "lightsteelblue", | ||||
| 					data: Object.keys(data.timePerJob).map(function(e){return data.timePerJob[e];}) | ||||
| 				}] | ||||
| 			},{}); | ||||
| 		}, | ||||
| 		job_queued: function(data) { | ||||
| 			$scope.jobsQueued.splice(0,0,data); | ||||
| @ -130,7 +129,7 @@ angular.module('laminar',['ngRoute','ngSanitize']) | ||||
| 				var job = $scope.jobsRunning[i]; | ||||
| 				if(job.name == data.name && job.number == data.number) { | ||||
| 					$scope.jobsRunning.splice(i,1); | ||||
| 					$scope.jobsRecent.splice(0,0,Laminar.jobFormatter(data)); | ||||
| 					$scope.jobsRecent.splice(0,0,data); | ||||
| 					$scope.$apply(); | ||||
| 					 | ||||
| 					break; | ||||
| @ -146,7 +145,6 @@ angular.module('laminar',['ngRoute','ngSanitize']) | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
| 	$scope.runIcon = Laminar.runIcon; | ||||
| 	timeUpdater = $interval(function() { | ||||
| 		$scope.jobsRunning.forEach(function(o){ | ||||
| 			if(o.etc) { | ||||
| @ -166,34 +164,82 @@ angular.module('laminar',['ngRoute','ngSanitize']) | ||||
| 		$interval.cancel(timeUpdater); | ||||
| 	}); | ||||
| }) | ||||
| .controller('BrowseController', function($scope, $ws, $interval){ | ||||
| .controller('BrowseController', function($rootScope, $scope, $ws, $interval){ | ||||
| 	$rootScope.bc = { | ||||
| 		nodes: [{ href: '/', label: 'Home' }], | ||||
| 		current: 'Jobs' | ||||
| 	}; | ||||
| 
 | ||||
| 	$scope.currentTag = null; | ||||
| 	$scope.activeTag = function(t) { | ||||
| 		return $scope.currentTag === t; | ||||
| 	}; | ||||
| 	$scope.bytag = function(job) { | ||||
| 		if($scope.currentTag === null) return true; | ||||
| 		return job.tags.indexOf($scope.currentTag) >= 0; | ||||
| 	}; | ||||
| 
 | ||||
| 	$scope.jobs = []; | ||||
| 	$ws.statusListener({ | ||||
| 		status: function(data) { | ||||
| 			$rootScope.title = data.title; | ||||
| 			$scope.jobs = data.jobs; | ||||
| 			var tags = {}; | ||||
| 			for(var i in data.jobs) { | ||||
| 				for(var j in data.jobs[i].tags) { | ||||
| 					tags[data.jobs[i].tags[j]] = true; | ||||
| 				} | ||||
| 			} | ||||
| 			$scope.tags = Object.keys(tags); | ||||
| 			$scope.$apply(); | ||||
| 		}, | ||||
| 	}); | ||||
| }) | ||||
| .controller('JobController', function($scope, $routeParams, $ws) { | ||||
| .controller('JobController', function($rootScope, $scope, $routeParams, $ws) { | ||||
| 	$rootScope.bc = { | ||||
| 		nodes: [{ href: '/', label: 'Home' },{ href: '/jobs', label: 'Jobs' }], | ||||
| 		current: $routeParams.name | ||||
| 	}; | ||||
| 	 | ||||
| 	$scope.name = $routeParams.name; | ||||
| 	$scope.jobsQueued = []; | ||||
| 	$scope.jobsRunning = []; | ||||
| 	$scope.jobsRecent = []; | ||||
| 
 | ||||
| 	$ws.statusListener({ | ||||
| 		status: function(data) { | ||||
| 			$scope.jobsQueued = data.queued.filter(function(e){return e.name == $routeParams.name;}); | ||||
| 			$scope.jobsRunning = data.running.filter(function(e){return e.name == $routeParams.name;}); | ||||
| 			$scope.jobsRecent = data.recent.filter(function(e){return e.name == $routeParams.name;}); | ||||
| 			$rootScope.title = data.title; | ||||
| 
 | ||||
| 			$scope.jobsRunning = data.running; | ||||
| 			$scope.jobsRecent = data.recent; | ||||
| 			$scope.lastSuccess = data.lastSuccess; | ||||
| 			$scope.lastFailed = data.lastFailed; | ||||
| 			$scope.$apply(); | ||||
| 		}, | ||||
| 		job_queued: function(data) { | ||||
| 			if(data.name == $routeParams.name) { | ||||
| 				$scope.jobsQueued.splice(0,0,data); | ||||
| 				$scope.$apply(); | ||||
| 			 | ||||
| 			var chtBt = new Chart(document.getElementById("chartBt").getContext("2d")).Bar({ | ||||
| 				labels: data.recent.map(function(e){return '#' + e.number;}), | ||||
| 				datasets: [{ | ||||
| 					fillColor: "darkseagreen", | ||||
| 					strokeColor: "forestgreen", | ||||
| 					data: data.recent.map(function(e){return e.duration;}) | ||||
| 				}] | ||||
| 			}, | ||||
| 			{barValueSpacing: 1,barStrokeWidth: 1,barDatasetSpacing:0} | ||||
| 			); | ||||
| 
 | ||||
| 			for(var i = 0; i < data.recent.length; ++i) { | ||||
| 				if(data.recent[i].result != "success") { | ||||
| 					chtBt.datasets[0].bars[i].fillColor = "darksalmon"; | ||||
| 					chtBt.datasets[0].bars[i].strokeColor = "crimson"; | ||||
| 				} | ||||
| 			} | ||||
| 			chtBt.update(); | ||||
| 			 | ||||
| 		}, | ||||
| 		job_queued: function() { | ||||
| 			$scope.nQueued++; | ||||
| 		}, | ||||
| 		job_started: function(data) { | ||||
| 			$scope.nQueued--; | ||||
| 			if(data.name == $routeParams.name) { | ||||
| 				$scope.jobsQueued.splice($scope.jobsQueued.length - 1,1); | ||||
| 				$scope.jobsRunning.splice(0,0,data); | ||||
| @ -212,36 +258,62 @@ angular.module('laminar',['ngRoute','ngSanitize']) | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
| 	$scope.runIcon = Laminar.runIcon; | ||||
| }) | ||||
| .controller('RunController', function($scope, $routeParams, $ws) { | ||||
| .controller('RunController', function($rootScope, $scope, $routeParams, $ws) { | ||||
| 	$rootScope.bc = { | ||||
| 		nodes: [{ href: '/', label: 'Home' }, | ||||
| 		{ href: '/jobs', label: 'Jobs' }, | ||||
| 		{ href: '/jobs/'+$routeParams.name, label: $routeParams.name } | ||||
| 		], | ||||
| 		current: '#' + $routeParams.num | ||||
| 	}; | ||||
| 
 | ||||
| 	$scope.name = $routeParams.name; | ||||
| 	$scope.num = $routeParams.num; | ||||
| 	$scope.num = parseInt($routeParams.num); | ||||
| 	$ws.statusListener({ | ||||
| 		status: function(data) { | ||||
| 			$scope.job = Laminar.jobFormatter(data); | ||||
| 			$rootScope.title = data.title; | ||||
| 			$scope.job = data; | ||||
| 			$scope.$apply(); | ||||
| 		}, | ||||
| 		job_started: function() { | ||||
| 			$scope.job.latestNum++; | ||||
| 			$scope.$apply(); | ||||
| 		}, | ||||
| 		job_completed: function(data) { | ||||
| 			$scope.job = Laminar.jobFormatter(data); | ||||
| 			$scope.job = data; | ||||
| 			$scope.$apply(); | ||||
| 		} | ||||
| 	}); | ||||
| 	$scope.runIcon = Laminar.runIcon; | ||||
| }) | ||||
| .controller('LogController', function($scope, $routeParams, $ws) { | ||||
| 	$scope.name = $routeParams.name; | ||||
| 	$scope.num = $routeParams.num; | ||||
| 	$scope._log = "" | ||||
| 	$ws.logListener(function(data) { | ||||
| 		$scope._log += ansi_up.ansi_to_html(data); | ||||
| 		$scope.$apply(); | ||||
| 		window.scrollTo(0, document.body.scrollHeight); | ||||
| 	}); | ||||
| 	$scope.log = function() { | ||||
| 		// TODO sanitize
 | ||||
| 		return ansi_up.ansi_to_html($scope._log); | ||||
| 	} | ||||
| 	 | ||||
| 	$scope.log = "" | ||||
| 	$scope.autoscroll = false; | ||||
| 	var firstLog = false; | ||||
| 	$ws.logListener(function(data) { | ||||
| 		$scope.log += ansi_up.ansi_to_html(data.replace('<','<').replace('>','>')); | ||||
| 		$scope.$apply(); | ||||
| 		if(!firstLog) { | ||||
| 			firstLog = true; | ||||
| 		} else if($scope.autoscroll) { | ||||
| 			window.scrollTo(0, document.body.scrollHeight); | ||||
| 		} | ||||
| 	}); | ||||
| 
 | ||||
| }) | ||||
| .run(function() {}); | ||||
| .run(function($rootScope) { | ||||
| 	angular.extend($rootScope, { | ||||
| 		runIcon: function(result) { | ||||
| 			return result === "success" ? '<span style="color:forestgreen;font-family:\'Zapf Dingbats\';">✔</span>' : result === "failed" ? '<span style="color:crimson;">✘</span>' : ''; | ||||
| 		}, | ||||
| 		formatDate: function(unix) { | ||||
| 			// TODO reimplement when toLocaleDateString() accepts formatting
 | ||||
| 			// options on most browsers
 | ||||
| 			var d = new Date(1000 * unix); | ||||
| 			return d.getHours() + ':' + d.getMinutes() + ' on ' +  | ||||
| 				['Sun','Mon','Tue','Wed','Thu','Fri','Sat'][d.getDay()] + ' ' | ||||
| 				+ d.getDate() + '. ' + ['Jan','Feb','Mar','Apr','May','Jun', | ||||
| 				'Jul','Aug','Sep', 'Oct','Nov','Dec'][d.getMonth()] + ' ' | ||||
| 				+ d.getFullYear(); | ||||
| 		} | ||||
| 	}); | ||||
| }); | ||||
|  | ||||
| @ -1,14 +1,19 @@ | ||||
| <div class="container-fluid"> | ||||
| <div class="row"> | ||||
| <div class="col-xs-12"> | ||||
|  <h3>Browse jobs</h3> | ||||
|  <div class="form-inline form-group"> | ||||
|   <label for="jobFilter">Filter</label> | ||||
|   <input id="jobFilter" ng-model="search.name"> | ||||
|  <div class="pull-right"> | ||||
|   <input class="form-control" id="jobFilter" ng-model="search.name" placeholder="Filter..."> | ||||
|  </div> | ||||
|  <table class="table table-bordered"> | ||||
|   <tr class="animate-repeat" ng-repeat="job in jobs | filter:search:strict"> | ||||
|  <ul class="nav nav-tabs"> | ||||
|   <li ng-class="{active:activeTag(null)}"><a href ng-click="currentTag = null">All Jobs</a></li> | ||||
|   <li ng-repeat="tag in tags" ng-class="{active:activeTag(tag)}"><a href ng-click="$parent.currentTag = tag">{{tag}}</a></li> | ||||
|  </ul> | ||||
|  <style>table#joblist tr:first-child td { border-top: 0; }</style> | ||||
|  <table class="table table-striped" id="joblist"> | ||||
|   <tr class="animate-repeat" ng-repeat="job in jobs | filter:bytag | filter:search"> | ||||
|    <td><a href="jobs/{{job.name}}">{{job.name}}</a></td> | ||||
|    <td class="text-center"><span ng-bind-html="runIcon(job.result)"></span> <a href="jobs/{{job.name}}/{{job.numberumber}}">#{{job.number}}</a></td> | ||||
|    <td class="text-center">{{formatDate(job.started)}}</a></td> | ||||
|   </tr> | ||||
|  </table> | ||||
| </div> | ||||
|  | ||||
| @ -1,7 +1,6 @@ | ||||
| <div class="container-fluid"> | ||||
| <div class="row"> | ||||
| <div class="col-sm-5 col-md-4 col-lg-3 dash"> | ||||
|  <h3>Recent Builds</h3> | ||||
|  <table class="table table-bordered"> | ||||
|   <tr class="animate-repeat" ng-repeat="job in jobsQueued track by $index"> | ||||
|    <td><a href="jobs/{{job.name}}">{{job.name}}</a> <i>queued</i></td> | ||||
| @ -13,16 +12,15 @@ | ||||
|    </td> | ||||
|   </tr> | ||||
|   <tr class="animate-repeat" ng-repeat="job in jobsRecent track by $index"> | ||||
|    <td><span ng-bind-html="runIcon(job.result)"></span> <a href="jobs/{{job.name}}">{{job.name}}</a> <a href="jobs/{{job.name}}/{{job.number}}">#{{job.number}}</a><br><small>Took {{job.duration}} at {{job.when}}</small></td> | ||||
|    <td><span ng-bind-html="runIcon(job.result)"></span> <a href="jobs/{{job.name}}">{{job.name}}</a> <a href="jobs/{{job.name}}/{{job.number}}">#{{job.number}}</a><br><small>Took {{job.duration}}s at {{formatDate(job.started)}}</small></td> | ||||
|   </tr> | ||||
|  </table> | ||||
| </div> | ||||
| <div class="col-sm-7 col-md-8 col-lg-9"> | ||||
| <h3>Dashboard</h3> | ||||
| <div class="row"> | ||||
| <div class="col-md-6"> | ||||
|  <div class="panel panel-default"> | ||||
|   <div class="panel-heading">Builds per day</div> | ||||
|   <div class="panel-heading">Total builds per day this week</div> | ||||
|   <div class="panel-body"> | ||||
|    <canvas id="chartBpd"></canvas> | ||||
|   </div> | ||||
| @ -38,16 +36,18 @@ | ||||
| </div> | ||||
| <div class="col-md-6"> | ||||
|  <div class="panel panel-default"> | ||||
|   <div class="panel-heading">Current executor utilization</div> | ||||
|   <div class="panel-heading">Average build time per job this week</div> | ||||
|   <div class="panel-body"> | ||||
|    <canvas id="chartUtil"></canvas> | ||||
|    <canvas id="chartTpj"></canvas> | ||||
|   </div> | ||||
| 
 | ||||
|  </div> | ||||
| </div> | ||||
| <div class="col-md-6"> | ||||
|  <div class="panel panel-default"> | ||||
|   <div class="panel-heading">what to put here?</div> | ||||
|   <div class="panel-heading">Current executor utilization</div> | ||||
|   <div class="panel-body"> | ||||
|    <canvas id="chartUtil"></canvas> | ||||
|   </div> | ||||
|  </div> | ||||
| </div> | ||||
|  | ||||
| @ -1,22 +1,48 @@ | ||||
| <div class="container-fluid"> | ||||
| <div class="row"> | ||||
| <div class="col-xs-12"> | ||||
|  <h3>{{name}}</h3> | ||||
|  <table class="table table-bordered"> | ||||
|   <tr class="animate-repeat" ng-repeat="job in jobsQueued track by $index"> | ||||
|    <td><i>queued</i></td> | ||||
|   </tr> | ||||
|   <tr class="animate-repeat" ng-repeat="job in jobsRunning track by $index"> | ||||
|    <td><a href="jobs/{{job.name}}/{{job.number}}">#{{job.number}}</a> progressbar?</td> | ||||
|   </tr> | ||||
|   <tr class="animate-repeat" ng-repeat="job in jobsRecent track by $index"> | ||||
|    <td><span ng-bind-html="runIcon(job.result)"></span> <a href="jobs/{{job.name}}/{{job.number}}">#{{job.number}}</a></td> | ||||
|   </tr> | ||||
|  </table> | ||||
| 	 | ||||
|  <div class="col-sm-5 col-md-6 col-lg-7"> | ||||
|   <h3>{{name}}</h3> | ||||
|   <dl class="dl-horizontal"> | ||||
|    <dt>Last Successful Run</dt><dd> | ||||
| 	   <a ng-show="lastSuccess" href="jobs/{{name}}/{{lastSuccess.number}}">#{{lastSuccess.number}}</a>    | ||||
| 	   {{lastSuccess?" - at "+formatDate(lastSuccess.started):"never"}}</dd> | ||||
|      <dt>Last Failed Run</dt><dd> | ||||
| 	   <a ng-show="lastFailed" href="jobs/{{name}}/{{lastFailed.number}}">#{{lastFailed.number}}</a>    | ||||
| 	   {{lastFailed?" - at "+formatDate(lastFailed.started):"never"}}</dd> | ||||
|   </dl> | ||||
| 
 | ||||
|  </div> | ||||
|  <div class="col-sm-7 col-md-6 col-lg-5"> | ||||
|   <div class="panel panel-default"> | ||||
|    <div class="panel-heading">Build time</div> | ||||
|    <div class="panel-body"> | ||||
|     <canvas id="chartBt"></canvas> | ||||
|    </div> | ||||
|   </div> | ||||
|  </div> | ||||
|   | ||||
| </div> | ||||
| <div class="col-sm-7 col-md-8 col-lg-9"> | ||||
| <div class="row"> | ||||
| 
 | ||||
|  <div class="col-xs-12"> | ||||
|   <table class="table table-striped"><thead> | ||||
|    <tr><th>Run</th><th class="text-center">Started</th><th class="text-center">Duration</th><th class="text-center hidden-xs">Reason</th></tr></thead> | ||||
|    <tr ng-show="nQueued"> | ||||
|     <td colspan="4"><i>{{nQueued}} run(s) queued</i></td> | ||||
|    </tr> | ||||
|    <tr class="animate-repeat" ng-repeat="job in jobsRunning track by $index"> | ||||
|     <td><a href="jobs/{{job.name}}/{{job.number}}">#{{job.number}}</a> progressbar?</td> | ||||
|    </tr> | ||||
|    <tr class="animate-repeat" ng-repeat="job in jobsRecent track by $index"> | ||||
|     <td><span ng-bind-html="runIcon(job.result)"></span> <a href="jobs/{{name}}/{{job.number}}">#{{job.number}}</a></td> | ||||
|     <td class="text-center">{{formatDate(job.started)}}</td> | ||||
|     <td class="text-center">{{job.duration + " seconds"}}</td> | ||||
|     <td class="text-center hidden-xs">{{job.reason}}</td> | ||||
|    </tr> | ||||
|   </table> | ||||
|  </div> | ||||
|   | ||||
| </div> | ||||
| </div> | ||||
| </div> | ||||
| 
 | ||||
|  | ||||
| @ -1,8 +0,0 @@ | ||||
| <div class="container-fluid"> | ||||
| <div class="row"> | ||||
| <div class="col-xs-12"> | ||||
|  <h3>Log output for {{name}} #{{num}}</h3> | ||||
|  <pre ng-bind-html="log()"></pre> | ||||
| </div> | ||||
| </div> | ||||
| </div> | ||||
| @ -1,14 +1,40 @@ | ||||
| <div class="container-fluid"> | ||||
| <div class="row"> | ||||
| <div class="col-xs-12"> | ||||
| <div class="col-sm-5 col-md-6 col-lg-7"> | ||||
|  <h3 style="float:left"><span ng-bind-html="runIcon(job.result)"></span> {{name}} #{{num}}</h3> | ||||
|  <nav class="pull-left"> | ||||
|   <ul class="pagination" style="margin:15px 20px"> | ||||
|    <li><a href="jobs/{{name}}/{{num-1}}">«</a></li> | ||||
|    <li ng-show="job.latestNum > num"><a ng-href="jobs/{{name}}/{{num+1}}">»</a></li> | ||||
|   </ul> | ||||
|  </nav> | ||||
|  <div style="clear:both;"></div> | ||||
|  <dl class="dl-horizontal"> | ||||
|   <dt style="vertical-align:bottom;"></dt><dd><h3><span ng-bind-html="runIcon(job.result)"></span> {{name}} #{{num}}</h3></dd> | ||||
|   <dt><a class="btn btn-default" href="jobs/{{name}}">< Job</a></dt><dd><a class="btn btn-default" href="jobs/{{name}}/{{num}}/log">Log output</a></dd> | ||||
|   <dt></dt><dd> </dd> | ||||
|   <dt>Reason</dt><dd>{{job.reason}}</dd> | ||||
|   <dt>Started</dt><dd>{{job.when}}</dd> | ||||
|   <dt>Artifacts</dt><dd><ul><li ng-repeat="art in job.artifacts"><a href="{{art.url}}" target="_self">{{art.filename}}</a></li></ul></dd> | ||||
|   <dt>Queued for</dt><dd>{{job.queued}}s</dd> | ||||
|   <dt>Started</dt><dd>{{formatDate(job.started)}}</dd> | ||||
|   <dt>Completed</dt><dd>{{formatDate(job.completed)}}</dd> | ||||
|   <dt>Duration</dt><dd>{{job.duration}}s</dd> | ||||
|  </dl> | ||||
| </div> | ||||
| <div class="col-sm-7 col-md-6 col-lg-5"> | ||||
|  <div class="panel panel-default" ng-show="job.artifacts.length"> | ||||
|   <div class="panel-heading">Artifacts</div> | ||||
|   <div class="panel-body"> | ||||
|    <ul class="list-unstyled" style="margin-bottom: 0"> | ||||
| 	<li ng-repeat="art in job.artifacts"> | ||||
| 	 <a href="{{art.url}}" target="_self">{{art.filename}}</a> | ||||
| 	</li> | ||||
|    </ul> | ||||
|   </div> | ||||
|  </div> | ||||
| </div> | ||||
| </div> | ||||
| <div class="row"> | ||||
| <div class="col-xs-12"> | ||||
|  <button type="button" class="btn btn-default btn-xs pull-right" ng-class="{active:autoscroll}" ng-click="autoscroll = !autoscroll" style="margin-top:10px">Autoscroll</button> | ||||
|  <h4>Console output</h4> | ||||
|  <pre ng-bind-html="log"></pre> | ||||
| </div> | ||||
| </div> | ||||
| </div> | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user