mirror of
				https://github.com/ohwgiles/laminar.git
				synced 2025-06-13 12:54:29 +00:00 
			
		
		
		
	frontend refactor
use a more modern style, reduce variable scopes where possible, fix several minor bugs such as pagination and scales in chart tooltips
This commit is contained in:
		
							parent
							
								
									c6b60646f6
								
							
						
					
					
						commit
						bd489bdbb0
					
				@ -3,30 +3,27 @@
 | 
				
			|||||||
 * https://laminar.ohwg.net
 | 
					 * https://laminar.ohwg.net
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 // A hash function added to String helps generating consistent
 | 
				
			||||||
 | 
					 // colours from job names for use in charts
 | 
				
			||||||
String.prototype.hashCode = function() {
 | 
					String.prototype.hashCode = function() {
 | 
				
			||||||
  for(var r=0, i=0; i<this.length; i++)
 | 
					  for(var r=0, i=0; i<this.length; i++)
 | 
				
			||||||
    r=(r<<5)-r+this.charCodeAt(i),r&=r;
 | 
					    r=(r<<5)-r+this.charCodeAt(i),r&=r;
 | 
				
			||||||
  return r;
 | 
					  return r;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Vue.filter('iecFileSize', function(bytes) {
 | 
					// Filter to pretty-print the size of artifacts
 | 
				
			||||||
  var exp = Math.floor(Math.log(bytes) / Math.log(1024));
 | 
					Vue.filter('iecFileSize', bytes => {
 | 
				
			||||||
 | 
					  const exp = Math.floor(Math.log(bytes) / Math.log(1024));
 | 
				
			||||||
  return (bytes / Math.pow(1024, exp)).toFixed(1) + ' ' +
 | 
					  return (bytes / Math.pow(1024, exp)).toFixed(1) + ' ' +
 | 
				
			||||||
    ['B', 'KiB', 'MiB', 'GiB', 'TiB'][exp];
 | 
					    ['B', 'KiB', 'MiB', 'GiB', 'TiB'][exp];
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const timeScale = function(max){
 | 
					// Mixin handling retrieving dynamic updates from the backend
 | 
				
			||||||
  return max > 3600
 | 
					Vue.mixin((() => {
 | 
				
			||||||
  ? { scale:function(v){return Math.round(v/360)/10}, label:'Hours' }
 | 
					  const setupEventSource = (to, query, next, comp) => {
 | 
				
			||||||
  : max > 60
 | 
					 | 
				
			||||||
  ? { scale:function(v){return Math.round(v/6)/10}, label:'Minutes' }
 | 
					 | 
				
			||||||
  : { scale:function(v){return v;}, label:'Seconds' };
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
const ServerEventHandler = function() {
 | 
					 | 
				
			||||||
  function setupEventSource(to, query, next, comp) {
 | 
					 | 
				
			||||||
    const es = new EventSource(document.head.baseURI + to.path.substr(1) + query);
 | 
					    const es = new EventSource(document.head.baseURI + to.path.substr(1) + query);
 | 
				
			||||||
    es.comp = comp;
 | 
					    es.comp = comp; // When reconnecting, we already have a component. Usually this will be null.
 | 
				
			||||||
    es.path = to.path; // save for later in case we need to add query params
 | 
					    es.to = to; // Save a ref, needed for adding query params for pagination.
 | 
				
			||||||
    es.onmessage = function(msg) {
 | 
					    es.onmessage = function(msg) {
 | 
				
			||||||
      msg = JSON.parse(msg.data);
 | 
					      msg = JSON.parse(msg.data);
 | 
				
			||||||
      // "status" is the first message the server always delivers.
 | 
					      // "status" is the first message the server always delivers.
 | 
				
			||||||
@ -39,7 +36,7 @@ const ServerEventHandler = function() {
 | 
				
			|||||||
      // should not be handled here, but treated the same as any other
 | 
					      // should not be handled here, but treated the same as any other
 | 
				
			||||||
      // message. An exception is if the connection has been lost - in
 | 
					      // message. An exception is if the connection has been lost - in
 | 
				
			||||||
      // that case we should treat this as a "first-time" status message.
 | 
					      // that case we should treat this as a "first-time" status message.
 | 
				
			||||||
      // this.comp.es is used as a proxy for this.
 | 
					      // !this.comp.es is used to test this condition.
 | 
				
			||||||
      if (msg.type === 'status' && (!this.comp || !this.comp.es)) {
 | 
					      if (msg.type === 'status' && (!this.comp || !this.comp.es)) {
 | 
				
			||||||
        next(comp => {
 | 
					        next(comp => {
 | 
				
			||||||
          // Set up bidirectional reference
 | 
					          // Set up bidirectional reference
 | 
				
			||||||
@ -72,6 +69,7 @@ const ServerEventHandler = function() {
 | 
				
			|||||||
    es.onerror = function(e) {
 | 
					    es.onerror = function(e) {
 | 
				
			||||||
      this.comp.$root.connected = false;
 | 
					      this.comp.$root.connected = false;
 | 
				
			||||||
      setTimeout(() => {
 | 
					      setTimeout(() => {
 | 
				
			||||||
 | 
					        // Recrate the EventSource, passing in the existing component
 | 
				
			||||||
        this.comp.es = setupEventSource(to, query, null, this.comp);
 | 
					        this.comp.es = setupEventSource(to, query, null, this.comp);
 | 
				
			||||||
      }, this.comp.esReconnectInterval);
 | 
					      }, this.comp.esReconnectInterval);
 | 
				
			||||||
      if(this.comp.esReconnectInterval < 7500)
 | 
					      if(this.comp.esReconnectInterval < 7500)
 | 
				
			||||||
@ -95,81 +93,26 @@ const ServerEventHandler = function() {
 | 
				
			|||||||
    methods: {
 | 
					    methods: {
 | 
				
			||||||
      query(q) {
 | 
					      query(q) {
 | 
				
			||||||
        this.es.close();
 | 
					        this.es.close();
 | 
				
			||||||
        setupEventSource(this.es.path, '?' + Object.entries(q).map(([k,v])=>`${k}=${v}`).join('&'), (fn) => { fn(this); });
 | 
					        setupEventSource(this.es.to, '?' + Object.entries(q).map(([k,v])=>`${k}=${v}`).join('&'), fn => fn(this));
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}();
 | 
					})());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Utils = {
 | 
					// Mixin for periodically updating a progress bar
 | 
				
			||||||
  methods: {
 | 
					Vue.mixin({
 | 
				
			||||||
    runIcon(result) {
 | 
					  data: () => ({ jobsRunning: [] }),
 | 
				
			||||||
      return (result == 'success') ? /* checkmark */
 | 
					 | 
				
			||||||
               `<svg class="status success" viewBox="0 0 100 100">
 | 
					 | 
				
			||||||
                 <path d="m 23,46 c -6,0 -17,3 -17,11 0,8 9,30 12,32 3,2 14,5 20,-2 6,-6 24,-36
 | 
					 | 
				
			||||||
                  56,-71 5,-3 -9,-8 -23,-2 -13,6 -33,42 -41,47 -6,-3 -5,-12 -8,-15 z" />
 | 
					 | 
				
			||||||
                </svg>`
 | 
					 | 
				
			||||||
           : (result == 'failed' || result == 'aborted') ? /* cross */
 | 
					 | 
				
			||||||
               `<svg class="status failed" viewBox="0 0 100 100">
 | 
					 | 
				
			||||||
                 <path d="m 19,20 c 2,8 12,29 15,32 -5,5 -18,21 -21,26 2,3 8,15 11,18 4,-6 17,-21
 | 
					 | 
				
			||||||
                  21,-26 5,5 11,15 15,20 8,-2 15,-9 20,-15 -3,-3 -17,-18 -20,-24 3,-5 23,-26 30,-33 -3,-5 -8,-9
 | 
					 | 
				
			||||||
                  -12,-12 -6,5 -26,26 -29,30 -6,-8 -11,-15 -15,-23 -3,0 -12,5 -15,7 z" />
 | 
					 | 
				
			||||||
                </svg>`
 | 
					 | 
				
			||||||
           : (result == 'queued') ? /* clock */
 | 
					 | 
				
			||||||
                `<svg class="status queued" viewBox="0 0 100 100">
 | 
					 | 
				
			||||||
                  <circle r="50" cy="50" cx="50" />
 | 
					 | 
				
			||||||
                  <path d="m 50,15 0,35 17,17" stroke-width="10" fill="none" />
 | 
					 | 
				
			||||||
                </svg>`
 | 
					 | 
				
			||||||
           : /* spinner */
 | 
					 | 
				
			||||||
                `<svg class="status running" viewBox="0 0 100 100">
 | 
					 | 
				
			||||||
                  <circle cx="50" cy="50" r="40" stroke-width="15" fill="none" stroke-dasharray="175">
 | 
					 | 
				
			||||||
                   <animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="2s" values="0 50 50;360 50 50"></animateTransform>
 | 
					 | 
				
			||||||
                  </circle>
 | 
					 | 
				
			||||||
                 </svg>`
 | 
					 | 
				
			||||||
           ;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    formatDate: function(unix) {
 | 
					 | 
				
			||||||
      // TODO: reimplement when toLocaleDateString() accepts formatting options on most browsers
 | 
					 | 
				
			||||||
      var d = new Date(1000 * unix);
 | 
					 | 
				
			||||||
      var m = d.getMinutes();
 | 
					 | 
				
			||||||
      if (m < 10) m = '0' + m;
 | 
					 | 
				
			||||||
      return d.getHours() + ':' + m + ' 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();
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    formatDuration: function(start, end) {
 | 
					 | 
				
			||||||
      if(!end)
 | 
					 | 
				
			||||||
        end = Math.floor(Date.now()/1000) + this.$root.clockSkew;
 | 
					 | 
				
			||||||
      if(end - start > 3600)
 | 
					 | 
				
			||||||
        return Math.floor((end-start)/3600) + ' hours, ' + Math.floor(((end-start)%3600)/60) + ' minutes';
 | 
					 | 
				
			||||||
      else if(end - start > 60)
 | 
					 | 
				
			||||||
        return Math.floor((end-start)/60) + ' minutes, ' + ((end-start)%60) + ' seconds';
 | 
					 | 
				
			||||||
      else
 | 
					 | 
				
			||||||
        return (end-start) + ' seconds';
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const ProgressUpdater = {
 | 
					 | 
				
			||||||
  data() { return { jobsRunning: [] }; },
 | 
					 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
    updateProgress(o) {
 | 
					    updateProgress(o) {
 | 
				
			||||||
      if (o.etc) {
 | 
					      if (o.etc) {
 | 
				
			||||||
        var p = (Math.floor(Date.now()/1000) + this.$root.clockSkew - o.started) / (o.etc - o.started);
 | 
					        const p = (Math.floor(Date.now()/1000) + this.$root.clockSkew - o.started) / (o.etc - o.started);
 | 
				
			||||||
        if (p > 1.2) {
 | 
					        if (p > 1.2)
 | 
				
			||||||
          o.overtime = true;
 | 
					          o.overtime = true;
 | 
				
			||||||
        }
 | 
					        o.progress = (p >= 1) ? 99 : 100 * p;
 | 
				
			||||||
        if (p >= 1) {
 | 
					 | 
				
			||||||
          o.progress = 99;
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
          o.progress = 100 * p;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  beforeDestroy() {
 | 
					  beforeDestroy: () => {
 | 
				
			||||||
    clearInterval(this.updateTimer);
 | 
					    clearInterval(this.updateTimer);
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  watch: {
 | 
					  watch: {
 | 
				
			||||||
@ -188,30 +131,296 @@ const ProgressUpdater = {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
};
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Home = function() {
 | 
					// Utility methods
 | 
				
			||||||
  var state = {
 | 
					Vue.mixin({
 | 
				
			||||||
 | 
					  methods: {
 | 
				
			||||||
 | 
					    // Get an svg icon given a run result
 | 
				
			||||||
 | 
					    runIcon: result =>
 | 
				
			||||||
 | 
					      (result == 'success') ? /* checkmark */
 | 
				
			||||||
 | 
					        `<svg class="status success" viewBox="0 0 100 100">
 | 
				
			||||||
 | 
					          <path d="m 23,46 c -6,0 -17,3 -17,11 0,8 9,30 12,32 3,2 14,5 20,-2 6,-6 24,-36
 | 
				
			||||||
 | 
					           56,-71 5,-3 -9,-8 -23,-2 -13,6 -33,42 -41,47 -6,-3 -5,-12 -8,-15 z" />
 | 
				
			||||||
 | 
					         </svg>`
 | 
				
			||||||
 | 
					      : (result == 'failed' || result == 'aborted') ? /* cross */
 | 
				
			||||||
 | 
					        `<svg class="status failed" viewBox="0 0 100 100">
 | 
				
			||||||
 | 
					          <path d="m 19,20 c 2,8 12,29 15,32 -5,5 -18,21 -21,26 2,3 8,15 11,18 4,-6 17,-21
 | 
				
			||||||
 | 
					           21,-26 5,5 11,15 15,20 8,-2 15,-9 20,-15 -3,-3 -17,-18 -20,-24 3,-5 23,-26 30,-33 -3,-5 -8,-9
 | 
				
			||||||
 | 
					           -12,-12 -6,5 -26,26 -29,30 -6,-8 -11,-15 -15,-23 -3,0 -12,5 -15,7 z" />
 | 
				
			||||||
 | 
					         </svg>`
 | 
				
			||||||
 | 
					      : (result == 'queued') ? /* clock */
 | 
				
			||||||
 | 
					        `<svg class="status queued" viewBox="0 0 100 100">
 | 
				
			||||||
 | 
					          <circle r="50" cy="50" cx="50" />
 | 
				
			||||||
 | 
					          <path d="m 50,15 0,35 17,17" stroke-width="10" fill="none" />
 | 
				
			||||||
 | 
					         </svg>`
 | 
				
			||||||
 | 
					      : /* spinner */
 | 
				
			||||||
 | 
					        `<svg class="status running" viewBox="0 0 100 100">
 | 
				
			||||||
 | 
					          <circle cx="50" cy="50" r="40" stroke-width="15" fill="none" stroke-dasharray="175">
 | 
				
			||||||
 | 
					           <animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" dur="2s" values="0 50 50;360 50 50"></animateTransform>
 | 
				
			||||||
 | 
					          </circle>
 | 
				
			||||||
 | 
					         </svg>`,
 | 
				
			||||||
 | 
					    // Pretty-print a unix date
 | 
				
			||||||
 | 
					    formatDate: unix => {
 | 
				
			||||||
 | 
					      // TODO: reimplement when toLocaleDateString() accepts formatting options on most browsers
 | 
				
			||||||
 | 
					      const d = new Date(1000 * unix);
 | 
				
			||||||
 | 
					      let m = d.getMinutes();
 | 
				
			||||||
 | 
					      if (m < 10)
 | 
				
			||||||
 | 
					        m = '0' + m;
 | 
				
			||||||
 | 
					      return d.getHours() + ':' + m + ' 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();
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    // Pretty-print a duration
 | 
				
			||||||
 | 
					    formatDuration: function(start, end) {
 | 
				
			||||||
 | 
					      if(!end)
 | 
				
			||||||
 | 
					        end = Math.floor(Date.now()/1000) + this.$root.clockSkew;
 | 
				
			||||||
 | 
					      if(end - start > 3600)
 | 
				
			||||||
 | 
					        return Math.floor((end-start)/3600) + ' hours, ' + Math.floor(((end-start)%3600)/60) + ' minutes';
 | 
				
			||||||
 | 
					      else if(end - start > 60)
 | 
				
			||||||
 | 
					        return Math.floor((end-start)/60) + ' minutes, ' + ((end-start)%60) + ' seconds';
 | 
				
			||||||
 | 
					      else
 | 
				
			||||||
 | 
					        return (end-start) + ' seconds';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Chart factory
 | 
				
			||||||
 | 
					const Charts = (() => {
 | 
				
			||||||
 | 
					  // TODO usage is broken!
 | 
				
			||||||
 | 
					  const timeScale = max => max > 3600
 | 
				
			||||||
 | 
					    ? { factor: 1/3600, ticks: v => v.toFixed(1), label:'Hours' }
 | 
				
			||||||
 | 
					    : max > 60
 | 
				
			||||||
 | 
					    ? { factor: 1/60, ticks: v => v.toFixed(1), label:'Minutes' }
 | 
				
			||||||
 | 
					    : { factor: 1, ticks: v => v, label:'Seconds' };
 | 
				
			||||||
 | 
					  return {
 | 
				
			||||||
 | 
					    createExecutorUtilizationChart: (id, nBusy, nTotal) => {
 | 
				
			||||||
 | 
					      const c = new Chart(document.getElementById(id), {
 | 
				
			||||||
 | 
					        type: 'pie',
 | 
				
			||||||
 | 
					        data: {
 | 
				
			||||||
 | 
					          labels: [ "Busy", "Idle" ],
 | 
				
			||||||
 | 
					          datasets: [{
 | 
				
			||||||
 | 
					            data: [ nBusy, nTotal - nBusy ],
 | 
				
			||||||
 | 
					            backgroundColor: [ "#afa674", "#7483af" ]
 | 
				
			||||||
 | 
					          }]
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        options: {
 | 
				
			||||||
 | 
					          hover: { mode: null }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      c.executorBusyChanged = busy => {
 | 
				
			||||||
 | 
					        c.data.datasets[0].data[0] += busy ? 1 : -1;
 | 
				
			||||||
 | 
					        c.data.datasets[0].data[1] -= busy ? 1 : -1;
 | 
				
			||||||
 | 
					        c.update();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return c;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    createRunsPerDayChart: (id, data) => {
 | 
				
			||||||
 | 
					      const dayNames = (() => {
 | 
				
			||||||
 | 
					        const res = [];
 | 
				
			||||||
 | 
					        var now = new Date();
 | 
				
			||||||
 | 
					        for (var i = 6; i >= 0; --i) {
 | 
				
			||||||
 | 
					          var then = new Date(now.getTime() - i * 86400000);
 | 
				
			||||||
 | 
					          res.push({
 | 
				
			||||||
 | 
					            short: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][then.getDay()],
 | 
				
			||||||
 | 
					            long: then.toLocaleDateString()}
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return res;
 | 
				
			||||||
 | 
					      })();
 | 
				
			||||||
 | 
					      const c = new Chart(document.getElementById(id), {
 | 
				
			||||||
 | 
					        type: 'line',
 | 
				
			||||||
 | 
					        data: {
 | 
				
			||||||
 | 
					          labels: dayNames.map(e => e.short),
 | 
				
			||||||
 | 
					          datasets: [{
 | 
				
			||||||
 | 
					            label: 'Failed Builds',
 | 
				
			||||||
 | 
					            backgroundColor: "#883d3d",
 | 
				
			||||||
 | 
					            data: data.map(e => e.failed || 0)
 | 
				
			||||||
 | 
					          },{
 | 
				
			||||||
 | 
					            label: 'Successful Builds',
 | 
				
			||||||
 | 
					            backgroundColor: "#74af77",
 | 
				
			||||||
 | 
					            data: data.map(e => e.success || 0)
 | 
				
			||||||
 | 
					          }]
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        options:{
 | 
				
			||||||
 | 
					          title: { display: true, text: 'Builds per day' },
 | 
				
			||||||
 | 
					          tooltips:{callbacks:{title: (tip, data) => dayNames[tip[0].index].long}},
 | 
				
			||||||
 | 
					          scales:{yAxes:[{
 | 
				
			||||||
 | 
					            ticks:{userCallback: (label, index, labels) => Number.isInteger(label) ? label: null},
 | 
				
			||||||
 | 
					            stacked: true
 | 
				
			||||||
 | 
					          }]}
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      c.jobCompleted = success => {
 | 
				
			||||||
 | 
					        c.data.datasets[success ? 0 : 1].data[6]++;
 | 
				
			||||||
 | 
					        c.update();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return c;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    createRunsPerJobChart: (id, data) => {
 | 
				
			||||||
 | 
					      const c = new Chart(document.getElementById("chartBpj"), {
 | 
				
			||||||
 | 
					        type: 'horizontalBar',
 | 
				
			||||||
 | 
					        data: {
 | 
				
			||||||
 | 
					          labels: Object.keys(data),
 | 
				
			||||||
 | 
					          datasets: [{
 | 
				
			||||||
 | 
					            label: 'Runs in last 24 hours',
 | 
				
			||||||
 | 
					            backgroundColor: "#7483af",
 | 
				
			||||||
 | 
					            data: Object.keys(data).map(e => data[e])
 | 
				
			||||||
 | 
					          }]
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        options:{
 | 
				
			||||||
 | 
					          title: { display: true, text: 'Builds per job' },
 | 
				
			||||||
 | 
					          hover: { mode: null },
 | 
				
			||||||
 | 
					          scales:{xAxes:[{ticks:{userCallback: (label, index, labels)=> Number.isInteger(label) ? label: null}}]}
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      c.jobCompleted = name => {
 | 
				
			||||||
 | 
					        for (var j = 0; j < c.data.datasets[0].data.length; ++j) {
 | 
				
			||||||
 | 
					          if (c.data.labels[j] == name) {
 | 
				
			||||||
 | 
					            c.data.datasets[0].data[j]++;
 | 
				
			||||||
 | 
					            c.update();
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return c;
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    createTimePerJobChart: (id, data) => {
 | 
				
			||||||
 | 
					      const scale = timeScale(Math.max(...Object.values(data)));
 | 
				
			||||||
 | 
					      return new Chart(document.getElementById(id), {
 | 
				
			||||||
 | 
					        type: 'horizontalBar',
 | 
				
			||||||
 | 
					        data: {
 | 
				
			||||||
 | 
					          labels: Object.keys(data),
 | 
				
			||||||
 | 
					          datasets: [{
 | 
				
			||||||
 | 
					            label: 'Mean run time this week',
 | 
				
			||||||
 | 
					            backgroundColor: "#7483af",
 | 
				
			||||||
 | 
					            data: Object.keys(data).map(e => data[e] * scale.factor)
 | 
				
			||||||
 | 
					          }]
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        options:{
 | 
				
			||||||
 | 
					          title: { display: true, text: 'Mean run time this week' },
 | 
				
			||||||
 | 
					          hover: { mode: null },
 | 
				
			||||||
 | 
					          scales:{xAxes:[{
 | 
				
			||||||
 | 
					            ticks:{userCallback: scale.ticks},
 | 
				
			||||||
 | 
					            scaleLabel: {
 | 
				
			||||||
 | 
					              display: true,
 | 
				
			||||||
 | 
					              labelString: scale.label
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          }]},
 | 
				
			||||||
 | 
					          tooltips:{callbacks:{
 | 
				
			||||||
 | 
					            label: (tip, data) => data.datasets[tip.datasetIndex].label + ': ' + tip.xLabel + ' ' + scale.label.toLowerCase()
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    createRunTimeChangesChart: (id, data) => {
 | 
				
			||||||
 | 
					      const scale = timeScale(Math.max(...data.map(e => Math.max(...e.durations))));
 | 
				
			||||||
 | 
					      return new Chart(document.getElementById(id), {
 | 
				
			||||||
 | 
					        type: 'line',
 | 
				
			||||||
 | 
					        data: {
 | 
				
			||||||
 | 
					          labels: [...Array(10).keys()],
 | 
				
			||||||
 | 
					          datasets: data.map(e => ({
 | 
				
			||||||
 | 
					            label: e.name,
 | 
				
			||||||
 | 
					            data: e.durations.map(x => x * scale.factor),
 | 
				
			||||||
 | 
					            borderColor: 'hsl('+(e.name.hashCode() % 360)+', 27%, 57%)',
 | 
				
			||||||
 | 
					            backgroundColor: 'transparent'
 | 
				
			||||||
 | 
					          }))
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        options:{
 | 
				
			||||||
 | 
					          title: { display: true, text: 'Build time changes' },
 | 
				
			||||||
 | 
					          legend:{ display: true, position: 'bottom' },
 | 
				
			||||||
 | 
					          scales:{
 | 
				
			||||||
 | 
					            xAxes:[{ticks:{display: false}}],
 | 
				
			||||||
 | 
					            yAxes:[{
 | 
				
			||||||
 | 
					              ticks:{userCallback: scale.ticks},
 | 
				
			||||||
 | 
					              scaleLabel: {
 | 
				
			||||||
 | 
					                display: true,
 | 
				
			||||||
 | 
					                labelString: scale.label
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }]
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          tooltips:{
 | 
				
			||||||
 | 
					            enabled:false
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    createRunTimeChart: (id, jobs, avg) => {
 | 
				
			||||||
 | 
					      const scale = timeScale(Math.max(...jobs.map(v=>v.completed-v.started)));
 | 
				
			||||||
 | 
					      return new Chart(document.getElementById(id), {
 | 
				
			||||||
 | 
					        type: 'bar',
 | 
				
			||||||
 | 
					        data: {
 | 
				
			||||||
 | 
					          labels: jobs.map(e => '#' + e.number).reverse(),
 | 
				
			||||||
 | 
					          datasets: [{
 | 
				
			||||||
 | 
					            label: 'Average',
 | 
				
			||||||
 | 
					            type: 'line',
 | 
				
			||||||
 | 
					            data: [{x:0, y:avg * scale.factor}, {x:1, y:avg * scale.factor}],
 | 
				
			||||||
 | 
					            borderColor: '#7483af',
 | 
				
			||||||
 | 
					            backgroundColor: 'transparent',
 | 
				
			||||||
 | 
					            xAxisID: 'avg',
 | 
				
			||||||
 | 
					            pointRadius: 0,
 | 
				
			||||||
 | 
					            pointHitRadius: 0,
 | 
				
			||||||
 | 
					            pointHoverRadius: 0,
 | 
				
			||||||
 | 
					          },{
 | 
				
			||||||
 | 
					            label: 'Build time',
 | 
				
			||||||
 | 
					            backgroundColor: jobs.map(e => e.result == 'success' ? '#74af77': '#883d3d').reverse(),
 | 
				
			||||||
 | 
					            data: jobs.map(e => (e.completed - e.started) * scale.factor).reverse()
 | 
				
			||||||
 | 
					          }]
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        options: {
 | 
				
			||||||
 | 
					          title: { display: true, text: 'Build time' },
 | 
				
			||||||
 | 
					          hover: { mode: null },
 | 
				
			||||||
 | 
					          scales:{
 | 
				
			||||||
 | 
					            xAxes:[{
 | 
				
			||||||
 | 
					              categoryPercentage: 0.95,
 | 
				
			||||||
 | 
					              barPercentage: 1.0
 | 
				
			||||||
 | 
					            },{
 | 
				
			||||||
 | 
					              id: 'avg',
 | 
				
			||||||
 | 
					              type: 'linear',
 | 
				
			||||||
 | 
					              ticks: {
 | 
				
			||||||
 | 
					                display: false
 | 
				
			||||||
 | 
					              },
 | 
				
			||||||
 | 
					              gridLines: {
 | 
				
			||||||
 | 
					                display: false,
 | 
				
			||||||
 | 
					                drawBorder: false
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            }],
 | 
				
			||||||
 | 
					            yAxes:[{
 | 
				
			||||||
 | 
					              ticks:{userCallback: scale.ticks},
 | 
				
			||||||
 | 
					              scaleLabel:{display: true, labelString: scale.label}
 | 
				
			||||||
 | 
					            }]
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          tooltips:{callbacks:{
 | 
				
			||||||
 | 
					            label: (tip, data) => scale.ticks(tip.yLabel) + ' ' + scale.label.toLowerCase()
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					})();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// For all charts, set miniumum Y to 0
 | 
				
			||||||
 | 
					Chart.scaleService.updateScaleDefaults('linear', {
 | 
				
			||||||
 | 
					    ticks: { suggestedMin: 0 }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					// Don't display legend by default
 | 
				
			||||||
 | 
					Chart.defaults.global.legend.display = false;
 | 
				
			||||||
 | 
					// Disable tooltip hover animations
 | 
				
			||||||
 | 
					Chart.defaults.global.hover.animationDuration = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Component for the / endpoint
 | 
				
			||||||
 | 
					const Home = templateId => {
 | 
				
			||||||
 | 
					  const state = {
 | 
				
			||||||
    jobsQueued: [],
 | 
					    jobsQueued: [],
 | 
				
			||||||
    jobsRecent: [],
 | 
					    jobsRecent: [],
 | 
				
			||||||
    resultChanged: [],
 | 
					    resultChanged: [],
 | 
				
			||||||
    lowPassRates: [],
 | 
					    lowPassRates: [],
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					  let chtUtilization, chtBuildsPerDay, chtBuildsPerJob, chtTimePerJob;
 | 
				
			||||||
  var chtUtilization, chtBuildsPerDay, chtBuildsPerJob, chtTimePerJob;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  var updateUtilization = function(busy) {
 | 
					 | 
				
			||||||
    chtUtilization.data.datasets[0].data[0] += busy ? 1 : -1;
 | 
					 | 
				
			||||||
    chtUtilization.data.datasets[0].data[1] -= busy ? 1 : -1;
 | 
					 | 
				
			||||||
    chtUtilization.update();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    template: '#home',
 | 
					    template: templateId,
 | 
				
			||||||
    mixins: [ServerEventHandler, Utils, ProgressUpdater],
 | 
					    data: () => state,
 | 
				
			||||||
    data: function() {
 | 
					 | 
				
			||||||
      return state;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    methods: {
 | 
					    methods: {
 | 
				
			||||||
      status: function(msg) {
 | 
					      status: function(msg) {
 | 
				
			||||||
        state.jobsQueued = msg.queued;
 | 
					        state.jobsQueued = msg.queued;
 | 
				
			||||||
@ -221,135 +430,11 @@ const Home = function() {
 | 
				
			|||||||
        state.lowPassRates = msg.lowPassRates;
 | 
					        state.lowPassRates = msg.lowPassRates;
 | 
				
			||||||
        this.$forceUpdate();
 | 
					        this.$forceUpdate();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        // setup charts
 | 
					        chtUtilization = Charts.createExecutorUtilizationChart("chartUtil", msg.executorsBusy, msg.executorsTotal);
 | 
				
			||||||
        chtUtilization = new Chart(document.getElementById("chartUtil"), {
 | 
					        chtBuildsPerDay = Charts.createRunsPerDayChart("chartBpd", msg.buildsPerDay);
 | 
				
			||||||
          type: 'pie',
 | 
					        chtBuildsPerJob = Charts.createRunsPerJobChart("chartBpj", msg.buildsPerJob);
 | 
				
			||||||
          data: {
 | 
					        chtTimePerJob = Charts.createTimePerJobChart("chartTpj", msg.timePerJob);
 | 
				
			||||||
            labels: ["Busy", "Idle"],
 | 
					        chtBuildTimeChanges = Charts.createRunTimeChangesChart("chartBuildTimeChanges", msg.buildTimeChanges);
 | 
				
			||||||
            datasets: [{
 | 
					 | 
				
			||||||
              data: [ msg.executorsBusy, msg.executorsTotal - msg.executorsBusy ],
 | 
					 | 
				
			||||||
              backgroundColor: ["#afa674", "#7483af"]
 | 
					 | 
				
			||||||
            }]
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          options: {
 | 
					 | 
				
			||||||
            hover: { mode: null }
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        var buildsPerDayDates = function(){
 | 
					 | 
				
			||||||
          res = [];
 | 
					 | 
				
			||||||
          var now = new Date();
 | 
					 | 
				
			||||||
          for (var i = 6; i >= 0; --i) {
 | 
					 | 
				
			||||||
            var then = new Date(now.getTime() - i * 86400000);
 | 
					 | 
				
			||||||
            res.push({
 | 
					 | 
				
			||||||
              short: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][then.getDay()],
 | 
					 | 
				
			||||||
              long: then.toLocaleDateString()}
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          return res;
 | 
					 | 
				
			||||||
        }();
 | 
					 | 
				
			||||||
        chtBuildsPerDay = new Chart(document.getElementById("chartBpd"), {
 | 
					 | 
				
			||||||
          type: 'line',
 | 
					 | 
				
			||||||
          data: {
 | 
					 | 
				
			||||||
            labels: buildsPerDayDates.map((e)=>{ return e.short; }),
 | 
					 | 
				
			||||||
            datasets: [{
 | 
					 | 
				
			||||||
              label: 'Failed Builds',
 | 
					 | 
				
			||||||
              backgroundColor: "#883d3d",
 | 
					 | 
				
			||||||
              data: msg.buildsPerDay.map((e)=>{ return e.failed || 0; })
 | 
					 | 
				
			||||||
            },{
 | 
					 | 
				
			||||||
              label: 'Successful Builds',
 | 
					 | 
				
			||||||
              backgroundColor: "#74af77",
 | 
					 | 
				
			||||||
              data: msg.buildsPerDay.map((e)=>{ return e.success || 0; })
 | 
					 | 
				
			||||||
            }]
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          options:{
 | 
					 | 
				
			||||||
            title: { display: true, text: 'Builds per day' },
 | 
					 | 
				
			||||||
            tooltips:{callbacks:{title: function(tip, data) {
 | 
					 | 
				
			||||||
              return buildsPerDayDates[tip[0].index].long;
 | 
					 | 
				
			||||||
            }}},
 | 
					 | 
				
			||||||
            scales:{yAxes:[{
 | 
					 | 
				
			||||||
              ticks:{userCallback: (label, index, labels)=>{
 | 
					 | 
				
			||||||
                if(Number.isInteger(label))
 | 
					 | 
				
			||||||
                  return label;
 | 
					 | 
				
			||||||
              }},
 | 
					 | 
				
			||||||
              stacked: true
 | 
					 | 
				
			||||||
            }]}
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        chtBuildsPerJob = new Chart(document.getElementById("chartBpj"), {
 | 
					 | 
				
			||||||
          type: 'horizontalBar',
 | 
					 | 
				
			||||||
          data: {
 | 
					 | 
				
			||||||
            labels: Object.keys(msg.buildsPerJob),
 | 
					 | 
				
			||||||
            datasets: [{
 | 
					 | 
				
			||||||
              label: 'Runs in last 24 hours',
 | 
					 | 
				
			||||||
              backgroundColor: "#7483af",
 | 
					 | 
				
			||||||
              data: Object.keys(msg.buildsPerJob).map((e)=>{ return msg.buildsPerJob[e]; })
 | 
					 | 
				
			||||||
            }]
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          options:{
 | 
					 | 
				
			||||||
            title: { display: true, text: 'Builds per job' },
 | 
					 | 
				
			||||||
            hover: { mode: null },
 | 
					 | 
				
			||||||
            scales:{xAxes:[{ticks:{userCallback: (label, index, labels)=>{
 | 
					 | 
				
			||||||
              if(Number.isInteger(label))
 | 
					 | 
				
			||||||
                return label;
 | 
					 | 
				
			||||||
            }}}]}
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        var tpjScale = timeScale(Math.max(...Object.values(msg.timePerJob)));
 | 
					 | 
				
			||||||
        chtTimePerJob = new Chart(document.getElementById("chartTpj"), {
 | 
					 | 
				
			||||||
          type: 'horizontalBar',
 | 
					 | 
				
			||||||
          data: {
 | 
					 | 
				
			||||||
            labels: Object.keys(msg.timePerJob),
 | 
					 | 
				
			||||||
            datasets: [{
 | 
					 | 
				
			||||||
              label: 'Mean run time this week',
 | 
					 | 
				
			||||||
              backgroundColor: "#7483af",
 | 
					 | 
				
			||||||
              data: Object.keys(msg.timePerJob).map((e)=>{ return msg.timePerJob[e]; })
 | 
					 | 
				
			||||||
            }]
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          options:{
 | 
					 | 
				
			||||||
            title: { display: true, text: 'Mean run time this week' },
 | 
					 | 
				
			||||||
            hover: { mode: null },
 | 
					 | 
				
			||||||
            scales:{xAxes:[{
 | 
					 | 
				
			||||||
              ticks:{userCallback: tpjScale.scale},
 | 
					 | 
				
			||||||
              scaleLabel: {
 | 
					 | 
				
			||||||
                display: true,
 | 
					 | 
				
			||||||
                labelString: tpjScale.label
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            }]},
 | 
					 | 
				
			||||||
            tooltips:{callbacks:{label:(tip, data)=>{
 | 
					 | 
				
			||||||
              return data.datasets[tip.datasetIndex].label + ': ' + tip.xLabel + ' ' + tpjScale.label.toLowerCase();
 | 
					 | 
				
			||||||
            }}}
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        const btcScale = timeScale(Math.max(...msg.buildTimeChanges.map(e=>Math.max(...e.durations))));
 | 
					 | 
				
			||||||
        var chtBuildTimeChanges = new Chart(document.getElementById("chartBuildTimeChanges"), {
 | 
					 | 
				
			||||||
          type: 'line',
 | 
					 | 
				
			||||||
          data: {
 | 
					 | 
				
			||||||
            labels: [...Array(10).keys()],
 | 
					 | 
				
			||||||
            datasets: msg.buildTimeChanges.map((e)=>{return {
 | 
					 | 
				
			||||||
              label: e.name,
 | 
					 | 
				
			||||||
              data: e.durations,
 | 
					 | 
				
			||||||
              borderColor: 'hsl('+(e.name.hashCode() % 360)+', 27%, 57%)',
 | 
					 | 
				
			||||||
              backgroundColor: 'transparent'
 | 
					 | 
				
			||||||
            }})
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          options:{
 | 
					 | 
				
			||||||
            title: { display: true, text: 'Build time changes' },
 | 
					 | 
				
			||||||
            legend:{ display: true, position: 'bottom' },
 | 
					 | 
				
			||||||
            scales:{
 | 
					 | 
				
			||||||
              xAxes:[{ticks:{display: false}}],
 | 
					 | 
				
			||||||
              yAxes:[{
 | 
					 | 
				
			||||||
                ticks:{userCallback: btcScale.scale},
 | 
					 | 
				
			||||||
                scaleLabel: {
 | 
					 | 
				
			||||||
                  display: true,
 | 
					 | 
				
			||||||
                  labelString: btcScale.label
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
              }]
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            tooltips:{
 | 
					 | 
				
			||||||
              enabled:false
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      job_queued: function(data) {
 | 
					      job_queued: function(data) {
 | 
				
			||||||
        state.jobsQueued.splice(0, 0, data);
 | 
					        state.jobsQueued.splice(0, 0, data);
 | 
				
			||||||
@ -359,15 +444,9 @@ const Home = function() {
 | 
				
			|||||||
        state.jobsQueued.splice(state.jobsQueued.length - data.queueIndex - 1, 1);
 | 
					        state.jobsQueued.splice(state.jobsQueued.length - data.queueIndex - 1, 1);
 | 
				
			||||||
        state.jobsRunning.splice(0, 0, data);
 | 
					        state.jobsRunning.splice(0, 0, data);
 | 
				
			||||||
        this.$forceUpdate();
 | 
					        this.$forceUpdate();
 | 
				
			||||||
        updateUtilization(true);
 | 
					        chtUtilization.executorBusyChanged(true);
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      job_completed: function(data) {
 | 
					      job_completed: function(data) {
 | 
				
			||||||
        if (data.result === "success")
 | 
					 | 
				
			||||||
          chtBuildsPerDay.data.datasets[0].data[6]++;
 | 
					 | 
				
			||||||
        else
 | 
					 | 
				
			||||||
          chtBuildsPerDay.data.datasets[1].data[6]++;
 | 
					 | 
				
			||||||
        chtBuildsPerDay.update();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        for (var i = 0; i < state.jobsRunning.length; ++i) {
 | 
					        for (var i = 0; i < state.jobsRunning.length; ++i) {
 | 
				
			||||||
          var job = state.jobsRunning[i];
 | 
					          var job = state.jobsRunning[i];
 | 
				
			||||||
          if (job.name == data.name && job.number == data.number) {
 | 
					          if (job.name == data.name && job.number == data.number) {
 | 
				
			||||||
@ -377,21 +456,17 @@ const Home = function() {
 | 
				
			|||||||
            break;
 | 
					            break;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        updateUtilization(false);
 | 
					        chtBuildsPerDay.jobCompleted(data.result === 'success')
 | 
				
			||||||
        for (var j = 0; j < chtBuildsPerJob.data.datasets[0].data.length; ++j) {
 | 
					        chtUtilization.executorBusyChanged(false);
 | 
				
			||||||
          if (chtBuildsPerJob.data.labels[j] == job.name) {
 | 
					        chtBuildsPerJob.jobCompleted(data.name)
 | 
				
			||||||
            chtBuildsPerJob.data.datasets[0].data[j]++;
 | 
					 | 
				
			||||||
            chtBuildsPerJob.update();
 | 
					 | 
				
			||||||
            break;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}();
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const All = function(templateId) {
 | 
					// Component for the /jobs and /wallboard endpoints
 | 
				
			||||||
  var state = {
 | 
					const All = templateId => {
 | 
				
			||||||
 | 
					  const state = {
 | 
				
			||||||
    jobs: [],
 | 
					    jobs: [],
 | 
				
			||||||
    search: '',
 | 
					    search: '',
 | 
				
			||||||
    groups: {},
 | 
					    groups: {},
 | 
				
			||||||
@ -401,23 +476,22 @@ const All = function(templateId) {
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    template: templateId,
 | 
					    template: templateId,
 | 
				
			||||||
    mixins: [ServerEventHandler, Utils, ProgressUpdater],
 | 
					    data: () => state,
 | 
				
			||||||
    data: function() { return state; },
 | 
					 | 
				
			||||||
    methods: {
 | 
					    methods: {
 | 
				
			||||||
      status: function(msg) {
 | 
					      status: function(msg) {
 | 
				
			||||||
        state.jobs = msg.jobs;
 | 
					        state.jobs = msg.jobs;
 | 
				
			||||||
        state.jobsRunning = msg.running;
 | 
					        state.jobsRunning = msg.running;
 | 
				
			||||||
        // mix running and completed jobs
 | 
					        // mix running and completed jobs
 | 
				
			||||||
        for (var i in msg.running) {
 | 
					        msg.running.forEach(job => {
 | 
				
			||||||
          var idx = state.jobs.findIndex(job => job.name === msg.running[i].name);
 | 
					          const idx = state.jobs.findIndex(j => j.name === job.name);
 | 
				
			||||||
          if (idx > -1)
 | 
					          if (idx > -1)
 | 
				
			||||||
            state.jobs[idx] = msg.running[i];
 | 
					            state.jobs[idx] = job;
 | 
				
			||||||
          else {
 | 
					          else {
 | 
				
			||||||
            // special case: first run of a job.
 | 
					            // special case: first run of a job.
 | 
				
			||||||
            state.jobs.unshift(msg.running[i]);
 | 
					            state.jobs.unshift(j);
 | 
				
			||||||
            state.jobs.sort(function(a, b){return a.name < b.name ? -1 : a.name > b.name ? 1 : 0;});
 | 
					            state.jobs.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        });
 | 
				
			||||||
        state.groups = {};
 | 
					        state.groups = {};
 | 
				
			||||||
        Object.keys(msg.groups).forEach(k => state.regexps[k] = new RegExp(state.groups[k] = 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.regexps).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);
 | 
				
			||||||
@ -425,30 +499,18 @@ const All = function(templateId) {
 | 
				
			|||||||
      },
 | 
					      },
 | 
				
			||||||
      job_started: function(data) {
 | 
					      job_started: function(data) {
 | 
				
			||||||
        data.result = 'running'; // for wallboard css
 | 
					        data.result = 'running'; // for wallboard css
 | 
				
			||||||
        var updAt = null;
 | 
					 | 
				
			||||||
        // jobsRunning must be maintained for ProgressUpdater
 | 
					        // jobsRunning must be maintained for ProgressUpdater
 | 
				
			||||||
        for (var i in state.jobsRunning) {
 | 
					        let updAt = state.jobsRunning.findIndex(j => j.name === data.name);
 | 
				
			||||||
          if (state.jobsRunning[i].name === data.name) {
 | 
					        if (updAt === -1) {
 | 
				
			||||||
            updAt = i;
 | 
					 | 
				
			||||||
            break;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (updAt === null) {
 | 
					 | 
				
			||||||
          state.jobsRunning.unshift(data);
 | 
					          state.jobsRunning.unshift(data);
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          state.jobsRunning[updAt] = data;
 | 
					          state.jobsRunning[updAt] = data;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        updAt = null;
 | 
					        updAt = state.jobs.findIndex(j => j.name === data.name);
 | 
				
			||||||
        for (var i in state.jobs) {
 | 
					        if (updAt === -1) {
 | 
				
			||||||
          if (state.jobs[i].name === data.name) {
 | 
					 | 
				
			||||||
            updAt = i;
 | 
					 | 
				
			||||||
            break;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (updAt === null) {
 | 
					 | 
				
			||||||
          // 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((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
 | 
				
			||||||
          if(!Object.values(state.regexps).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 {
 | 
				
			||||||
@ -457,19 +519,13 @@ const All = function(templateId) {
 | 
				
			|||||||
        this.$forceUpdate();
 | 
					        this.$forceUpdate();
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      job_completed: function(data) {
 | 
					      job_completed: function(data) {
 | 
				
			||||||
        for (var i in state.jobs) {
 | 
					        let updAt = state.jobs.findIndex(j => j.name === data.name);
 | 
				
			||||||
          if (state.jobs[i].name === data.name) {
 | 
					        if (updAt > -1)
 | 
				
			||||||
            state.jobs[i] = data;
 | 
					          state.jobs[updAt] = data;
 | 
				
			||||||
            // forceUpdate in second loop
 | 
					        updAt = state.jobsRunning.findIndex(j => j.name === data.name);
 | 
				
			||||||
            break;
 | 
					        if (updAt > -1) {
 | 
				
			||||||
          }
 | 
					          state.jobsRunning.splice(updAt, 1);
 | 
				
			||||||
        }
 | 
					          this.$forceUpdate();
 | 
				
			||||||
        for (var i in state.jobsRunning) {
 | 
					 | 
				
			||||||
          if (state.jobsRunning[i].name === data.name) {
 | 
					 | 
				
			||||||
            state.jobsRunning.splice(i, 1);
 | 
					 | 
				
			||||||
            this.$forceUpdate();
 | 
					 | 
				
			||||||
            break;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      filteredJobs: function() {
 | 
					      filteredJobs: function() {
 | 
				
			||||||
@ -500,8 +556,9 @@ const All = function(templateId) {
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var Job = function() {
 | 
					// Component for the /job/:name endpoint
 | 
				
			||||||
  var state = {
 | 
					const Job = templateId => {
 | 
				
			||||||
 | 
					  const state = {
 | 
				
			||||||
    description: '',
 | 
					    description: '',
 | 
				
			||||||
    jobsRunning: [],
 | 
					    jobsRunning: [],
 | 
				
			||||||
    jobsRecent: [],
 | 
					    jobsRecent: [],
 | 
				
			||||||
@ -511,13 +568,10 @@ var Job = function() {
 | 
				
			|||||||
    pages: 0,
 | 
					    pages: 0,
 | 
				
			||||||
    sort: {}
 | 
					    sort: {}
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
  var chtBt = null;
 | 
					  let chtBt = null;
 | 
				
			||||||
  return Vue.extend({
 | 
					  return {
 | 
				
			||||||
    template: '#job',
 | 
					    template: templateId,
 | 
				
			||||||
    mixins: [ServerEventHandler, Utils, ProgressUpdater],
 | 
					    data: () => state,
 | 
				
			||||||
    data: function() {
 | 
					 | 
				
			||||||
      return state;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    methods: {
 | 
					    methods: {
 | 
				
			||||||
      status: function(msg) {
 | 
					      status: function(msg) {
 | 
				
			||||||
        state.description = msg.description;
 | 
					        state.description = msg.description;
 | 
				
			||||||
@ -533,59 +587,7 @@ var Job = function() {
 | 
				
			|||||||
        // old chart and recreate it to prevent flickering of old data
 | 
					        // old chart and recreate it to prevent flickering of old data
 | 
				
			||||||
        if(chtBt)
 | 
					        if(chtBt)
 | 
				
			||||||
          chtBt.destroy();
 | 
					          chtBt.destroy();
 | 
				
			||||||
        const btScale = timeScale(Math.max(...msg.recent.map(v=>v.completed-v.started)));
 | 
					        chtBt = Charts.createRunTimeChart("chartBt", msg.recent, msg.averageRuntime);
 | 
				
			||||||
        chtBt = new Chart(document.getElementById("chartBt"), {
 | 
					 | 
				
			||||||
          type: 'bar',
 | 
					 | 
				
			||||||
          data: {
 | 
					 | 
				
			||||||
            labels: msg.recent.map(function(e) {
 | 
					 | 
				
			||||||
              return '#' + e.number;
 | 
					 | 
				
			||||||
            }).reverse(),
 | 
					 | 
				
			||||||
            datasets: [{
 | 
					 | 
				
			||||||
              label: 'Average',
 | 
					 | 
				
			||||||
              type: 'line',
 | 
					 | 
				
			||||||
              data: [{x:0,y:msg.averageRuntime},{x:1,y:msg.averageRuntime}],
 | 
					 | 
				
			||||||
              borderColor: '#7483af',
 | 
					 | 
				
			||||||
              backgroundColor: 'transparent',
 | 
					 | 
				
			||||||
              xAxisID: 'avg',
 | 
					 | 
				
			||||||
              pointRadius: 0,
 | 
					 | 
				
			||||||
              pointHitRadius: 0,
 | 
					 | 
				
			||||||
              pointHoverRadius: 0,
 | 
					 | 
				
			||||||
            },{
 | 
					 | 
				
			||||||
              label: 'Build time',
 | 
					 | 
				
			||||||
              backgroundColor: msg.recent.map(e => e.result == 'success' ? '#74af77': '#883d3d').reverse(),
 | 
					 | 
				
			||||||
              data: msg.recent.map(function(e) {
 | 
					 | 
				
			||||||
                return e.completed - e.started;
 | 
					 | 
				
			||||||
              }).reverse()
 | 
					 | 
				
			||||||
            }]
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          options: {
 | 
					 | 
				
			||||||
            title: { display: true, text: 'Build time' },
 | 
					 | 
				
			||||||
            hover: { mode: null },
 | 
					 | 
				
			||||||
            scales:{
 | 
					 | 
				
			||||||
              xAxes:[{
 | 
					 | 
				
			||||||
                categoryPercentage: 1.0,
 | 
					 | 
				
			||||||
                barPercentage: 1.0
 | 
					 | 
				
			||||||
              },{
 | 
					 | 
				
			||||||
                id: 'avg',
 | 
					 | 
				
			||||||
                type: 'linear',
 | 
					 | 
				
			||||||
                ticks: {
 | 
					 | 
				
			||||||
                  display: false
 | 
					 | 
				
			||||||
                },
 | 
					 | 
				
			||||||
                gridLines: {
 | 
					 | 
				
			||||||
                  display: false,
 | 
					 | 
				
			||||||
                  drawBorder: false
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
              }],
 | 
					 | 
				
			||||||
              yAxes:[{
 | 
					 | 
				
			||||||
                ticks:{userCallback: btScale.scale},
 | 
					 | 
				
			||||||
                scaleLabel:{display: true, labelString: btScale.label}
 | 
					 | 
				
			||||||
              }]
 | 
					 | 
				
			||||||
            },
 | 
					 | 
				
			||||||
            tooltips:{callbacks:{label:(tip, data)=>{
 | 
					 | 
				
			||||||
              return data.datasets[tip.datasetIndex].label + ': ' + tip.yLabel + ' ' + btScale.label.toLowerCase();
 | 
					 | 
				
			||||||
            }}}
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      job_queued: function() {
 | 
					      job_queued: function() {
 | 
				
			||||||
        state.nQueued++;
 | 
					        state.nQueued++;
 | 
				
			||||||
@ -596,15 +598,12 @@ var Job = function() {
 | 
				
			|||||||
        this.$forceUpdate();
 | 
					        this.$forceUpdate();
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      job_completed: function(data) {
 | 
					      job_completed: function(data) {
 | 
				
			||||||
        for (var i = 0; i < state.jobsRunning.length; ++i) {
 | 
					        const i = state.jobsRunning.findIndex(j => j.number === data.number);
 | 
				
			||||||
          var job = state.jobsRunning[i];
 | 
					        if (i > -1) {
 | 
				
			||||||
          if (job.number === data.number) {
 | 
					 | 
				
			||||||
            state.jobsRunning.splice(i, 1);
 | 
					            state.jobsRunning.splice(i, 1);
 | 
				
			||||||
            state.jobsRecent.splice(0, 0, data);
 | 
					            state.jobsRecent.splice(0, 0, data);
 | 
				
			||||||
            this.$forceUpdate();
 | 
					            this.$forceUpdate();
 | 
				
			||||||
            // TODO: update the chart
 | 
					            // TODO: update the chart
 | 
				
			||||||
            break;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      page_next: function() {
 | 
					      page_next: function() {
 | 
				
			||||||
@ -625,12 +624,13 @@ var Job = function() {
 | 
				
			|||||||
        this.query(state.sort)
 | 
					        this.query(state.sort)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  });
 | 
					  };
 | 
				
			||||||
}();
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Run = function() {
 | 
					// Component for the /job/:name/:number endpoint
 | 
				
			||||||
 | 
					const Run = templateId => {
 | 
				
			||||||
  const utf8decoder = new TextDecoder('utf-8');
 | 
					  const utf8decoder = new TextDecoder('utf-8');
 | 
				
			||||||
  var state = {
 | 
					  const state = {
 | 
				
			||||||
    job: { artifacts: [], upstream: {} },
 | 
					    job: { artifacts: [], upstream: {} },
 | 
				
			||||||
    latestNum: null,
 | 
					    latestNum: null,
 | 
				
			||||||
    log: '',
 | 
					    log: '',
 | 
				
			||||||
@ -641,13 +641,19 @@ const Run = function() {
 | 
				
			|||||||
      // ATOW pipeThrough not supported in Firefox
 | 
					      // ATOW pipeThrough not supported in Firefox
 | 
				
			||||||
      //const reader = res.body.pipeThrough(new TextDecoderStream).getReader();
 | 
					      //const reader = res.body.pipeThrough(new TextDecoderStream).getReader();
 | 
				
			||||||
      const reader = res.body.getReader();
 | 
					      const reader = res.body.getReader();
 | 
				
			||||||
      let total = 0;
 | 
					 | 
				
			||||||
      return function pump() {
 | 
					      return function pump() {
 | 
				
			||||||
        return reader.read().then(({done, value}) => {
 | 
					        return reader.read().then(({done, value}) => {
 | 
				
			||||||
          value = utf8decoder.decode(value);
 | 
					          value = utf8decoder.decode(value);
 | 
				
			||||||
          if (done)
 | 
					          if (done)
 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
          state.log += ansi_up.ansi_to_html(value.replace(/</g,'<').replace(/>/g,'>').replace(/\033\[\{([^:]+):(\d+)\033\\/g, (m,$1,$2)=>{return '<a href="jobs/'+$1+'" onclick="return vroute(this);">'+$1+'</a>:<a href="jobs/'+$1+'/'+$2+'" onclick="return vroute(this);">#'+$2+'</a>';}));
 | 
					          state.log += ansi_up.ansi_to_html(
 | 
				
			||||||
 | 
					            value.replace(/</g,'<')
 | 
				
			||||||
 | 
					                 .replace(/>/g,'>')
 | 
				
			||||||
 | 
					                 .replace(/\033\[\{([^:]+):(\d+)\033\\/g, (m, $1, $2) =>
 | 
				
			||||||
 | 
					                   '<a href="jobs/'+$1+'" onclick="return vroute(this);">'+$1+'</a>:'+
 | 
				
			||||||
 | 
					                   '<a href="jobs/'+$1+'/'+$2+'" onclick="return vroute(this);">#'+$2+'</a>'
 | 
				
			||||||
 | 
					                 )
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
          vm.$forceUpdate();
 | 
					          vm.$forceUpdate();
 | 
				
			||||||
          return pump();
 | 
					          return pump();
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
@ -655,13 +661,9 @@ const Run = function() {
 | 
				
			|||||||
    }).catch(e => {});
 | 
					    }).catch(e => {});
 | 
				
			||||||
    return abort;
 | 
					    return abort;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    template: '#run',
 | 
					    template: templateId,
 | 
				
			||||||
    mixins: [ServerEventHandler, Utils, ProgressUpdater],
 | 
					    data: () => state,
 | 
				
			||||||
    data: function() {
 | 
					 | 
				
			||||||
      return state;
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    methods: {
 | 
					    methods: {
 | 
				
			||||||
      status: function(data, params) {
 | 
					      status: function(data, params) {
 | 
				
			||||||
        // Check for the /latest endpoint
 | 
					        // Check for the /latest endpoint
 | 
				
			||||||
@ -705,72 +707,47 @@ const Run = function() {
 | 
				
			|||||||
      },
 | 
					      },
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}();
 | 
					};
 | 
				
			||||||
 | 
					 | 
				
			||||||
// For all charts, set miniumum Y to 0
 | 
					 | 
				
			||||||
Chart.scaleService.updateScaleDefaults('linear', {
 | 
					 | 
				
			||||||
    ticks: { suggestedMin: 0 }
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
// Don't display legend by default
 | 
					 | 
				
			||||||
Chart.defaults.global.legend.display = false;
 | 
					 | 
				
			||||||
// Disable tooltip hover animations
 | 
					 | 
				
			||||||
Chart.defaults.global.hover.animationDuration = 0;
 | 
					 | 
				
			||||||
// Plugin to move a DOM item on top of a chart element
 | 
					 | 
				
			||||||
Chart.plugins.register({
 | 
					 | 
				
			||||||
  afterDatasetsDraw: (chart) => {
 | 
					 | 
				
			||||||
    chart.data.datasets.forEach((dataset, i) => {
 | 
					 | 
				
			||||||
      var meta = chart.getDatasetMeta(i);
 | 
					 | 
				
			||||||
      if(dataset.itemid)
 | 
					 | 
				
			||||||
        meta.data.forEach((e,j) => {
 | 
					 | 
				
			||||||
          var pos = e.getCenterPoint();
 | 
					 | 
				
			||||||
          var node = document.getElementById(dataset.itemid[j]);
 | 
					 | 
				
			||||||
          node.style.top = (pos.y - node.clientHeight/2) + 'px';
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
new Vue({
 | 
					new Vue({
 | 
				
			||||||
  el: '#app',
 | 
					  el: '#app',
 | 
				
			||||||
  data: {
 | 
					  data: {
 | 
				
			||||||
    title: '', // populated by status ws message
 | 
					    title: '', // populated by status message
 | 
				
			||||||
    version: '',
 | 
					    version: '',
 | 
				
			||||||
    clockSkew: 0,
 | 
					    clockSkew: 0,
 | 
				
			||||||
    connected: false,
 | 
					    connected: false,
 | 
				
			||||||
    notify: 'localStorage' in window && localStorage.getItem('showNotifications') == 1
 | 
					    notify: 'localStorage' in window && localStorage.getItem('showNotifications') == 1
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  computed: {
 | 
					  computed: {
 | 
				
			||||||
    supportsNotifications() {
 | 
					    supportsNotifications: () =>
 | 
				
			||||||
      return 'Notification' in window && Notification.permission !== 'denied';
 | 
					      'Notification' in window && Notification.permission !== 'denied'
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  methods: {
 | 
					  methods: {
 | 
				
			||||||
    toggleNotifications(en) {
 | 
					    toggleNotifications: function(en) {
 | 
				
			||||||
      if(Notification.permission !== 'granted')
 | 
					      if(Notification.permission !== 'granted')
 | 
				
			||||||
        Notification.requestPermission(p => this.notify = (p === 'granted'))
 | 
					        Notification.requestPermission(p => this.notify = (p === 'granted'))
 | 
				
			||||||
      else
 | 
					      else
 | 
				
			||||||
        this.notify = en;
 | 
					        this.notify = en;
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    showNotify(msg, data) {
 | 
					    showNotify: function(msg, data) {
 | 
				
			||||||
      if(this.notify && msg === 'job_completed')
 | 
					      if(this.notify && msg === 'job_completed')
 | 
				
			||||||
        new Notification('Job ' + data.result, {
 | 
					        new Notification('Job ' + data.result, {
 | 
				
			||||||
          body: data.name + ' ' + '#' + data.number + ': ' + data.result
 | 
					          body: data.name + ' ' + '#' + data.number + ': ' + data.result
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    },
 | 
					    }
 | 
				
			||||||
    runIcon: Utils.methods.runIcon
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  watch: {
 | 
					  watch: {
 | 
				
			||||||
    notify(e) { localStorage.setItem('showNotifications', e ? 1 : 0); }
 | 
					    notify: e => localStorage.setItem('showNotifications', e ? 1 : 0)
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  router: new VueRouter({
 | 
					  router: new VueRouter({
 | 
				
			||||||
    mode: 'history',
 | 
					    mode: 'history',
 | 
				
			||||||
    base: document.head.baseURI.substr(location.origin.length),
 | 
					    base: document.head.baseURI.substr(location.origin.length),
 | 
				
			||||||
    routes: [
 | 
					    routes: [
 | 
				
			||||||
      { path: '/',                   component: Home },
 | 
					      { path: '/',                   component: Home('#home') },
 | 
				
			||||||
      { path: '/jobs',               component: All('#jobs') },
 | 
					      { path: '/jobs',               component:  All('#jobs') },
 | 
				
			||||||
      { path: '/wallboard',          component: All('#wallboard') },
 | 
					      { path: '/wallboard',          component:  All('#wallboard') },
 | 
				
			||||||
      { path: '/jobs/:name',         component: Job },
 | 
					      { path: '/jobs/:name',         component:  Job('#job') },
 | 
				
			||||||
      { path: '/jobs/:name/:number', component: Run }
 | 
					      { path: '/jobs/:name/:number', component:  Run('#run') }
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user