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>
 | 
				
			||||||
   <input class="form-control" id="jobFilter" v-model="search" placeholder="Filter...">
 | 
					   <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>
 | 
					  </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