(function() { /** * Repository Build view page. Displays the status of a repository build. */ angular.module('quayPages').config(['pages', function(pages) { pages.create('repo-build', 'repo-build.html', RepoBuildCtrl); }]); function RepoBuildCtrl($scope, Restangular, ApiService, $routeParams, $rootScope, $location, $interval, $sanitize, ansi2html, AngularViewArray, AngularPollChannel) { var namespace = $routeParams.namespace; var name = $routeParams.name; // Watch for changes to the current parameter. $scope.$on('$routeUpdate', function(){ if ($location.search().current) { $scope.setCurrentBuild($location.search().current, false); } }); $scope.builds = null; $scope.pollChannel = null; $scope.buildDialogShowCounter = 0; $scope.showNewBuildDialog = function() { $scope.buildDialogShowCounter++; }; $scope.handleBuildStarted = function(newBuild) { if (!$scope.builds) { return; } $scope.builds.unshift(newBuild); $scope.setCurrentBuild(newBuild['id'], true); }; $scope.adjustLogHeight = function() { var triggerOffset = 0; if ($scope.currentBuild && $scope.currentBuild.trigger) { triggerOffset = 85; } $('.build-logs').height($(window).height() - 415 - triggerOffset); }; $scope.askRestartBuild = function(build) { $('#confirmRestartBuildModal').modal({}); }; $scope.askCancelBuild = function(build) { bootbox.confirm('Are you sure you want to cancel this build?', function(r) { if (r) { var params = { 'repository': namespace + '/' + name, 'build_uuid': build.id }; ApiService.cancelRepoBuild(null, params).then(function() { if (!$scope.builds) { return; } $scope.builds.splice($.inArray(build, $scope.builds), 1); if ($scope.builds.length) { $scope.currentBuild = $scope.builds[0]; } else { $scope.currentBuild = null; } }, ApiService.errorDisplay('Cannot cancel build')); } }); }; $scope.restartBuild = function(build) { $('#confirmRestartBuildModal').modal('hide'); var subdirectory = ''; if (build['job_config']) { subdirectory = build['job_config']['build_subdir'] || ''; } var data = { 'file_id': build['resource_key'], 'subdirectory': subdirectory, 'docker_tags': build['job_config']['docker_tags'] }; if (build['pull_robot']) { data['pull_robot'] = build['pull_robot']['name']; } var params = { 'repository': namespace + '/' + name }; ApiService.requestRepoBuild(data, params).then(function(newBuild) { if (!$scope.builds) { return; } $scope.builds.unshift(newBuild); $scope.setCurrentBuild(newBuild['id'], true); }); }; $scope.hasLogs = function(container) { return container.logs.hasEntries; }; $scope.setCurrentBuild = function(buildId, opt_updateURL) { if (!$scope.builds) { return; } // Find the build. for (var i = 0; i < $scope.builds.length; ++i) { if ($scope.builds[i].id == buildId) { $scope.setCurrentBuildInternal(i, $scope.builds[i], opt_updateURL); return; } } }; $scope.processANSI = function(message, container) { var filter = container.logs._filter = (container.logs._filter || ansi2html.create()); // Note: order is important here. var setup = filter.getSetupHtml(); var stream = filter.addInputToStream(message); var teardown = filter.getTeardownHtml(); return setup + stream + teardown; }; $scope.setCurrentBuildInternal = function(index, build, opt_updateURL) { if (build == $scope.currentBuild) { return; } $scope.logEntries = null; $scope.logStartIndex = null; $scope.currentParentEntry = null; $scope.currentBuild = build; if (opt_updateURL) { if (build) { $location.search('current', build.id); } else { $location.search('current', null); } } // Timeout needed to ensure the log element has been created // before its height is adjusted. setTimeout(function() { $scope.adjustLogHeight(); }, 1); // Stop any existing polling. if ($scope.pollChannel) { $scope.pollChannel.stop(); } // Create a new channel for polling the build status and logs. var conductStatusAndLogRequest = function(callback) { getBuildStatusAndLogs(build, callback); }; $scope.pollChannel = AngularPollChannel.create($scope, conductStatusAndLogRequest, 5 * 1000 /* 5s */); $scope.pollChannel.start(); }; var processLogs = function(logs, startIndex, endIndex) { if (!$scope.logEntries) { $scope.logEntries = []; } // If the start index given is less than that requested, then we've received a larger // pool of logs, and we need to only consider the new ones. if (startIndex < $scope.logStartIndex) { logs = logs.slice($scope.logStartIndex - startIndex); } for (var i = 0; i < logs.length; ++i) { var entry = logs[i]; var type = entry['type'] || 'entry'; if (type == 'command' || type == 'phase' || type == 'error') { entry['logs'] = AngularViewArray.create(); entry['index'] = $scope.logStartIndex + i; $scope.logEntries.push(entry); $scope.currentParentEntry = entry; } else if ($scope.currentParentEntry) { $scope.currentParentEntry['logs'].push(entry); } } return endIndex; }; var getBuildStatusAndLogs = function(build, callback) { var params = { 'repository': namespace + '/' + name, 'build_uuid': build.id }; ApiService.getRepoBuildStatus(null, params, true).then(function(resp) { if (build != $scope.currentBuild) { callback(false); return; } // Note: We use extend here rather than replacing as Angular is depending on the // root build object to remain the same object. var matchingBuilds = $.grep($scope.builds, function(elem) { return elem['id'] == resp['id'] }); var currentBuild = matchingBuilds.length > 0 ? matchingBuilds[0] : null; if (currentBuild) { currentBuild = $.extend(true, currentBuild, resp); } else { currentBuild = resp; $scope.builds.push(currentBuild); } // Load the updated logs for the build. var options = { 'start': $scope.logStartIndex }; ApiService.getRepoBuildLogsAsResource(params, true).withOptions(options).get(function(resp) { if (build != $scope.currentBuild) { callback(false); return; } // Process the logs we've received. $scope.logStartIndex = processLogs(resp['logs'], resp['start'], resp['total']); // If the build status is an error, open the last two log entries. if (currentBuild['phase'] == 'error' && $scope.logEntries.length > 1) { var openLogEntries = function(entry) { if (entry.logs) { entry.logs.setVisible(true); } }; openLogEntries($scope.logEntries[$scope.logEntries.length - 2]); openLogEntries($scope.logEntries[$scope.logEntries.length - 1]); } // If the build phase is an error or a complete, then we mark the channel // as closed. callback(currentBuild['phase'] != 'error' && currentBuild['phase'] != 'complete'); }, function() { callback(false); }); }, function() { callback(false); }); }; var fetchRepository = function() { var params = {'repository': namespace + '/' + name}; $rootScope.title = 'Loading Repository...'; $scope.repository = ApiService.getRepoAsResource(params).get(function(repo) { if (!repo.can_write) { $rootScope.title = 'Unknown builds'; $scope.accessDenied = true; return; } $rootScope.title = 'Repository Builds'; $scope.repo = repo; getBuildInfo(); }); }; var getBuildInfo = function(repo) { var params = { 'repository': namespace + '/' + name }; ApiService.getRepoBuilds(null, params).then(function(resp) { $scope.builds = resp.builds; if ($location.search().current) { $scope.setCurrentBuild($location.search().current, false); } else if ($scope.builds.length > 0) { $scope.setCurrentBuild($scope.builds[0].id, true); } }); }; fetchRepository(); } })();