mirror of
				https://github.com/ohwgiles/laminar.git
				synced 2025-06-13 12:54:29 +00:00 
			
		
		
		
	
							parent
							
								
									746ab24676
								
							
						
					
					
						commit
						1ea9713536
					
				| @ -78,14 +78,14 @@ kj::Maybe<MonitorScope> fromUrl(std::string resource, char* query) { | |||||||
|         return kj::mv(scope); |         return kj::mv(scope); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     if(resource.substr(0, 5) != "/jobs") |     if(resource == "/jobs" || resource == "/wallboard") { | ||||||
|         return nullptr; |  | ||||||
| 
 |  | ||||||
|     if(resource.length() == 5) { |  | ||||||
|         scope.type = MonitorScope::ALL; |         scope.type = MonitorScope::ALL; | ||||||
|         return kj::mv(scope); |         return kj::mv(scope); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|  |     if(resource.substr(0, 5) != "/jobs") | ||||||
|  |         return nullptr; | ||||||
|  | 
 | ||||||
|     resource = resource.substr(5); |     resource = resource.substr(5); | ||||||
|     size_t split = resource.find('/',1); |     size_t split = resource.find('/',1); | ||||||
|     std::string job = resource.substr(1,split-1); |     std::string job = resource.substr(1,split-1); | ||||||
|  | |||||||
| @ -325,16 +325,17 @@ std::string Laminar::getStatus(MonitorScope scope) { | |||||||
|         j.set("description", desc == jobDescriptions.end() ? "" : desc->second); |         j.set("description", desc == jobDescriptions.end() ? "" : desc->second); | ||||||
|     } else if(scope.type == MonitorScope::ALL) { |     } else if(scope.type == MonitorScope::ALL) { | ||||||
|         j.startArray("jobs"); |         j.startArray("jobs"); | ||||||
|         db->stmt("SELECT name,number,startedAt,completedAt,result FROM builds b " |         db->stmt("SELECT name,number,startedAt,completedAt,result,reason FROM builds b " | ||||||
|                  "JOIN (SELECT name n,MAX(number) latest FROM builds WHERE result IS NOT NULL GROUP BY n) q " |                  "JOIN (SELECT name n,MAX(number) latest FROM builds WHERE result IS NOT NULL GROUP BY n) q " | ||||||
|                  "ON b.name = q.n AND b.number = latest") |                  "ON b.name = q.n AND b.number = latest") | ||||||
|         .fetch<str,uint,time_t,time_t,int>([&](str name,uint number, time_t started, time_t completed, int result){ |         .fetch<str,uint,time_t,time_t,int,str>([&](str name,uint number, time_t started, time_t completed, int result, str reason){ | ||||||
|             j.StartObject(); |             j.StartObject(); | ||||||
|             j.set("name", name); |             j.set("name", name); | ||||||
|             j.set("number", number); |             j.set("number", number); | ||||||
|             j.set("result", to_string(RunState(result))); |             j.set("result", to_string(RunState(result))); | ||||||
|             j.set("started", started); |             j.set("started", started); | ||||||
|             j.set("completed", completed); |             j.set("completed", completed); | ||||||
|  |             j.set("reason", reason); | ||||||
|             j.EndObject(); |             j.EndObject(); | ||||||
|         }); |         }); | ||||||
|         j.EndArray(); |         j.EndArray(); | ||||||
|  | |||||||
| @ -122,7 +122,7 @@ inline bool beginsWith(std::string haystack, const char* needle) { | |||||||
| bool Resources::handleRequest(std::string path, const char** start, const char** end, const char** content_type) { | bool Resources::handleRequest(std::string path, const char** start, const char** end, const char** content_type) { | ||||||
|     // need to keep the list of "application links" synchronised with the angular
 |     // need to keep the list of "application links" synchronised with the angular
 | ||||||
|     // application. We cannot return a 404 for any of these
 |     // application. We cannot return a 404 for any of these
 | ||||||
|     auto it = beginsWith(path,"/jobs") |     auto it = beginsWith(path,"/jobs") || path == "/wallboard" | ||||||
|             ? resources.find("/") |             ? resources.find("/") | ||||||
|             : resources.find(path); |             : resources.find(path); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -81,7 +81,19 @@ | |||||||
|     <a v-show="ungrouped.length" :class="{'active':group==null}" href v-on:click.prevent="group = null">Ungrouped Jobs</a> |     <a v-show="ungrouped.length" :class="{'active':group==null}" href v-on:click.prevent="group = null">Ungrouped Jobs</a> | ||||||
|     <a v-for="g in Object.keys(groups)" :class="{'active':g==group}" href v-on:click.prevent="group = g">{{g}}</a> |     <a v-for="g in Object.keys(groups)" :class="{'active':g==group}" href v-on:click.prevent="group = g">{{g}}</a> | ||||||
|    </div> |    </div> | ||||||
|  |    <div style="display: grid; grid-auto-flow: column; align-items: center; gap: 15px"> | ||||||
|  |     <router-link :to="wallboardLink()" style="display: inherit;" title="Wallboard"> | ||||||
|  |      <svg width="18" viewBox="0 0 13 13"> | ||||||
|  |       <g fill="#728494"> | ||||||
|  |        <rect x="0" y="2" width="6" height="4" /> | ||||||
|  |        <rect x="0" y="7" width="6" height="4" /> | ||||||
|  |        <rect x="7" y="2" width="6" height="4" /> | ||||||
|  |        <rect x="7" y="7" width="6" height="4" /> | ||||||
|  |       </g> | ||||||
|  |      </svg> | ||||||
|  |     </router-link> | ||||||
|     <input class="form-control" id="jobFilter" v-model="search" placeholder="Filter..."> |     <input class="form-control" id="jobFilter" v-model="search" placeholder="Filter..."> | ||||||
|  |    </div> | ||||||
|   </nav> |   </nav> | ||||||
|   <table class="striped" id="job-list"> |   <table class="striped" id="job-list"> | ||||||
|     <tr v-for="job in filteredJobs()"> |     <tr v-for="job in filteredJobs()"> | ||||||
| @ -93,6 +105,14 @@ | |||||||
|   </table> |   </table> | ||||||
|  </div></template> |  </div></template> | ||||||
|   |   | ||||||
|  |  <template id="wallboard"><div class="wallboard"> | ||||||
|  |   <router-link :to="'/jobs/'+job.name+'/'+job.number" tag="div" v-for="job in wallboardJobs()" :data-result="job.result"> | ||||||
|  |    <span style="font-size: 36px; font-weight: bold;">{{job.name}} #{{job.number}}</span><br> | ||||||
|  |    <span style="font-size: 30px;">{{formatDate(job.started)}}</span><br> | ||||||
|  |    <span style="font-size: 26px;">{{job.reason}}</span> | ||||||
|  |   </router-link> | ||||||
|  |  </div></template> | ||||||
|  | 
 | ||||||
|  <template id="job"><div id="page-job-main"> |  <template id="job"><div id="page-job-main"> | ||||||
|   <div style="padding: 15px;"> |   <div style="padding: 15px;"> | ||||||
|    <h2>{{$route.params.name}}</h2> |    <h2>{{$route.params.name}}</h2> | ||||||
|  | |||||||
| @ -390,16 +390,17 @@ const Home = function() { | |||||||
|   }; |   }; | ||||||
| }(); | }(); | ||||||
| 
 | 
 | ||||||
| const Jobs = function() { | const All = function(templateId) { | ||||||
|   var state = { |   var state = { | ||||||
|     jobs: [], |     jobs: [], | ||||||
|     search: '', |     search: '', | ||||||
|     groups: {}, |     groups: {}, | ||||||
|  |     regexps: {}, | ||||||
|     group: null, |     group: null, | ||||||
|     ungrouped: [] |     ungrouped: [] | ||||||
|   }; |   }; | ||||||
|   return { |   return { | ||||||
|     template: '#jobs', |     template: templateId, | ||||||
|     mixins: [ServerEventHandler, Utils, ProgressUpdater], |     mixins: [ServerEventHandler, Utils, ProgressUpdater], | ||||||
|     data: function() { return state; }, |     data: function() { return state; }, | ||||||
|     methods: { |     methods: { | ||||||
| @ -418,11 +419,12 @@ const Jobs = function() { | |||||||
|           } |           } | ||||||
|         } |         } | ||||||
|         state.groups = {}; |         state.groups = {}; | ||||||
|         Object.keys(msg.groups).map(k => state.groups[k] = new RegExp(msg.groups[k])); |         Object.keys(msg.groups).forEach(k => state.regexps[k] = new RegExp(state.groups[k] = msg.groups[k])); | ||||||
|         state.ungrouped = state.jobs.filter(j => !Object.values(state.groups).some(r => r.test(j.name))).map(j => j.name); |         state.ungrouped = state.jobs.filter(j => !Object.values(state.regexps).some(r => r.test(j.name))).map(j => j.name); | ||||||
|         state.group = state.ungrouped.length ? null : Object.keys(state.groups)[0]; |         state.group = state.ungrouped.length ? null : Object.keys(state.groups)[0]; | ||||||
|       }, |       }, | ||||||
|       job_started: function(data) { |       job_started: function(data) { | ||||||
|  |         data.result = 'running'; // for wallboard css
 | ||||||
|         var updAt = null; |         var updAt = null; | ||||||
|         // jobsRunning must be maintained for ProgressUpdater
 |         // jobsRunning must be maintained for ProgressUpdater
 | ||||||
|         for (var i in state.jobsRunning) { |         for (var i in state.jobsRunning) { | ||||||
| @ -447,7 +449,7 @@ const Jobs = function() { | |||||||
|           // first execution of new job. TODO insert without resort
 |           // first execution of new job. TODO insert without resort
 | ||||||
|           state.jobs.unshift(data); |           state.jobs.unshift(data); | ||||||
|           state.jobs.sort(function(a, b){return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;}); |           state.jobs.sort(function(a, b){return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;}); | ||||||
|           if(!Object.values(state.groups).some(r => r.test(data.name))) |           if(!Object.values(state.regexps).some(r => r.test(data.name))) | ||||||
|               state.ungrouped.push(data.name); |               state.ungrouped.push(data.name); | ||||||
|         } else { |         } else { | ||||||
|           state.jobs[updAt] = data; |           state.jobs[updAt] = data; | ||||||
| @ -473,16 +475,30 @@ const Jobs = function() { | |||||||
|       filteredJobs: function() { |       filteredJobs: function() { | ||||||
|         let ret = []; |         let ret = []; | ||||||
|         if (state.group) |         if (state.group) | ||||||
|           ret = state.jobs.filter(job => state.groups[state.group].test(job.name)); |           ret = state.jobs.filter(job => state.regexps[state.group].test(job.name)); | ||||||
|         else |         else | ||||||
|           ret = state.jobs.filter(job => state.ungrouped.includes(job.name)); |           ret = state.jobs.filter(job => state.ungrouped.includes(job.name)); | ||||||
|         if (this.search) |         if (this.search) | ||||||
|           ret = ret.filter(job => job.name.indexOf(this.search) > -1); |           ret = ret.filter(job => job.name.indexOf(this.search) > -1); | ||||||
|         return ret; |         return ret; | ||||||
|       }, |       }, | ||||||
|  |       wallboardJobs: function() { | ||||||
|  |         let ret = []; | ||||||
|  |         const expr = (new URLSearchParams(window.location.search)).get('filter'); | ||||||
|  |         if (expr) | ||||||
|  |           ret = state.jobs.filter(job => (new RegExp(expr)).test(job.name)); | ||||||
|  |         else | ||||||
|  |           ret = state.jobs; | ||||||
|  |         // sort failed before success, newest first
 | ||||||
|  |         ret.sort((a,b) => a.result == b.result ? a.started - b.started : 2*(b.result == 'success')-1); | ||||||
|  |         return ret; | ||||||
|  |       }, | ||||||
|  |       wallboardLink: function() { | ||||||
|  |         return '/wallboard' + (state.group ? '?filter=' + state.groups[state.group] : ''); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
| }(); | }; | ||||||
| 
 | 
 | ||||||
| var Job = function() { | var Job = function() { | ||||||
|   var state = { |   var state = { | ||||||
| @ -751,7 +767,8 @@ new Vue({ | |||||||
|     base: document.head.baseURI.substr(location.origin.length), |     base: document.head.baseURI.substr(location.origin.length), | ||||||
|     routes: [ |     routes: [ | ||||||
|       { path: '/',                   component: Home }, |       { path: '/',                   component: Home }, | ||||||
|       { path: '/jobs',               component: Jobs }, |       { path: '/jobs',               component: All('#jobs') }, | ||||||
|  |       { path: '/wallboard',          component: All('#wallboard') }, | ||||||
|       { path: '/jobs/:name',         component: Job }, |       { path: '/jobs/:name',         component: Job }, | ||||||
|       { path: '/jobs/:name/:number', component: Run } |       { path: '/jobs/:name/:number', component: Run } | ||||||
|     ], |     ], | ||||||
|  | |||||||
| @ -179,6 +179,41 @@ button:not([disabled]) { cursor: pointer; color: var(--main-fg); } | |||||||
|  from { background-position: 1rem 0; } to { background-position: 0 0; } |  from { background-position: 1rem 0; } to { background-position: 0 0; } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /* wallboard */ | ||||||
|  | .wallboard { | ||||||
|  |  display: flex; | ||||||
|  |  flex-wrap: wrap-reverse; | ||||||
|  |  flex-direction: row-reverse; | ||||||
|  |  gap: 20px; | ||||||
|  |  padding: 20px; | ||||||
|  |  position: fixed; | ||||||
|  |  height: 100%; | ||||||
|  |  width: 100%; | ||||||
|  |  overflow: auto; | ||||||
|  |  background-color: #000 | ||||||
|  | } | ||||||
|  | .wallboard > div { | ||||||
|  |  padding: 30px; | ||||||
|  |  flex-grow: 1; | ||||||
|  |  background-color: var(--failure); | ||||||
|  |  color: var(--nav-fg-light); | ||||||
|  | } | ||||||
|  | .wallboard > div:hover { | ||||||
|  |   cursor: pointer; | ||||||
|  | } | ||||||
|  | .wallboard > div[data-result="running"] { | ||||||
|  |   animation: wallboard-bg-fade 2s ease infinite; | ||||||
|  | } | ||||||
|  | @keyframes wallboard-bg-fade { | ||||||
|  |   from { background-color: #4786ab; } | ||||||
|  |   50% { background-color: #446597; } | ||||||
|  |   to { background-color: #4786ab; } | ||||||
|  | } | ||||||
|  | .wallboard > div[data-result="success"] { | ||||||
|  |   background-color: var(--success); | ||||||
|  |   color: var(--main-fg); | ||||||
|  | } | ||||||
|  | 
 | ||||||
| /* connecting overlay */ | /* connecting overlay */ | ||||||
| #connecting-overlay { | #connecting-overlay { | ||||||
|  position: fixed; |  position: fixed; | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user