Strider-CD/strider

View on GitHub
clients/classic-ui/client/job-status/controllers/job.js

Summary

Maintainability
F
4 days
Test Coverage
'use strict';

var _ = require('lodash');
var bootbox = require('bootbox');
var $ = require('jquery');
var io = require('socket.io-client');
var JobDataMonitor = require('../../utils/job-data-monitor');
var PHASES = require('../../utils/phases');
var SKELS = require('../../utils/skels');
var statusClasses = require('../../utils/status-classes');
var outputConsole;
var runtime = null;
var job = global.job;

module.exports = function($scope, $route, $location, $filter) {
  var params = $route.current ? $route.current.params : {};
  var project = global.project;
  var jobid = params.id || (global.job && global.job._id);
  var socket = io.connect();
  var lastRoute = $route.current;
  var jobman = new BuildPage(
    socket,
    project.name,
    $scope.$digest.bind($scope),
    $scope,
    global.jobs,
    global.job
  );

  outputConsole = global.document.querySelector('.console-output');

  $scope.statusClasses = statusClasses;
  $scope.phases = ['environment', 'prepare', 'test', 'deploy', 'cleanup'];
  $scope.project = project;
  $scope.jobs = global.jobs;
  $scope.job = global.job;
  $scope.canAdminProject = global.canAdminProject;
  $scope.showStatus = global.showStatus;

  if ($scope.job && $scope.job.phases.test.commands.length) {
    if (job.phases.environment) {
      job.phases.environment.collapsed = true;
    }
    if (job.phases.prepare) {
      job.phases.prepare.collapsed = true;
    }
    if (job.phases.cleanup) {
      job.phases.cleanup.collapsed = true;
    }
  }

  $scope.toggleErrorDetails = function() {
    $scope.showErrorDetails = !$scope.showErrorDetails;
  };

  $scope.clearCache = function() {
    $scope.clearingCache = true;
    $.ajax(`/${$scope.project.name}/cache/${$scope.job.ref.branch}`, {
      type: 'DELETE',
      success: function() {
        $scope.clearingCache = false;
        $scope.$digest();
      },
      error: function() {
        $scope.clearingCache = false;
        $scope.$digest();
        bootbox.alert('Failed to clear the cache');
      }
    });
  };

  $scope.$on('$locationChangeSuccess', handleLocationChange);
  // Treat the initial page load as a forced route change to make sure that the correct job is displayed.
  // If we don't do this, the top-most job will be shown, even through the URL points to a different job.
  handleLocationChange(true);

  function handleLocationChange(force) {
    if (global.location.pathname.match(/\/config$/)) {
      // eslint-disable-next-line no-self-assign
      global.location = global.location;
      return;
    }
    params = $route.current.params;
    if (!params.id) params.id = $scope.jobs[0]._id;
    // don't refresh the page
    $route.current = lastRoute;
    if (jobid !== params.id || force) {
      jobid = params.id;
      var cached = jobman.get(jobid, function(err, job, cached) {
        if (job.phases.environment) {
          job.phases.environment.collapsed = true;
        }
        if (job.phases.prepare) {
          job.phases.prepare.collapsed = true;
        }
        if (job.phases.cleanup) {
          job.phases.cleanup.collapsed = true;
        }
        $scope.job = job;
        if ($scope.job.phases.test.commands.length) {
          $scope.job.phases.environment.collapsed = true;
          $scope.job.phases.prepare.collapsed = true;
          $scope.job.phases.cleanup.collapsed = true;
        }
        if (!cached) $scope.$digest();
      });
      if (!cached) {
        for (var i = 0; i < $scope.jobs.length; i++) {
          if ($scope.jobs[i]._id === jobid) {
            $scope.job = $scope.jobs[i];
            break;
          }
        }
      }
    }
  }

  $scope.triggers = {
    commit: {
      icon: 'code-fork',
      title: 'Commit'
    },
    manual: {
      icon: 'hand-o-right',
      title: 'Manual'
    },
    plugin: {
      icon: 'puzzle-piece',
      title: 'Plugin'
    },
    api: {
      icon: 'cloud',
      title: 'Cloud'
    }
  };

  // shared templates ; need to know what to show
  $scope.page = 'build';
  // a history item is clicked
  $scope.selectJob = function(id) {
    $location.path(`/job/${id}`).replace();
  };

  // set the favicon according to job status
  $scope.$watch('job.status', function(value) {
    updateFavicon(value);
  });

  buildSwitcher($scope);

  $scope.$watch('job.std.merged_latest', function(value) {
    /* Tracking isn't quite working right
     if ($scope.job.status === 'running') {
     height = outputConsole.getBoundingClientRect().height;
     tracking = height + outputConsole.scrollTop > outputConsole.scrollHeight - 50;
     // console.log(tracking, height, outputConsole.scrollTop, outputConsole.scrollHeight);
     if (!tracking) return;
     }
     */
    var ansiFilter = $filter('ansi');
    $('.job-output')
      .last()
      .append(ansiFilter(value));
    outputConsole.scrollTop = outputConsole.scrollHeight;
    setTimeout(function() {
      outputConsole.scrollTop = outputConsole.scrollHeight;
    }, 10);
  });
  // button handlers
  $scope.startDeploy = function(job) {
    $('.tooltip').hide();
    socket.emit('deploy', project.name, job && job.ref.branch);
    $scope.job = {
      project: $scope.job.project,
      status: 'submitted'
    };
  };
  $scope.startTest = function(job) {
    $('.tooltip').hide();
    socket.emit('test', project.name, job && job.ref.branch);
    $scope.job = {
      project: $scope.job.project,
      status: 'submitted'
    };
  };
  $scope.restartJob = function(job) {
    socket.emit('restart', job);
  };
  $scope.cancelJob = function(id) {
    socket.emit('cancel', id);
  };
};

function BuildPage(socket, project, change, scope, jobs, job) {
  JobDataMonitor.call(this, socket, change);
  this.scope = scope;
  this.project = project;
  this.jobs = {};
  this.jobs[job._id] = job;
}

_.extend(BuildPage.prototype, JobDataMonitor.prototype, {
  emits: {
    getUnknown: 'build:job'
  },

  job(id) {
    return this.jobs[id];
  },

  addJob(job) {
    if ((job.project.name || job.project) !== this.project) return;
    this.jobs[job._id] = job;
    var found = -1;

    for (var i = 0; i < this.scope.jobs.length; i++) {
      if (this.scope.jobs[i]._id === job._id) {
        found = i;
        break;
      }
    }
    if (found !== -1) {
      this.scope.jobs.splice(found, 1);
    }
    if (!job.phase) job.phase = 'environment';
    if (!job.std) {
      job.std = {
        out: '',
        err: '',
        merged: ''
      };
    }
    if (!job.phases) {
      job.phases = {};
      for (i = 0; i < PHASES.length; i++) {
        job.phases[PHASES[i]] = _.cloneDeep(SKELS.phase);
      }
      job.phases[job.phase].started = new Date();
    } else {
      if (job.phases.test.commands.length) {
        if (job.phases.environment) {
          job.phases.environment.collapsed = true;
        }
        if (job.phases.prepare) {
          job.phases.prepare.collapsed = true;
        }
        if (job.phases.cleanup) {
          job.phases.cleanup.collapsed = true;
        }
      }
    }

    this.scope.jobs.unshift(job);
    this.scope.job = job;
  },

  get(id, done) {
    if (this.jobs[id]) {
      done(null, this.jobs[id], true);
      return true;
    }
    var self = this;
    this.sock.emit('build:job', id, function(job) {
      self.jobs[id] = job;
      done(null, job);
    });
  }
});

/** manage the favicons **/
function setFavicon(status) {
  $('link[rel*="icon"]').attr('href', `/images/icons/favicon-${status}.png`);
}

function animateFav() {
  var alt = false;

  function switchit() {
    setFavicon(`running${alt ? '-alt' : ''}`);
    alt = !alt;
  }

  return setInterval(switchit, 500);
}

function updateFavicon(value) {
  if (value === 'running') {
    if (runtime === null) {
      runtime = animateFav();
    }
  } else {
    if (runtime !== null) {
      clearInterval(runtime);
      runtime = null;
    }
    setFavicon(value);
  }
}

function buildSwitcher($scope) {
  function switchBuilds(evt) {
    var dy = { 40: 1, 38: -1 }[evt.keyCode];
    var id = $scope.job._id;
    var idx;
    if (!dy) return;
    for (var i = 0; i < $scope.jobs.length; i++) {
      if ($scope.jobs[i]._id === id) {
        idx = i;
        break;
      }
    }
    if (idx === -1) {
      console.log('Failed to find job.');
      // eslint-disable-next-line no-self-assign
      return (global.location = global.location);
    }
    idx += dy;
    if (idx < 0 || idx >= $scope.jobs.length) {
      return;
    }
    evt.preventDefault();
    $scope.selectJob($scope.jobs[idx]._id);
    $scope.$root.$digest();
  }

  global.document.addEventListener('keydown', switchBuilds);
}