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); | ||||
|     } | ||||
| 
 | ||||
|     if(resource.substr(0, 5) != "/jobs") | ||||
|         return nullptr; | ||||
| 
 | ||||
|     if(resource.length() == 5) { | ||||
|     if(resource == "/jobs" || resource == "/wallboard") { | ||||
|         scope.type = MonitorScope::ALL; | ||||
|         return kj::mv(scope); | ||||
|     } | ||||
| 
 | ||||
|     if(resource.substr(0, 5) != "/jobs") | ||||
|         return nullptr; | ||||
| 
 | ||||
|     resource = resource.substr(5); | ||||
|     size_t split = resource.find('/',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); | ||||
|     } else if(scope.type == MonitorScope::ALL) { | ||||
|         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 " | ||||
|                  "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.set("name", name); | ||||
|             j.set("number", number); | ||||
|             j.set("result", to_string(RunState(result))); | ||||
|             j.set("started", started); | ||||
|             j.set("completed", completed); | ||||
|             j.set("reason", reason); | ||||
|             j.EndObject(); | ||||
|         }); | ||||
|         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) { | ||||
|     // need to keep the list of "application links" synchronised with the angular
 | ||||
|     // 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(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-for="g in Object.keys(groups)" :class="{'active':g==group}" href v-on:click.prevent="group = g">{{g}}</a> | ||||
|    </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..."> | ||||
|    </div> | ||||
|   </nav> | ||||
|   <table class="striped" id="job-list"> | ||||
|     <tr v-for="job in filteredJobs()"> | ||||
| @ -93,6 +105,14 @@ | ||||
|   </table> | ||||
|  </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"> | ||||
|   <div style="padding: 15px;"> | ||||
|    <h2>{{$route.params.name}}</h2> | ||||
|  | ||||
| @ -390,16 +390,17 @@ const Home = function() { | ||||
|   }; | ||||
| }(); | ||||
| 
 | ||||
| const Jobs = function() { | ||||
| const All = function(templateId) { | ||||
|   var state = { | ||||
|     jobs: [], | ||||
|     search: '', | ||||
|     groups: {}, | ||||
|     regexps: {}, | ||||
|     group: null, | ||||
|     ungrouped: [] | ||||
|   }; | ||||
|   return { | ||||
|     template: '#jobs', | ||||
|     template: templateId, | ||||
|     mixins: [ServerEventHandler, Utils, ProgressUpdater], | ||||
|     data: function() { return state; }, | ||||
|     methods: { | ||||
| @ -418,11 +419,12 @@ const Jobs = function() { | ||||
|           } | ||||
|         } | ||||
|         state.groups = {}; | ||||
|         Object.keys(msg.groups).map(k => state.groups[k] = new RegExp(msg.groups[k])); | ||||
|         state.ungrouped = state.jobs.filter(j => !Object.values(state.groups).some(r => r.test(j.name))).map(j => j.name); | ||||
|         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.regexps).some(r => r.test(j.name))).map(j => j.name); | ||||
|         state.group = state.ungrouped.length ? null : Object.keys(state.groups)[0]; | ||||
|       }, | ||||
|       job_started: function(data) { | ||||
|         data.result = 'running'; // for wallboard css
 | ||||
|         var updAt = null; | ||||
|         // jobsRunning must be maintained for ProgressUpdater
 | ||||
|         for (var i in state.jobsRunning) { | ||||
| @ -447,7 +449,7 @@ const Jobs = function() { | ||||
|           // first execution of new job. TODO insert without resort
 | ||||
|           state.jobs.unshift(data); | ||||
|           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); | ||||
|         } else { | ||||
|           state.jobs[updAt] = data; | ||||
| @ -473,16 +475,30 @@ const Jobs = function() { | ||||
|       filteredJobs: function() { | ||||
|         let ret = []; | ||||
|         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 | ||||
|           ret = state.jobs.filter(job => state.ungrouped.includes(job.name)); | ||||
|         if (this.search) | ||||
|           ret = ret.filter(job => job.name.indexOf(this.search) > -1); | ||||
|         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 state = { | ||||
| @ -751,7 +767,8 @@ new Vue({ | ||||
|     base: document.head.baseURI.substr(location.origin.length), | ||||
|     routes: [ | ||||
|       { 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/: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; } | ||||
| } | ||||
| 
 | ||||
| /* 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 { | ||||
|  position: fixed; | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user